@warlock.js/ai-panoptic 5.30.0 → 5.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -3
- package/README.md +2 -2
- package/cjs/index.cjs +22 -19
- package/cjs/index.cjs.map +1 -1
- package/esm/collector/extract-span-attributes.d.mts.map +1 -1
- package/esm/collector/extract-span-attributes.mjs +1 -0
- package/esm/collector/extract-span-attributes.mjs.map +1 -1
- package/esm/collector/report-to-span.mjs.map +1 -1
- package/esm/config/apply-panoptic-config.mjs.map +1 -1
- package/esm/dashboard/dashboard.mjs.map +1 -1
- package/esm/dashboard/parse-query.mjs +1 -0
- package/esm/dashboard/parse-query.mjs.map +1 -1
- package/esm/dashboard/serve.mjs.map +1 -1
- package/esm/dashboard/trace-filter.d.mts.map +1 -1
- package/esm/dashboard/trace-filter.mjs +1 -1
- package/esm/dashboard/trace-filter.mjs.map +1 -1
- package/esm/dashboard/ui.html.mjs +5 -4
- package/esm/dashboard/ui.html.mjs.map +1 -1
- package/esm/exporters/console/console-exporter.mjs +4 -3
- package/esm/exporters/console/console-exporter.mjs.map +1 -1
- package/esm/exporters/console/format-span-io.d.mts.map +1 -1
- package/esm/exporters/console/format-span-io.mjs.map +1 -1
- package/esm/exporters/console/format-span-line.mjs +2 -0
- package/esm/exporters/console/format-span-line.mjs.map +1 -1
- package/esm/exporters/console/index.mjs +5 -0
- package/esm/exporters/file/file-exporter.mjs.map +1 -1
- package/esm/exporters/index.mjs +2 -0
- package/esm/exporters/langfuse/langfuse-exporter.mjs +3 -2
- package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
- package/esm/exporters/otel/otel-api.shim.type.d.mts.map +1 -1
- package/esm/exporters/otel/otel-exporter.mjs +2 -1
- package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
- package/esm/exporters/utils/index.mjs +5 -0
- package/esm/exporters/utils/total-cost.d.mts +2 -4
- package/esm/exporters/utils/total-cost.d.mts.map +1 -1
- package/esm/exporters/utils/total-cost.mjs +5 -7
- package/esm/exporters/utils/total-cost.mjs.map +1 -1
- package/esm/panoptic/panoptic-middleware.mjs.map +1 -1
- package/esm/panoptic/panoptic.d.mts.map +1 -1
- package/esm/panoptic/panoptic.mjs.map +1 -1
- package/esm/store/cache-trace-store.d.mts.map +1 -1
- package/esm/store/cache-trace-store.mjs +1 -0
- package/esm/store/cache-trace-store.mjs.map +1 -1
- package/esm/store/in-memory-trace-store.d.mts.map +1 -1
- package/esm/store/in-memory-trace-store.mjs +3 -1
- package/esm/store/in-memory-trace-store.mjs.map +1 -1
- package/esm/store/sum-usage.mjs.map +1 -1
- package/esm/store/trace-aggregate.type.d.mts +2 -6
- package/esm/store/trace-aggregate.type.d.mts.map +1 -1
- package/llms-full.txt +134 -117
- package/package.json +4 -4
- package/skills/evaluate-system-prompt/SKILL.md +21 -17
- package/skills/export-traces/SKILL.md +32 -24
- package/skills/index.md +3 -0
- package/skills/observe-with-panoptic/SKILL.md +18 -18
- package/skills/query-traces/SKILL.md +31 -33
- package/skills/use-local-dashboard/SKILL.md +33 -26
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-exporter.mjs","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/file/file-exporter.ts"],"sourcesContent":["import { appendFile, mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { ExporterContract, Trace } from \"../../contracts\";\nimport type { FileExporterOptions, TraceRecord } from \"./file-exporter.type\";\n\nconst EXPORTER_NAME = \"file\";\n\n/**\n * Zero-dependency {@link ExporterContract} that appends completed traces\n * to a JSON-Lines file (one JSON record per line by default). Buffers in\n * memory and flushes either every `flushEvery` traces or on an explicit\n * `flush()` / `shutdown()`, so a batch of traces costs one append.\n *\n * Useful as a durable local sink (replay traces later, ship the file to\n * a backend out of band) and as a test fixture for the pipeline without\n * a vendor SDK.\n *\n * @example\n * collector.use(fileExporter({ path: \"storage/traces.jsonl\" }));\n * // on shutdown:\n * await collector.shutdown(); // drains the buffer\n */\nexport function fileExporter(options: FileExporterOptions): ExporterContract {\n const writer = new FileTraceWriter(options);\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n await writer.add(trace);\n },\n async flush(): Promise<void> {\n await writer.flush();\n },\n async shutdown(): Promise<void> {\n await writer.flush();\n },\n };\n}\n\n/**\n * Internal buffered writer for {@link fileExporter}. Owns the pending\n * trace buffer and the directory-created guard across the exporter's\n * lifetime; kept unexported so callers only ever see the factory.\n */\nclass FileTraceWriter {\n private readonly path: string;\n private readonly flushEvery: number;\n private readonly pretty: boolean;\n private buffer: TraceRecord[] = [];\n private directoryReady = false;\n\n public constructor(options: FileExporterOptions) {\n this.path = options.path;\n this.flushEvery = Math.max(1, options.flushEvery ?? 1);\n this.pretty = options.pretty ?? false;\n }\n\n /**\n * Buffer one trace and flush when the buffer reaches `flushEvery`.\n */\n public async add(trace: Trace): Promise<void> {\n this.buffer.push({\n type: \"trace\",\n exportedAt: new Date().toISOString(),\n trace,\n });\n\n if (this.buffer.length >= this.flushEvery) {\n await this.flush();\n }\n }\n\n /**\n * Serialize and append every buffered record, then clear the buffer.\n * No-op when nothing is pending so callers can flush defensively.\n */\n public async flush(): Promise<void> {\n if (this.buffer.length === 0) {\n return;\n }\n\n const pending = this.buffer;\n this.buffer = [];\n\n await this.ensureDirectory();\n\n const payload = pending.map((record) => this.serialize(record)).join(\"\");\n\n await appendFile(this.path, payload, \"utf8\");\n }\n\n /**\n * Create the parent directory once, lazily, on the first write. Stores\n * a guard so subsequent flushes skip the syscall.\n */\n private async ensureDirectory(): Promise<void> {\n if (this.directoryReady) {\n return;\n }\n\n await mkdir(dirname(this.path), { recursive: true });\n this.directoryReady = true;\n }\n\n /**\n * Render one record as a newline-terminated JSON string. Pretty mode\n * indents for human reading; compact mode keeps the file valid JSON\n * Lines (exactly one record per physical line).\n */\n private serialize(record: TraceRecord): string {\n const json = this.pretty
|
|
1
|
+
{"version":3,"file":"file-exporter.mjs","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/file/file-exporter.ts"],"sourcesContent":["import { appendFile, mkdir } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\nimport type { ExporterContract, Trace } from \"../../contracts\";\nimport type { FileExporterOptions, TraceRecord } from \"./file-exporter.type\";\n\nconst EXPORTER_NAME = \"file\";\n\n/**\n * Zero-dependency {@link ExporterContract} that appends completed traces\n * to a JSON-Lines file (one JSON record per line by default). Buffers in\n * memory and flushes either every `flushEvery` traces or on an explicit\n * `flush()` / `shutdown()`, so a batch of traces costs one append.\n *\n * Useful as a durable local sink (replay traces later, ship the file to\n * a backend out of band) and as a test fixture for the pipeline without\n * a vendor SDK.\n *\n * @example\n * collector.use(fileExporter({ path: \"storage/traces.jsonl\" }));\n * // on shutdown:\n * await collector.shutdown(); // drains the buffer\n */\nexport function fileExporter(options: FileExporterOptions): ExporterContract {\n const writer = new FileTraceWriter(options);\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n await writer.add(trace);\n },\n async flush(): Promise<void> {\n await writer.flush();\n },\n async shutdown(): Promise<void> {\n await writer.flush();\n },\n };\n}\n\n/**\n * Internal buffered writer for {@link fileExporter}. Owns the pending\n * trace buffer and the directory-created guard across the exporter's\n * lifetime; kept unexported so callers only ever see the factory.\n */\nclass FileTraceWriter {\n private readonly path: string;\n private readonly flushEvery: number;\n private readonly pretty: boolean;\n private buffer: TraceRecord[] = [];\n private directoryReady = false;\n\n public constructor(options: FileExporterOptions) {\n this.path = options.path;\n this.flushEvery = Math.max(1, options.flushEvery ?? 1);\n this.pretty = options.pretty ?? false;\n }\n\n /**\n * Buffer one trace and flush when the buffer reaches `flushEvery`.\n */\n public async add(trace: Trace): Promise<void> {\n this.buffer.push({\n type: \"trace\",\n exportedAt: new Date().toISOString(),\n trace,\n });\n\n if (this.buffer.length >= this.flushEvery) {\n await this.flush();\n }\n }\n\n /**\n * Serialize and append every buffered record, then clear the buffer.\n * No-op when nothing is pending so callers can flush defensively.\n */\n public async flush(): Promise<void> {\n if (this.buffer.length === 0) {\n return;\n }\n\n const pending = this.buffer;\n this.buffer = [];\n\n await this.ensureDirectory();\n\n const payload = pending.map((record) => this.serialize(record)).join(\"\");\n\n await appendFile(this.path, payload, \"utf8\");\n }\n\n /**\n * Create the parent directory once, lazily, on the first write. Stores\n * a guard so subsequent flushes skip the syscall.\n */\n private async ensureDirectory(): Promise<void> {\n if (this.directoryReady) {\n return;\n }\n\n await mkdir(dirname(this.path), { recursive: true });\n this.directoryReady = true;\n }\n\n /**\n * Render one record as a newline-terminated JSON string. Pretty mode\n * indents for human reading; compact mode keeps the file valid JSON\n * Lines (exactly one record per physical line).\n */\n private serialize(record: TraceRecord): string {\n const json = this.pretty ? JSON.stringify(record, undefined, 2) : JSON.stringify(record);\n\n return `${json}\\n`;\n }\n}\n"],"mappings":";;;;AAKA,MAAM,gBAAgB;;;;;;;;;;;;;;;;AAiBtB,SAAgB,aAAa,SAAgD;CAC3E,MAAM,SAAS,IAAI,gBAAgB,OAAO;CAE1C,OAAO;EACL,MAAM;EACN,MAAM,OAAO,OAA6B;GACxC,MAAM,OAAO,IAAI,KAAK;EACxB;EACA,MAAM,QAAuB;GAC3B,MAAM,OAAO,MAAM;EACrB;EACA,MAAM,WAA0B;GAC9B,MAAM,OAAO,MAAM;EACrB;CACF;AACF;;;;;;AAOA,IAAM,kBAAN,MAAsB;CACpB,AAAiB;CACjB,AAAiB;CACjB,AAAiB;CACjB,AAAQ,SAAwB,CAAC;CACjC,AAAQ,iBAAiB;CAEzB,AAAO,YAAY,SAA8B;EAC/C,KAAK,OAAO,QAAQ;EACpB,KAAK,aAAa,KAAK,IAAI,GAAG,QAAQ,cAAc,CAAC;EACrD,KAAK,SAAS,QAAQ,UAAU;CAClC;;;;CAKA,MAAa,IAAI,OAA6B;EAC5C,KAAK,OAAO,KAAK;GACf,MAAM;GACN,6BAAY,IAAI,KAAK,EAAC,CAAC,YAAY;GACnC;EACF,CAAC;EAED,IAAI,KAAK,OAAO,UAAU,KAAK,YAC7B,MAAM,KAAK,MAAM;CAErB;;;;;CAMA,MAAa,QAAuB;EAClC,IAAI,KAAK,OAAO,WAAW,GACzB;EAGF,MAAM,UAAU,KAAK;EACrB,KAAK,SAAS,CAAC;EAEf,MAAM,KAAK,gBAAgB;EAE3B,MAAM,UAAU,QAAQ,KAAK,WAAW,KAAK,UAAU,MAAM,CAAC,CAAC,CAAC,KAAK,EAAE;EAEvE,MAAM,WAAW,KAAK,MAAM,SAAS,MAAM;CAC7C;;;;;CAMA,MAAc,kBAAiC;EAC7C,IAAI,KAAK,gBACP;EAGF,MAAM,MAAM,QAAQ,KAAK,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;EACnD,KAAK,iBAAiB;CACxB;;;;;;CAOA,AAAQ,UAAU,QAA6B;EAG7C,OAAO,GAFM,KAAK,SAAS,KAAK,UAAU,QAAQ,QAAW,CAAC,IAAI,KAAK,UAAU,MAAM,EAExE;CACjB;AACF"}
|
package/esm/exporters/index.mjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { walkSpans } from "./utils/walk-spans.mjs";
|
|
2
2
|
import { totalCostUsd } from "./utils/total-cost.mjs";
|
|
3
3
|
import { GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES, toGenAiAttributes } from "./utils/gen-ai-attributes.mjs";
|
|
4
|
+
import "./utils/index.mjs";
|
|
4
5
|
import { formatSpanIO } from "./console/format-span-io.mjs";
|
|
5
6
|
import { formatSpanLine } from "./console/format-span-line.mjs";
|
|
6
7
|
import { consoleExporter } from "./console/console-exporter.mjs";
|
|
8
|
+
import "./console/index.mjs";
|
|
7
9
|
import { fileExporter } from "./file/file-exporter.mjs";
|
|
8
10
|
import { langfuseExporter } from "./langfuse/langfuse-exporter.mjs";
|
|
9
11
|
import "./langfuse/index.mjs";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES, toGenAiAttributes } from "../utils/gen-ai-attributes.mjs";
|
|
2
|
+
import "../utils/index.mjs";
|
|
2
3
|
|
|
3
4
|
//#region ../../packages/ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts
|
|
4
5
|
const EXPORTER_NAME = "langfuse";
|
|
@@ -196,10 +197,10 @@ function langfuseMetadata(span) {
|
|
|
196
197
|
}
|
|
197
198
|
/**
|
|
198
199
|
* Map the Panoptic span status onto a Langfuse observation level —
|
|
199
|
-
* failed/cancelled spans surface as `ERROR`, everything else `DEFAULT`.
|
|
200
|
+
* failed/cancelled/incomplete spans surface as `ERROR`, everything else `DEFAULT`.
|
|
200
201
|
*/
|
|
201
202
|
function toLevel(span) {
|
|
202
|
-
if (span.status === "failed" || span.status === "cancelled") return "ERROR";
|
|
203
|
+
if (span.status === "failed" || span.status === "cancelled" || span.status === "incomplete") return "ERROR";
|
|
203
204
|
return "DEFAULT";
|
|
204
205
|
}
|
|
205
206
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"langfuse-exporter.mjs","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts"],"sourcesContent":["import type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport type { AttributeValue } from \"../utils\";\nimport { GEN_AI_ATTRIBUTES, toGenAiAttributes, WARLOCK_ATTRIBUTES } from \"../utils\";\nimport type {\n LangfuseClientLike,\n LangfuseExporterOptions,\n LangfuseObservationBody,\n LangfuseObservationLevel,\n LangfuseObservationLike,\n LangfuseTraceBody,\n LangfuseTraceLike,\n} from \"./langfuse-exporter.type\";\n\nconst EXPORTER_NAME = \"langfuse\";\n\n// ============================================================\n// Lazily-loaded langfuse SDK (OPTIONAL peer)\n// ============================================================\n\nlet LangfuseSdk: typeof import(\"langfuse\");\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst LANGFUSE_INSTALL_INSTRUCTIONS = `\nThe Panoptic Langfuse exporter requires the langfuse package.\nInstall it with:\n\n npm install langfuse\n\nOr with your preferred package manager:\n\n pnpm add langfuse\n yarn add langfuse\n`.trim();\n\n/**\n * Settle the lazy import of `langfuse` once, concurrency-safe. Only\n * needed when the caller did not pass a ready `client`. A bare `catch`\n * flips the flag to `false`; the curated install string surfaces at use\n * time, never a raw module-resolution stack trace.\n */\nfunction loadLangfuse(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n LangfuseSdk = await import(\"langfuse\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * {@link ExporterContract} that maps Panoptic traces onto Langfuse\n * traces and observations. Lazily imports `langfuse` so it stays an\n * OPTIONAL peer — importing this module never forces the SDK to be\n * installed, and a missing SDK surfaces as a curated \"install this\"\n * error when the exporter first needs to build a client.\n *\n * The root {@link TraceSpan} becomes a Langfuse trace AND its top\n * observation (so the root execution's own tokens are metered, not just\n * its children's); every node is an observation — a `generation` when it\n * metered its OWN tokens (LLM-backed agents, supervisors), otherwise a\n * plain `span` (tools, callbacks, and composite nodes whose tokens came\n * only from children). Timing, status, and version map 1:1; each\n * generation reports its own token usage (rolled-up minus children) so\n * the trace total isn't double-counted; non-usage GenAI attributes ride\n * along as metadata, and captured content (under `captureContent`)\n * surfaces as native `input` / `output`.\n *\n * @example\n * collector.use(langfuseExporter({ publicKey: \"pk-...\", secretKey: \"sk-...\" }));\n * // or reuse an existing client:\n * collector.use(langfuseExporter({ client: myLangfuse }));\n */\nexport function langfuseExporter(options: LangfuseExporterOptions): ExporterContract {\n let client: LangfuseClientLike | undefined = options.client;\n\n if (!client) {\n loadLangfuse();\n }\n\n const resolveClient = async (): Promise<LangfuseClientLike> => {\n if (client) {\n return client;\n }\n\n await loadLangfuse();\n\n if (!isModuleExists) {\n throw new Error(LANGFUSE_INSTALL_INSTRUCTIONS);\n }\n\n client = new LangfuseSdk.Langfuse({\n publicKey: options.publicKey,\n secretKey: options.secretKey,\n baseUrl: options.baseUrl,\n }) as unknown as LangfuseClientLike;\n\n return client;\n };\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n const activeClient = await resolveClient();\n emitTrace(activeClient, trace);\n },\n async flush(): Promise<void> {\n if (!client) {\n return;\n }\n\n await client.flushAsync();\n },\n async shutdown(): Promise<void> {\n if (!client) {\n return;\n }\n\n await client.shutdownAsync();\n },\n };\n}\n\n/**\n * Create the Langfuse trace from the root span, then recurse the\n * children into nested observations.\n */\nfunction emitTrace(client: LangfuseClientLike, trace: Trace): void {\n const root = trace.root;\n\n const traceBody: LangfuseTraceBody = {\n id: root.traceId,\n name: root.name,\n sessionId: trace.sessionId,\n version: root.version,\n timestamp: new Date(root.startedAt),\n metadata: langfuseMetadata(root),\n };\n\n // Surface the root's captured content at the TRACE level too — not only on\n // the root observation — so Langfuse's trace Preview shows the top-level\n // input/output instead of \"this trace received no input/output\". Present\n // only under content capture, and only for roots that carry I/O (an agent\n // or tool; a composite root like a planner has none of its own).\n if (root.input !== undefined) {\n traceBody.input = root.input;\n }\n\n if (root.output !== undefined) {\n traceBody.output = root.output;\n }\n\n const langfuseTrace = client.trace(traceBody);\n\n // Emit the ROOT as an observation too — not just its children — so the\n // root execution's OWN tokens are metered. Langfuse derives the trace\n // total by summing observation usage; leaving the root (often the single\n // top-level agent) as trace-only would drop its own spend. Children nest\n // under the root observation, mirroring the execution tree, and own-usage\n // metering telescopes the per-node sums back to the true trace total.\n emitObservation(langfuseTrace, root);\n}\n\n/**\n * Map one {@link TraceSpan} onto a Langfuse observation under `parent`,\n * then recurse its children. Spans that metered their OWN tokens become\n * `generation`s; everything else (tools, callbacks, composite nodes)\n * becomes a plain `span`.\n */\nfunction emitObservation(\n parent: LangfuseTraceLike | LangfuseObservationLike,\n span: TraceSpan,\n): void {\n const body: LangfuseObservationBody = {\n id: span.spanId,\n name: span.name,\n startTime: new Date(span.startedAt),\n endTime: new Date(span.endedAt),\n level: toLevel(span),\n statusMessage: span.error?.message,\n version: span.version,\n metadata: langfuseMetadata(span),\n };\n\n // Captured content (only present under `captureContent`) maps onto\n // Langfuse's native observation input/output.\n if (span.input !== undefined) {\n body.input = span.input;\n }\n\n if (span.output !== undefined) {\n body.output = span.output;\n }\n\n let observation: LangfuseObservationLike;\n\n // Classify + meter on OWN tokens (this node's rolled-up usage minus its\n // children's), not the subtree rollup. A composite node whose tokens\n // came only from descendants has zero own-usage and becomes a plain\n // span — so its children's tokens aren't counted twice in the trace\n // total Langfuse sums across observations.\n const own = ownUsage(span);\n\n if (own.total > 0) {\n body.usage = {\n input: own.input,\n output: own.output,\n total: own.total,\n unit: \"TOKENS\",\n };\n observation = parent.generation(body);\n } else {\n observation = parent.span(body);\n }\n\n for (const child of span.children) {\n emitObservation(observation, child);\n }\n\n // Explicitly end the observation. The body already carries `endTime`,\n // so this is idempotent — but the SDK only finalizes (and flushes) an\n // observation on `end()`, so without it long-lived clients can leave\n // observations open. Safe against the local `LangfuseObservationLike`\n // shape, which declares `end(body?)`.\n observation.end({ endTime: body.endTime });\n}\n\n/**\n * Own token usage for a span — its rolled-up {@link TraceSpan.usage}\n * minus the rolled-up usage of its direct children. `TraceSpan.usage` is\n * the subtree total (this node plus every descendant), so subtracting the\n * children leaves the tokens THIS node alone metered, clamped at zero\n * defensively.\n *\n * Langfuse sums observation usage into the trace total, so reporting\n * own-usage on each generation (rather than the subtree rollup) is what\n * keeps the trace total correct instead of multiply-counting nested\n * spans. A composite node with no own tokens (e.g. a workflow whose\n * tokens all came from agent children) yields `total: 0` and is emitted\n * as a plain span, not a generation.\n */\nfunction ownUsage(span: TraceSpan): { input: number; output: number; total: number } {\n let childInput = 0;\n let childOutput = 0;\n let childTotal = 0;\n\n for (const child of span.children) {\n childInput += child.usage.input;\n childOutput += child.usage.output;\n childTotal += child.usage.total;\n }\n\n return {\n input: Math.max(0, span.usage.input - childInput),\n output: Math.max(0, span.usage.output - childOutput),\n total: Math.max(0, span.usage.total - childTotal),\n };\n}\n\n/**\n * Token-usage + cost keys that the per-observation `usage` block already\n * carries authoritatively (as OWN usage). Omitting them from `metadata`\n * avoids a confusing contradiction — metadata would otherwise show the\n * rolled-up subtree totals next to an own-usage `usage` block. Cost is\n * omitted for the same reason; Langfuse prices the own tokens itself.\n */\nconst LANGFUSE_METADATA_OMIT = new Set<string>([\n GEN_AI_ATTRIBUTES.usageInputTokens,\n GEN_AI_ATTRIBUTES.usageOutputTokens,\n WARLOCK_ATTRIBUTES.totalTokens,\n WARLOCK_ATTRIBUTES.cachedTokens,\n WARLOCK_ATTRIBUTES.reasoningTokens,\n WARLOCK_ATTRIBUTES.costUsd,\n]);\n\n/**\n * Observation metadata — the GenAI attribute set minus the usage/cost\n * keys that live authoritatively on the `usage` block (see\n * {@link LANGFUSE_METADATA_OMIT}). Keeps model identity, report type,\n * version, session id, and any collector-set attributes.\n */\nfunction langfuseMetadata(span: TraceSpan): Record<string, AttributeValue> {\n const all = toGenAiAttributes(span);\n const metadata: Record<string, AttributeValue> = {};\n\n for (const [key, value] of Object.entries(all)) {\n if (!LANGFUSE_METADATA_OMIT.has(key)) {\n metadata[key] = value;\n }\n }\n\n return metadata;\n}\n\n/**\n * Map the Panoptic span status onto a Langfuse observation level —\n * failed/cancelled spans surface as `ERROR`, everything else `DEFAULT`.\n */\nfunction toLevel(span: TraceSpan): LangfuseObservationLevel {\n if (span.status === \"failed\" || span.status === \"cancelled\") {\n return \"ERROR\";\n }\n\n return \"DEFAULT\";\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"langfuse-exporter.mjs","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts"],"sourcesContent":["import type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport type { AttributeValue } from \"../utils\";\nimport { GEN_AI_ATTRIBUTES, toGenAiAttributes, WARLOCK_ATTRIBUTES } from \"../utils\";\nimport type {\n LangfuseClientLike,\n LangfuseExporterOptions,\n LangfuseObservationBody,\n LangfuseObservationLevel,\n LangfuseObservationLike,\n LangfuseTraceBody,\n LangfuseTraceLike,\n} from \"./langfuse-exporter.type\";\n\nconst EXPORTER_NAME = \"langfuse\";\n\n// ============================================================\n// Lazily-loaded langfuse SDK (OPTIONAL peer)\n// ============================================================\n\nlet LangfuseSdk: typeof import(\"langfuse\");\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst LANGFUSE_INSTALL_INSTRUCTIONS = `\nThe Panoptic Langfuse exporter requires the langfuse package.\nInstall it with:\n\n npm install langfuse\n\nOr with your preferred package manager:\n\n pnpm add langfuse\n yarn add langfuse\n`.trim();\n\n/**\n * Settle the lazy import of `langfuse` once, concurrency-safe. Only\n * needed when the caller did not pass a ready `client`. A bare `catch`\n * flips the flag to `false`; the curated install string surfaces at use\n * time, never a raw module-resolution stack trace.\n */\nfunction loadLangfuse(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n LangfuseSdk = await import(\"langfuse\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * {@link ExporterContract} that maps Panoptic traces onto Langfuse\n * traces and observations. Lazily imports `langfuse` so it stays an\n * OPTIONAL peer — importing this module never forces the SDK to be\n * installed, and a missing SDK surfaces as a curated \"install this\"\n * error when the exporter first needs to build a client.\n *\n * The root {@link TraceSpan} becomes a Langfuse trace AND its top\n * observation (so the root execution's own tokens are metered, not just\n * its children's); every node is an observation — a `generation` when it\n * metered its OWN tokens (LLM-backed agents, supervisors), otherwise a\n * plain `span` (tools, callbacks, and composite nodes whose tokens came\n * only from children). Timing, status, and version map 1:1; each\n * generation reports its own token usage (rolled-up minus children) so\n * the trace total isn't double-counted; non-usage GenAI attributes ride\n * along as metadata, and captured content (under `captureContent`)\n * surfaces as native `input` / `output`.\n *\n * @example\n * collector.use(langfuseExporter({ publicKey: \"pk-...\", secretKey: \"sk-...\" }));\n * // or reuse an existing client:\n * collector.use(langfuseExporter({ client: myLangfuse }));\n */\nexport function langfuseExporter(options: LangfuseExporterOptions): ExporterContract {\n let client: LangfuseClientLike | undefined = options.client;\n\n if (!client) {\n loadLangfuse();\n }\n\n const resolveClient = async (): Promise<LangfuseClientLike> => {\n if (client) {\n return client;\n }\n\n await loadLangfuse();\n\n if (!isModuleExists) {\n throw new Error(LANGFUSE_INSTALL_INSTRUCTIONS);\n }\n\n client = new LangfuseSdk.Langfuse({\n publicKey: options.publicKey,\n secretKey: options.secretKey,\n baseUrl: options.baseUrl,\n }) as unknown as LangfuseClientLike;\n\n return client;\n };\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n const activeClient = await resolveClient();\n emitTrace(activeClient, trace);\n },\n async flush(): Promise<void> {\n if (!client) {\n return;\n }\n\n await client.flushAsync();\n },\n async shutdown(): Promise<void> {\n if (!client) {\n return;\n }\n\n await client.shutdownAsync();\n },\n };\n}\n\n/**\n * Create the Langfuse trace from the root span, then recurse the\n * children into nested observations.\n */\nfunction emitTrace(client: LangfuseClientLike, trace: Trace): void {\n const root = trace.root;\n\n const traceBody: LangfuseTraceBody = {\n id: root.traceId,\n name: root.name,\n sessionId: trace.sessionId,\n version: root.version,\n timestamp: new Date(root.startedAt),\n metadata: langfuseMetadata(root),\n };\n\n // Surface the root's captured content at the TRACE level too — not only on\n // the root observation — so Langfuse's trace Preview shows the top-level\n // input/output instead of \"this trace received no input/output\". Present\n // only under content capture, and only for roots that carry I/O (an agent\n // or tool; a composite root like a planner has none of its own).\n if (root.input !== undefined) {\n traceBody.input = root.input;\n }\n\n if (root.output !== undefined) {\n traceBody.output = root.output;\n }\n\n const langfuseTrace = client.trace(traceBody);\n\n // Emit the ROOT as an observation too — not just its children — so the\n // root execution's OWN tokens are metered. Langfuse derives the trace\n // total by summing observation usage; leaving the root (often the single\n // top-level agent) as trace-only would drop its own spend. Children nest\n // under the root observation, mirroring the execution tree, and own-usage\n // metering telescopes the per-node sums back to the true trace total.\n emitObservation(langfuseTrace, root);\n}\n\n/**\n * Map one {@link TraceSpan} onto a Langfuse observation under `parent`,\n * then recurse its children. Spans that metered their OWN tokens become\n * `generation`s; everything else (tools, callbacks, composite nodes)\n * becomes a plain `span`.\n */\nfunction emitObservation(\n parent: LangfuseTraceLike | LangfuseObservationLike,\n span: TraceSpan,\n): void {\n const body: LangfuseObservationBody = {\n id: span.spanId,\n name: span.name,\n startTime: new Date(span.startedAt),\n endTime: new Date(span.endedAt),\n level: toLevel(span),\n statusMessage: span.error?.message,\n version: span.version,\n metadata: langfuseMetadata(span),\n };\n\n // Captured content (only present under `captureContent`) maps onto\n // Langfuse's native observation input/output.\n if (span.input !== undefined) {\n body.input = span.input;\n }\n\n if (span.output !== undefined) {\n body.output = span.output;\n }\n\n let observation: LangfuseObservationLike;\n\n // Classify + meter on OWN tokens (this node's rolled-up usage minus its\n // children's), not the subtree rollup. A composite node whose tokens\n // came only from descendants has zero own-usage and becomes a plain\n // span — so its children's tokens aren't counted twice in the trace\n // total Langfuse sums across observations.\n const own = ownUsage(span);\n\n if (own.total > 0) {\n body.usage = {\n input: own.input,\n output: own.output,\n total: own.total,\n unit: \"TOKENS\",\n };\n observation = parent.generation(body);\n } else {\n observation = parent.span(body);\n }\n\n for (const child of span.children) {\n emitObservation(observation, child);\n }\n\n // Explicitly end the observation. The body already carries `endTime`,\n // so this is idempotent — but the SDK only finalizes (and flushes) an\n // observation on `end()`, so without it long-lived clients can leave\n // observations open. Safe against the local `LangfuseObservationLike`\n // shape, which declares `end(body?)`.\n observation.end({ endTime: body.endTime });\n}\n\n/**\n * Own token usage for a span — its rolled-up {@link TraceSpan.usage}\n * minus the rolled-up usage of its direct children. `TraceSpan.usage` is\n * the subtree total (this node plus every descendant), so subtracting the\n * children leaves the tokens THIS node alone metered, clamped at zero\n * defensively.\n *\n * Langfuse sums observation usage into the trace total, so reporting\n * own-usage on each generation (rather than the subtree rollup) is what\n * keeps the trace total correct instead of multiply-counting nested\n * spans. A composite node with no own tokens (e.g. a workflow whose\n * tokens all came from agent children) yields `total: 0` and is emitted\n * as a plain span, not a generation.\n */\nfunction ownUsage(span: TraceSpan): { input: number; output: number; total: number } {\n let childInput = 0;\n let childOutput = 0;\n let childTotal = 0;\n\n for (const child of span.children) {\n childInput += child.usage.input;\n childOutput += child.usage.output;\n childTotal += child.usage.total;\n }\n\n return {\n input: Math.max(0, span.usage.input - childInput),\n output: Math.max(0, span.usage.output - childOutput),\n total: Math.max(0, span.usage.total - childTotal),\n };\n}\n\n/**\n * Token-usage + cost keys that the per-observation `usage` block already\n * carries authoritatively (as OWN usage). Omitting them from `metadata`\n * avoids a confusing contradiction — metadata would otherwise show the\n * rolled-up subtree totals next to an own-usage `usage` block. Cost is\n * omitted for the same reason; Langfuse prices the own tokens itself.\n */\nconst LANGFUSE_METADATA_OMIT = new Set<string>([\n GEN_AI_ATTRIBUTES.usageInputTokens,\n GEN_AI_ATTRIBUTES.usageOutputTokens,\n WARLOCK_ATTRIBUTES.totalTokens,\n WARLOCK_ATTRIBUTES.cachedTokens,\n WARLOCK_ATTRIBUTES.reasoningTokens,\n WARLOCK_ATTRIBUTES.costUsd,\n]);\n\n/**\n * Observation metadata — the GenAI attribute set minus the usage/cost\n * keys that live authoritatively on the `usage` block (see\n * {@link LANGFUSE_METADATA_OMIT}). Keeps model identity, report type,\n * version, session id, and any collector-set attributes.\n */\nfunction langfuseMetadata(span: TraceSpan): Record<string, AttributeValue> {\n const all = toGenAiAttributes(span);\n const metadata: Record<string, AttributeValue> = {};\n\n for (const [key, value] of Object.entries(all)) {\n if (!LANGFUSE_METADATA_OMIT.has(key)) {\n metadata[key] = value;\n }\n }\n\n return metadata;\n}\n\n/**\n * Map the Panoptic span status onto a Langfuse observation level —\n * failed/cancelled/incomplete spans surface as `ERROR`, everything else `DEFAULT`.\n */\nfunction toLevel(span: TraceSpan): LangfuseObservationLevel {\n if (span.status === \"failed\" || span.status === \"cancelled\" || span.status === \"incomplete\") {\n return \"ERROR\";\n }\n\n return \"DEFAULT\";\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otel-api.shim.type.d.mts","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/otel/otel-api.shim.type.ts"],"mappings":";;AAqBA;;;;AAAyB;AAGzB;;;;AAAuB;AAMvB;;;;;;;;AAGO;AAAA,KAZK,aAAA;;KAGA,WAAA;;AAeH;AAOT;;KAhBY,aAAA;EACV,IAAA;EACA,OAAA;EACA,KAAA;AAAA;;KAIU,cAAA;EACV,IAAA;EACA,OAAO;AAAA;;;;;KAOG,QAAA;EACV,aAAA,CAAc,UAAA,EAAY,MAAA;EAC1B,SAAA,CAAU,MAAA,EAAQ,cAAA;EAClB,eAAA,CAAgB,SAAA,EAAW,aAAA;EAC3B,GAAA,CAAI,OAAA,GAAU,aAAA;AAAA;AAAa;AAO7B;;;AAP6B,KAOjB,UAAA;EACV,SAAA,
|
|
1
|
+
{"version":3,"file":"otel-api.shim.type.d.mts","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/otel/otel-api.shim.type.ts"],"mappings":";;AAqBA;;;;AAAyB;AAGzB;;;;AAAuB;AAMvB;;;;;;;;AAGO;AAAA,KAZK,aAAA;;KAGA,WAAA;;AAeH;AAOT;;KAhBY,aAAA;EACV,IAAA;EACA,OAAA;EACA,KAAA;AAAA;;KAIU,cAAA;EACV,IAAA;EACA,OAAO;AAAA;;;;;KAOG,QAAA;EACV,aAAA,CAAc,UAAA,EAAY,MAAA;EAC1B,SAAA,CAAU,MAAA,EAAQ,cAAA;EAClB,eAAA,CAAgB,SAAA,EAAW,aAAA;EAC3B,GAAA,CAAI,OAAA,GAAU,aAAA;AAAA;AAAa;AAO7B;;;AAP6B,KAOjB,UAAA;EACV,SAAA,CAAU,IAAA,UAAc,OAAA;IAAW,SAAA,GAAY,aAAA;EAAA,GAAiB,OAAA,GAAU,WAAA,GAAc,QAAA;AAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GEN_AI_ATTRIBUTES, toGenAiAttributes } from "../utils/gen-ai-attributes.mjs";
|
|
2
|
+
import "../utils/index.mjs";
|
|
2
3
|
|
|
3
4
|
//#region ../../packages/ai-panoptic/src/exporters/otel/otel-exporter.ts
|
|
4
5
|
const EXPORTER_NAME = "otel";
|
|
@@ -128,7 +129,7 @@ function applyStatus(otelSpan, span) {
|
|
|
128
129
|
message: span.error.message,
|
|
129
130
|
stack: span.error.stack
|
|
130
131
|
});
|
|
131
|
-
if (span.status === "failed" || span.status === "cancelled") {
|
|
132
|
+
if (span.status === "failed" || span.status === "cancelled" || span.status === "incomplete") {
|
|
132
133
|
otelSpan.setStatus({
|
|
133
134
|
code: codes.ERROR,
|
|
134
135
|
message: span.error?.message
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otel-exporter.mjs","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/otel/otel-exporter.ts"],"sourcesContent":["import type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport { toGenAiAttributes, GEN_AI_ATTRIBUTES } from \"../utils\";\nimport type {\n OtelApiModule,\n OtelContext,\n OtelSpan,\n OtelSpanStatusCode,\n OtelTracer,\n} from \"./otel-api.shim.type\";\nimport type { OtelExporterOptions } from \"./otel-exporter.type\";\n\nconst EXPORTER_NAME = \"otel\";\nconst DEFAULT_TRACER_NAME = \"@warlock.js/ai-panoptic\";\n\n// ============================================================\n// Lazily-loaded @opentelemetry/api (OPTIONAL peer)\n// ============================================================\n\nlet OtelApi: OtelApiModule;\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst OTEL_INSTALL_INSTRUCTIONS = `\nThe Panoptic OpenTelemetry exporter requires the @opentelemetry/api package.\nInstall it with:\n\n npm install @opentelemetry/api\n\nOr with your preferred package manager:\n\n pnpm add @opentelemetry/api\n yarn add @opentelemetry/api\n`.trim();\n\n/**\n * Settle the lazy import of `@opentelemetry/api` once, concurrency-safe.\n * A bare `catch` flips the flag to `false`; the curated install string\n * surfaces at use time so a missing SDK never throws a raw module\n * resolution error.\n */\nfunction loadOtel(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n // Indirect specifier so TS does not statically resolve the\n // optional `@opentelemetry/api` peer at compile time (it is\n // intentionally not installed). The result is structurally the\n // `OtelApiModule` shim — the exporter only touches that surface.\n const moduleName = \"@opentelemetry/api\";\n OtelApi = (await import(moduleName)) as OtelApiModule;\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * {@link ExporterContract} that maps Panoptic traces onto OpenTelemetry\n * spans following the GenAI semantic conventions (`gen_ai.*`\n * attributes). Lazily imports `@opentelemetry/api` so it stays an\n * OPTIONAL peer — importing this module never forces the SDK to be\n * installed, and a missing SDK surfaces as a curated \"install this\"\n * error on first `export`, not a boot-time stack trace.\n *\n * The exporter emits onto a `Tracer` you supply (or fetches one from the\n * globally registered provider). It never configures the SDK — wiring a\n * `TracerProvider`, processors, and span exporters is the host app's\n * job, exactly as with any other OTel instrumentation.\n *\n * Each {@link TraceSpan} becomes one OTel span with the source span's\n * start/end times and parent relationship reconstructed, so the emitted\n * tree matches the original execution tree.\n *\n * @example\n * // app already set up @opentelemetry/sdk-trace-base + a provider\n * collector.use(otelExporter({ tracerName: \"my-app\", system: \"openai\" }));\n */\nexport function otelExporter(options: OtelExporterOptions = {}): ExporterContract {\n loadOtel();\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n await loadOtel();\n\n if (!isModuleExists) {\n throw new Error(OTEL_INSTALL_INSTRUCTIONS);\n }\n\n const tracer = resolveTracer(options);\n emitSpan(tracer, trace.root, undefined, options);\n },\n };\n}\n\n/**\n * Resolve the `Tracer` spans are emitted on — the caller-supplied one,\n * or one fetched from the globally registered provider by name.\n */\nfunction resolveTracer(options: OtelExporterOptions): OtelTracer {\n if (options.tracer) {\n return options.tracer;\n }\n\n return OtelApi.trace.getTracer(\n options.tracerName ?? DEFAULT_TRACER_NAME,\n options.tracerVersion,\n );\n}\n\n/**\n * Recreate one {@link TraceSpan} (and its subtree) as OTel spans. The\n * span is started with the source `startedAt`, parented under\n * `parentContext` so the tree is preserved, annotated with GenAI\n * attributes, given the mapped status, and ended at `endedAt`. Children\n * recurse under this span's context.\n */\nfunction emitSpan(\n tracer: OtelTracer,\n span: TraceSpan,\n parentContext: OtelContext | undefined,\n options: OtelExporterOptions,\n): void {\n const startTime = toEpochMillis(span.startedAt);\n const baseContext = parentContext ?? OtelApi.context.active();\n\n const otelSpan = tracer.startSpan(span.name, { startTime }, baseContext);\n\n applyAttributes(otelSpan, span, options);\n applyStatus(otelSpan, span);\n\n const childContext = OtelApi.trace.setSpan(baseContext, otelSpan);\n\n for (const child of span.children) {\n emitSpan(tracer, child, childContext, options);\n }\n\n otelSpan.end(toEpochMillis(span.endedAt));\n}\n\n/**\n * Set the GenAI + Warlock attributes on the OTel span, defaulting\n * `gen_ai.system` from the exporter options when the span carried none.\n */\nfunction applyAttributes(\n otelSpan: OtelSpan,\n span: TraceSpan,\n options: OtelExporterOptions,\n): void {\n const attributes = toGenAiAttributes(span);\n\n if (options.system !== undefined && attributes[GEN_AI_ATTRIBUTES.system] === undefined) {\n attributes[GEN_AI_ATTRIBUTES.system] = options.system;\n }\n\n // Captured content (only present under `captureContent`) maps onto the\n // GenAI prompt/completion attributes, stringified since OTel attribute\n // values must be primitives.\n if (span.input !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.prompt] = stringifyContent(span.input);\n }\n\n if (span.output !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.completion] = stringifyContent(span.output);\n }\n\n otelSpan.setAttributes(attributes);\n}\n\n/**\n * Coerce a captured content value to a string OTel attribute. Strings\n * pass through; structured values are JSON-encoded (falling back to\n * `String()` if they can't be serialized).\n */\nfunction stringifyContent(value: unknown): string {\n if (typeof value === \"string\") {\n return value;\n }\n\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Map the Panoptic span status onto the OTel span status, recording the\n * normalized error as an exception event + ERROR status when present.\n */\nfunction applyStatus(otelSpan: OtelSpan, span: TraceSpan): void {\n const codes: OtelSpanStatusCode = OtelApi.SpanStatusCode;\n\n if (span.error) {\n otelSpan.recordException({\n name: span.error.type,\n message: span.error.message,\n stack: span.error.stack,\n });\n }\n\n if (span.status === \"failed\" || span.status === \"cancelled\") {\n otelSpan.setStatus({\n code: codes.ERROR,\n message: span.error?.message,\n });\n return;\n }\n\n // A capped / paused run is neither a failure nor a clean success.\n // Mapping it to OK would let a hit iteration cap read as a healthy\n // run; leave the OTel status UNSET with a descriptive message so the\n // outcome is visible without being miscounted as an error.\n if (span.status === \"max-iterations\" || span.status === \"awaiting-input\") {\n otelSpan.setStatus({\n code: codes.UNSET,\n message:\n span.status === \"max-iterations\"\n ? \"Run hit the iteration cap without an explicit end\"\n : \"Run is awaiting the next input turn\",\n });\n return;\n }\n\n otelSpan.setStatus({ code: codes.OK });\n}\n\n/**\n * Convert an ISO-8601 timestamp to epoch milliseconds — the `TimeInput`\n * form OTel's `startSpan` / `Span.end` accept directly.\n */\nfunction toEpochMillis(isoTimestamp: string): number {\n return new Date(isoTimestamp).getTime();\n}\n"],"mappings":";;;AAWA,MAAM,gBAAgB;AACtB,MAAM,sBAAsB;AAM5B,IAAI;AACJ,IAAI,iBAAiC;AACrC,IAAI;AAEJ,MAAM,4BAA4B;;;;;;;;;;EAUhC,KAAK;;;;;;;AAQP,SAAS,WAA0B;CACjC,IAAI,mBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GAMF,UAAW,MAAM,OAAO;GACxB,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,aAAa,UAA+B,CAAC,GAAqB;CAChF,SAAS;CAET,OAAO;EACL,MAAM;EACN,MAAM,OAAO,OAA6B;GACxC,MAAM,SAAS;GAEf,IAAI,CAAC,gBACH,MAAM,IAAI,MAAM,yBAAyB;GAI3C,SADe,cAAc,OACf,GAAG,MAAM,MAAM,QAAW,OAAO;EACjD;CACF;AACF;;;;;AAMA,SAAS,cAAc,SAA0C;CAC/D,IAAI,QAAQ,QACV,OAAO,QAAQ;CAGjB,OAAO,QAAQ,MAAM,UACnB,QAAQ,cAAc,qBACtB,QAAQ,aACV;AACF;;;;;;;;AASA,SAAS,SACP,QACA,MACA,eACA,SACM;CACN,MAAM,YAAY,cAAc,KAAK,SAAS;CAC9C,MAAM,cAAc,iBAAiB,QAAQ,QAAQ,OAAO;CAE5D,MAAM,WAAW,OAAO,UAAU,KAAK,MAAM,EAAE,UAAU,GAAG,WAAW;CAEvE,gBAAgB,UAAU,MAAM,OAAO;CACvC,YAAY,UAAU,IAAI;CAE1B,MAAM,eAAe,QAAQ,MAAM,QAAQ,aAAa,QAAQ;CAEhE,KAAK,MAAM,SAAS,KAAK,UACvB,SAAS,QAAQ,OAAO,cAAc,OAAO;CAG/C,SAAS,IAAI,cAAc,KAAK,OAAO,CAAC;AAC1C;;;;;AAMA,SAAS,gBACP,UACA,MACA,SACM;CACN,MAAM,aAAa,kBAAkB,IAAI;CAEzC,IAAI,QAAQ,WAAW,UAAa,WAAW,kBAAkB,YAAY,QAC3E,WAAW,kBAAkB,UAAU,QAAQ;CAMjD,IAAI,KAAK,UAAU,QACjB,WAAW,kBAAkB,UAAU,iBAAiB,KAAK,KAAK;CAGpE,IAAI,KAAK,WAAW,QAClB,WAAW,kBAAkB,cAAc,iBAAiB,KAAK,MAAM;CAGzE,SAAS,cAAc,UAAU;AACnC;;;;;;AAOA,SAAS,iBAAiB,OAAwB;CAChD,IAAI,OAAO,UAAU,UACnB,OAAO;CAGT,IAAI;EACF,OAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;CAC9C,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;;;;;AAMA,SAAS,YAAY,UAAoB,MAAuB;CAC9D,MAAM,QAA4B,QAAQ;CAE1C,IAAI,KAAK,OACP,SAAS,gBAAgB;EACvB,MAAM,KAAK,MAAM;EACjB,SAAS,KAAK,MAAM;EACpB,OAAO,KAAK,MAAM;CACpB,CAAC;CAGH,IAAI,KAAK,WAAW,YAAY,KAAK,WAAW,aAAa;EAC3D,SAAS,UAAU;GACjB,MAAM,MAAM;GACZ,SAAS,KAAK,OAAO;EACvB,CAAC;EACD;CACF;CAMA,IAAI,KAAK,WAAW,oBAAoB,KAAK,WAAW,kBAAkB;EACxE,SAAS,UAAU;GACjB,MAAM,MAAM;GACZ,SACE,KAAK,WAAW,mBACZ,sDACA;EACR,CAAC;EACD;CACF;CAEA,SAAS,UAAU,EAAE,MAAM,MAAM,GAAG,CAAC;AACvC;;;;;AAMA,SAAS,cAAc,cAA8B;CACnD,OAAO,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC"}
|
|
1
|
+
{"version":3,"file":"otel-exporter.mjs","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/otel/otel-exporter.ts"],"sourcesContent":["import type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport { toGenAiAttributes, GEN_AI_ATTRIBUTES } from \"../utils\";\nimport type {\n OtelApiModule,\n OtelContext,\n OtelSpan,\n OtelSpanStatusCode,\n OtelTracer,\n} from \"./otel-api.shim.type\";\nimport type { OtelExporterOptions } from \"./otel-exporter.type\";\n\nconst EXPORTER_NAME = \"otel\";\nconst DEFAULT_TRACER_NAME = \"@warlock.js/ai-panoptic\";\n\n// ============================================================\n// Lazily-loaded @opentelemetry/api (OPTIONAL peer)\n// ============================================================\n\nlet OtelApi: OtelApiModule;\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst OTEL_INSTALL_INSTRUCTIONS = `\nThe Panoptic OpenTelemetry exporter requires the @opentelemetry/api package.\nInstall it with:\n\n npm install @opentelemetry/api\n\nOr with your preferred package manager:\n\n pnpm add @opentelemetry/api\n yarn add @opentelemetry/api\n`.trim();\n\n/**\n * Settle the lazy import of `@opentelemetry/api` once, concurrency-safe.\n * A bare `catch` flips the flag to `false`; the curated install string\n * surfaces at use time so a missing SDK never throws a raw module\n * resolution error.\n */\nfunction loadOtel(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n // Indirect specifier so TS does not statically resolve the\n // optional `@opentelemetry/api` peer at compile time (it is\n // intentionally not installed). The result is structurally the\n // `OtelApiModule` shim — the exporter only touches that surface.\n const moduleName = \"@opentelemetry/api\";\n OtelApi = (await import(moduleName)) as OtelApiModule;\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * {@link ExporterContract} that maps Panoptic traces onto OpenTelemetry\n * spans following the GenAI semantic conventions (`gen_ai.*`\n * attributes). Lazily imports `@opentelemetry/api` so it stays an\n * OPTIONAL peer — importing this module never forces the SDK to be\n * installed, and a missing SDK surfaces as a curated \"install this\"\n * error on first `export`, not a boot-time stack trace.\n *\n * The exporter emits onto a `Tracer` you supply (or fetches one from the\n * globally registered provider). It never configures the SDK — wiring a\n * `TracerProvider`, processors, and span exporters is the host app's\n * job, exactly as with any other OTel instrumentation.\n *\n * Each {@link TraceSpan} becomes one OTel span with the source span's\n * start/end times and parent relationship reconstructed, so the emitted\n * tree matches the original execution tree.\n *\n * @example\n * // app already set up @opentelemetry/sdk-trace-base + a provider\n * collector.use(otelExporter({ tracerName: \"my-app\", system: \"openai\" }));\n */\nexport function otelExporter(options: OtelExporterOptions = {}): ExporterContract {\n loadOtel();\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n await loadOtel();\n\n if (!isModuleExists) {\n throw new Error(OTEL_INSTALL_INSTRUCTIONS);\n }\n\n const tracer = resolveTracer(options);\n emitSpan(tracer, trace.root, undefined, options);\n },\n };\n}\n\n/**\n * Resolve the `Tracer` spans are emitted on — the caller-supplied one,\n * or one fetched from the globally registered provider by name.\n */\nfunction resolveTracer(options: OtelExporterOptions): OtelTracer {\n if (options.tracer) {\n return options.tracer;\n }\n\n return OtelApi.trace.getTracer(options.tracerName ?? DEFAULT_TRACER_NAME, options.tracerVersion);\n}\n\n/**\n * Recreate one {@link TraceSpan} (and its subtree) as OTel spans. The\n * span is started with the source `startedAt`, parented under\n * `parentContext` so the tree is preserved, annotated with GenAI\n * attributes, given the mapped status, and ended at `endedAt`. Children\n * recurse under this span's context.\n */\nfunction emitSpan(\n tracer: OtelTracer,\n span: TraceSpan,\n parentContext: OtelContext | undefined,\n options: OtelExporterOptions,\n): void {\n const startTime = toEpochMillis(span.startedAt);\n const baseContext = parentContext ?? OtelApi.context.active();\n\n const otelSpan = tracer.startSpan(span.name, { startTime }, baseContext);\n\n applyAttributes(otelSpan, span, options);\n applyStatus(otelSpan, span);\n\n const childContext = OtelApi.trace.setSpan(baseContext, otelSpan);\n\n for (const child of span.children) {\n emitSpan(tracer, child, childContext, options);\n }\n\n otelSpan.end(toEpochMillis(span.endedAt));\n}\n\n/**\n * Set the GenAI + Warlock attributes on the OTel span, defaulting\n * `gen_ai.system` from the exporter options when the span carried none.\n */\nfunction applyAttributes(otelSpan: OtelSpan, span: TraceSpan, options: OtelExporterOptions): void {\n const attributes = toGenAiAttributes(span);\n\n if (options.system !== undefined && attributes[GEN_AI_ATTRIBUTES.system] === undefined) {\n attributes[GEN_AI_ATTRIBUTES.system] = options.system;\n }\n\n // Captured content (only present under `captureContent`) maps onto the\n // GenAI prompt/completion attributes, stringified since OTel attribute\n // values must be primitives.\n if (span.input !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.prompt] = stringifyContent(span.input);\n }\n\n if (span.output !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.completion] = stringifyContent(span.output);\n }\n\n otelSpan.setAttributes(attributes);\n}\n\n/**\n * Coerce a captured content value to a string OTel attribute. Strings\n * pass through; structured values are JSON-encoded (falling back to\n * `String()` if they can't be serialized).\n */\nfunction stringifyContent(value: unknown): string {\n if (typeof value === \"string\") {\n return value;\n }\n\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Map the Panoptic span status onto the OTel span status, recording the\n * normalized error as an exception event + ERROR status when present.\n */\nfunction applyStatus(otelSpan: OtelSpan, span: TraceSpan): void {\n const codes: OtelSpanStatusCode = OtelApi.SpanStatusCode;\n\n if (span.error) {\n otelSpan.recordException({\n name: span.error.type,\n message: span.error.message,\n stack: span.error.stack,\n });\n }\n\n // An incomplete run (the model's answer was cut off) has no usable\n // result, so it is an error like a failed one.\n if (span.status === \"failed\" || span.status === \"cancelled\" || span.status === \"incomplete\") {\n otelSpan.setStatus({\n code: codes.ERROR,\n message: span.error?.message,\n });\n return;\n }\n\n // A capped / paused run is neither a failure nor a clean success.\n // Mapping it to OK would let a hit iteration cap read as a healthy\n // run; leave the OTel status UNSET with a descriptive message so the\n // outcome is visible without being miscounted as an error.\n if (span.status === \"max-iterations\" || span.status === \"awaiting-input\") {\n otelSpan.setStatus({\n code: codes.UNSET,\n message:\n span.status === \"max-iterations\"\n ? \"Run hit the iteration cap without an explicit end\"\n : \"Run is awaiting the next input turn\",\n });\n return;\n }\n\n otelSpan.setStatus({ code: codes.OK });\n}\n\n/**\n * Convert an ISO-8601 timestamp to epoch milliseconds — the `TimeInput`\n * form OTel's `startSpan` / `Span.end` accept directly.\n */\nfunction toEpochMillis(isoTimestamp: string): number {\n return new Date(isoTimestamp).getTime();\n}\n"],"mappings":";;;;AAWA,MAAM,gBAAgB;AACtB,MAAM,sBAAsB;AAM5B,IAAI;AACJ,IAAI,iBAAiC;AACrC,IAAI;AAEJ,MAAM,4BAA4B;;;;;;;;;;EAUhC,KAAK;;;;;;;AAQP,SAAS,WAA0B;CACjC,IAAI,mBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GAMF,UAAW,MAAM,OAAO;GACxB,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,aAAa,UAA+B,CAAC,GAAqB;CAChF,SAAS;CAET,OAAO;EACL,MAAM;EACN,MAAM,OAAO,OAA6B;GACxC,MAAM,SAAS;GAEf,IAAI,CAAC,gBACH,MAAM,IAAI,MAAM,yBAAyB;GAI3C,SADe,cAAc,OACf,GAAG,MAAM,MAAM,QAAW,OAAO;EACjD;CACF;AACF;;;;;AAMA,SAAS,cAAc,SAA0C;CAC/D,IAAI,QAAQ,QACV,OAAO,QAAQ;CAGjB,OAAO,QAAQ,MAAM,UAAU,QAAQ,cAAc,qBAAqB,QAAQ,aAAa;AACjG;;;;;;;;AASA,SAAS,SACP,QACA,MACA,eACA,SACM;CACN,MAAM,YAAY,cAAc,KAAK,SAAS;CAC9C,MAAM,cAAc,iBAAiB,QAAQ,QAAQ,OAAO;CAE5D,MAAM,WAAW,OAAO,UAAU,KAAK,MAAM,EAAE,UAAU,GAAG,WAAW;CAEvE,gBAAgB,UAAU,MAAM,OAAO;CACvC,YAAY,UAAU,IAAI;CAE1B,MAAM,eAAe,QAAQ,MAAM,QAAQ,aAAa,QAAQ;CAEhE,KAAK,MAAM,SAAS,KAAK,UACvB,SAAS,QAAQ,OAAO,cAAc,OAAO;CAG/C,SAAS,IAAI,cAAc,KAAK,OAAO,CAAC;AAC1C;;;;;AAMA,SAAS,gBAAgB,UAAoB,MAAiB,SAAoC;CAChG,MAAM,aAAa,kBAAkB,IAAI;CAEzC,IAAI,QAAQ,WAAW,UAAa,WAAW,kBAAkB,YAAY,QAC3E,WAAW,kBAAkB,UAAU,QAAQ;CAMjD,IAAI,KAAK,UAAU,QACjB,WAAW,kBAAkB,UAAU,iBAAiB,KAAK,KAAK;CAGpE,IAAI,KAAK,WAAW,QAClB,WAAW,kBAAkB,cAAc,iBAAiB,KAAK,MAAM;CAGzE,SAAS,cAAc,UAAU;AACnC;;;;;;AAOA,SAAS,iBAAiB,OAAwB;CAChD,IAAI,OAAO,UAAU,UACnB,OAAO;CAGT,IAAI;EACF,OAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;CAC9C,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;;;;;AAMA,SAAS,YAAY,UAAoB,MAAuB;CAC9D,MAAM,QAA4B,QAAQ;CAE1C,IAAI,KAAK,OACP,SAAS,gBAAgB;EACvB,MAAM,KAAK,MAAM;EACjB,SAAS,KAAK,MAAM;EACpB,OAAO,KAAK,MAAM;CACpB,CAAC;CAKH,IAAI,KAAK,WAAW,YAAY,KAAK,WAAW,eAAe,KAAK,WAAW,cAAc;EAC3F,SAAS,UAAU;GACjB,MAAM,MAAM;GACZ,SAAS,KAAK,OAAO;EACvB,CAAC;EACD;CACF;CAMA,IAAI,KAAK,WAAW,oBAAoB,KAAK,WAAW,kBAAkB;EACxE,SAAS,UAAU;GACjB,MAAM,MAAM;GACZ,SACE,KAAK,WAAW,mBACZ,sDACA;EACR,CAAC;EACD;CACF;CAEA,SAAS,UAAU,EAAE,MAAM,MAAM,GAAG,CAAC;AACvC;;;;;AAMA,SAAS,cAAc,cAA8B;CACnD,OAAO,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC"}
|
|
@@ -3,10 +3,8 @@ import { Usage } from "@warlock.js/ai";
|
|
|
3
3
|
//#region ../../packages/ai-panoptic/src/exporters/utils/total-cost.d.ts
|
|
4
4
|
/**
|
|
5
5
|
* Collapse a {@link Usage.cost} breakdown into a single USD scalar by
|
|
6
|
-
* summing every populated
|
|
7
|
-
*
|
|
8
|
-
* folds in `reasoning` for forward-safety when a provider prices
|
|
9
|
-
* reasoning tokens as a separate channel. Returns `undefined` when no
|
|
6
|
+
* summing every populated channel (input, output, cache reads, cache
|
|
7
|
+
* writes, reasoning) via `totalCost` from `@warlock.js/ai`. Returns `undefined` when no
|
|
10
8
|
* pricing was attached, so exporters can omit the cost attribute
|
|
11
9
|
* entirely rather than reporting a misleading `0`.
|
|
12
10
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"total-cost.d.mts","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/utils/total-cost.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"total-cost.d.mts","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/utils/total-cost.ts"],"mappings":";;;;;AAaA;;;;AAAyC;;;;;iBAAzB,YAAA,CAAa,KAAY,EAAL,KAAK"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
+
import { totalCost } from "@warlock.js/ai";
|
|
2
|
+
|
|
1
3
|
//#region ../../packages/ai-panoptic/src/exporters/utils/total-cost.ts
|
|
2
4
|
/**
|
|
3
5
|
* Collapse a {@link Usage.cost} breakdown into a single USD scalar by
|
|
4
|
-
* summing every populated
|
|
5
|
-
*
|
|
6
|
-
* folds in `reasoning` for forward-safety when a provider prices
|
|
7
|
-
* reasoning tokens as a separate channel. Returns `undefined` when no
|
|
6
|
+
* summing every populated channel (input, output, cache reads, cache
|
|
7
|
+
* writes, reasoning) via `totalCost` from `@warlock.js/ai`. Returns `undefined` when no
|
|
8
8
|
* pricing was attached, so exporters can omit the cost attribute
|
|
9
9
|
* entirely rather than reporting a misleading `0`.
|
|
10
10
|
*
|
|
@@ -13,9 +13,7 @@
|
|
|
13
13
|
* // => 0.05
|
|
14
14
|
*/
|
|
15
15
|
function totalCostUsd(usage) {
|
|
16
|
-
|
|
17
|
-
if (!cost) return;
|
|
18
|
-
return (cost.input ?? 0) + (cost.output ?? 0) + (cost.cachedInput ?? 0) + (cost.cachedOutput ?? 0) + (cost.reasoning ?? 0);
|
|
16
|
+
return totalCost(usage.cost);
|
|
19
17
|
}
|
|
20
18
|
|
|
21
19
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"total-cost.mjs","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/utils/total-cost.ts"],"sourcesContent":["import type
|
|
1
|
+
{"version":3,"file":"total-cost.mjs","names":[],"sources":["../../../../../../../../../packages/ai-panoptic/src/exporters/utils/total-cost.ts"],"sourcesContent":["import { totalCost, type Usage } from \"@warlock.js/ai\";\n\n/**\n * Collapse a {@link Usage.cost} breakdown into a single USD scalar by\n * summing every populated channel (input, output, cache reads, cache\n * writes, reasoning) via `totalCost` from `@warlock.js/ai`. Returns `undefined` when no\n * pricing was attached, so exporters can omit the cost attribute\n * entirely rather than reporting a misleading `0`.\n *\n * @example\n * totalCostUsd({ input: 1, output: 2, total: 3, cost: { input: 0.01, output: 0.04 } });\n * // => 0.05\n */\nexport function totalCostUsd(usage: Usage): number | undefined {\n return totalCost(usage.cost);\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,SAAgB,aAAa,OAAkC;CAC7D,OAAO,UAAU,MAAM,IAAI;AAC7B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panoptic-middleware.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/panoptic/panoptic-middleware.ts"],"sourcesContent":["import type { AgentMiddleware } from \"@warlock.js/ai\";\nimport type { CollectorContract } from \"../contracts/collector.contract\";\n\n/**\n * Build an {@link AgentMiddleware} that feeds a collector from the\n * `execute`- and `supervisor`-level hooks. An alternative wiring to event\n * subscription for apps that already compose cross-cutting concerns\n * through the agent middleware pipeline (`[cache, budget, guardrail,\n * observability]`). Declaring both hook maps lets a single middleware\n * object work uniformly on agents (which fire the `execute` map) and\n * supervisors (which fire the `supervisor` map) — registering it on a\n * supervisor would otherwise install and collect nothing silently.\n *\n * Both terminal paths are covered on each surface:\n * - `after` — fires on a run that produced a result. A run can complete\n * with `result.error` populated (the engine still calls `after`), so\n * the report AND the envelope error are collected; the error type and\n * message land on the root span.\n * - `onError` — fires when the run threw before assembling a result. The\n * error carries the partial result's report on its envelope; when\n * present it is collected, with the error itself threaded onto the\n * root span so failed runs still produce a trace.\n *\n * The hooks never return a value, so they never mutate the agent's /\n * supervisor's result. The `collect` call is fire-and-forget relative to\n * the run — the collector isolates exporter failures internally, and we\n * additionally swallow any rejection here so an observability fault can\n * never surface on the run's hot path.\n *\n * @param collector - the collector traces are fed into.\n * @param name - stable middleware name (kebab-case). Defaults to\n * `\"panoptic\"`.\n */\nexport function createPanopticMiddleware(\n collector: CollectorContract,\n name = \"panoptic\",\n): AgentMiddleware {\n const collectReport = (report: unknown, rootError?: unknown): void => {\n if (!isReport(report)) {\n return;\n }\n\n void collector.collect(report, rootError).catch(() => {\n // Swallow — the collector already isolates exporter failures; this\n // guard keeps an observability fault off the run's hot path.\n });\n };\n\n const onResult = (result: unknown): void => {\n // A run can complete with `result.error` populated (`after` still\n // fires); thread that envelope error onto the root span.\n collectReport(
|
|
1
|
+
{"version":3,"file":"panoptic-middleware.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/panoptic/panoptic-middleware.ts"],"sourcesContent":["import type { AgentMiddleware } from \"@warlock.js/ai\";\nimport type { CollectorContract } from \"../contracts/collector.contract\";\n\n/**\n * Build an {@link AgentMiddleware} that feeds a collector from the\n * `execute`- and `supervisor`-level hooks. An alternative wiring to event\n * subscription for apps that already compose cross-cutting concerns\n * through the agent middleware pipeline (`[cache, budget, guardrail,\n * observability]`). Declaring both hook maps lets a single middleware\n * object work uniformly on agents (which fire the `execute` map) and\n * supervisors (which fire the `supervisor` map) — registering it on a\n * supervisor would otherwise install and collect nothing silently.\n *\n * Both terminal paths are covered on each surface:\n * - `after` — fires on a run that produced a result. A run can complete\n * with `result.error` populated (the engine still calls `after`), so\n * the report AND the envelope error are collected; the error type and\n * message land on the root span.\n * - `onError` — fires when the run threw before assembling a result. The\n * error carries the partial result's report on its envelope; when\n * present it is collected, with the error itself threaded onto the\n * root span so failed runs still produce a trace.\n *\n * The hooks never return a value, so they never mutate the agent's /\n * supervisor's result. The `collect` call is fire-and-forget relative to\n * the run — the collector isolates exporter failures internally, and we\n * additionally swallow any rejection here so an observability fault can\n * never surface on the run's hot path.\n *\n * @param collector - the collector traces are fed into.\n * @param name - stable middleware name (kebab-case). Defaults to\n * `\"panoptic\"`.\n */\nexport function createPanopticMiddleware(\n collector: CollectorContract,\n name = \"panoptic\",\n): AgentMiddleware {\n const collectReport = (report: unknown, rootError?: unknown): void => {\n if (!isReport(report)) {\n return;\n }\n\n void collector.collect(report, rootError).catch(() => {\n // Swallow — the collector already isolates exporter failures; this\n // guard keeps an observability fault off the run's hot path.\n });\n };\n\n const onResult = (result: unknown): void => {\n // A run can complete with `result.error` populated (`after` still\n // fires); thread that envelope error onto the root span.\n collectReport((result as { report?: unknown }).report, (result as { error?: unknown }).error);\n };\n\n const onError = (error: unknown): void => {\n // A failed run's report rides on the error envelope when the engine\n // built one before throwing; collect it so failures trace, threading\n // the error itself onto the root span.\n collectReport((error as { report?: unknown }).report, error);\n };\n\n const terminalHooks = {\n after(_ctx: unknown, result: unknown) {\n onResult(result);\n },\n onError(_ctx: unknown, error: unknown) {\n onError(error);\n },\n };\n\n return {\n name,\n execute: terminalHooks as AgentMiddleware[\"execute\"],\n supervisor: terminalHooks as AgentMiddleware[\"supervisor\"],\n };\n}\n\n/**\n * Narrow an unknown value to a `BaseReport`-shaped object. Structural\n * (checks the lineage fields the collector reads) so it accepts any\n * primitive's report subtype without importing each concrete type.\n */\nfunction isReport(value: unknown): value is import(\"@warlock.js/ai\").BaseReport {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as { runId?: unknown }).runId === \"string\" &&\n typeof (value as { rootRunId?: unknown }).rootRunId === \"string\"\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,SAAgB,yBACd,WACA,OAAO,YACU;CACjB,MAAM,iBAAiB,QAAiB,cAA8B;EACpE,IAAI,CAAC,SAAS,MAAM,GAClB;EAGF,AAAK,UAAU,QAAQ,QAAQ,SAAS,CAAC,CAAC,YAAY,CAGtD,CAAC;CACH;CAEA,MAAM,YAAY,WAA0B;EAG1C,cAAe,OAAgC,QAAS,OAA+B,KAAK;CAC9F;CAEA,MAAM,WAAW,UAAyB;EAIxC,cAAe,MAA+B,QAAQ,KAAK;CAC7D;CAEA,MAAM,gBAAgB;EACpB,MAAM,MAAe,QAAiB;GACpC,SAAS,MAAM;EACjB;EACA,QAAQ,MAAe,OAAgB;GACrC,QAAQ,KAAK;EACf;CACF;CAEA,OAAO;EACL;EACA,SAAS;EACT,YAAY;CACd;AACF;;;;;;AAOA,SAAS,SAAS,OAA8D;CAC9E,OACE,OAAO,UAAU,YACjB,UAAU,QACV,OAAQ,MAA8B,UAAU,YAChD,OAAQ,MAAkC,cAAc;AAE5D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panoptic.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/panoptic/panoptic.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"panoptic.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/panoptic/panoptic.ts"],"mappings":";;;;;AA8LA;;;;;;;;AAAiE;;;;;;;;;;;;;;;;;;;;iBAAjD,QAAA,CAAS,OAAA,GAAS,eAAA,GAAuB,QAAQ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"panoptic.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/panoptic/panoptic.ts"],"sourcesContent":["import type { AgentMiddleware, BaseReport } from \"@warlock.js/ai\";\nimport { createCollector } from \"../collector/collector\";\nimport type { CollectorContract } from \"../contracts/collector.contract\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { createPanopticMiddleware } from \"./panoptic-middleware\";\nimport type { CompletedEventPayload, PanopticTarget } from \"./panoptic-target.type\";\nimport type { Panoptic, PanopticOptions } from \"./panoptic.type\";\n\n/**\n * Terminal `*.completed` events of every core primitive that carries the\n * finalized `result` (and therefore the `report` tree). These fire once\n * per run regardless of outcome — the matching `*.error` event fires\n * first on failure, then `*.completed` still fires — so subscribing here\n * captures completed, failed, and cancelled runs alike.\n *\n * The orchestrator is intentionally absent: its `orchestrator.turn.*`\n * events carry only session identity, not a result. Collect an\n * orchestrator turn via {@link Panoptic.collect} with\n * `result.report` instead.\n */\nconst DEFAULT_COMPLETED_EVENTS = [\n \"agent.completed\",\n \"workflow.completed\",\n \"supervisor.completed\",\n] as const;\n\n/**\n * The Panoptic subscriber — binds a collector + its exporters to the\n * three feed paths (events, middleware, direct). Instantiated via\n * {@link panoptic}; callers never see `new`.\n */\nclass PanopticSubscriber implements Panoptic {\n public readonly collector: CollectorContract;\n\n private readonly completedEvents: string[];\n\n private readonly middlewareName: string;\n\n public constructor(options: PanopticOptions = {}) {\n this.collector =\n options.collector ??\n createCollector({\n captureContent: options.captureContent,\n redactContent: options.redactContent,\n fullHistory: options.fullHistory,\n onError: options.onError,\n });\n\n for (const exporter of options.exporters ?? []) {\n this.collector.use(exporter);\n }\n\n this.completedEvents
|
|
1
|
+
{"version":3,"file":"panoptic.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/panoptic/panoptic.ts"],"sourcesContent":["import type { AgentMiddleware, BaseReport } from \"@warlock.js/ai\";\nimport { createCollector } from \"../collector/collector\";\nimport type { CollectorContract } from \"../contracts/collector.contract\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { createPanopticMiddleware } from \"./panoptic-middleware\";\nimport type { CompletedEventPayload, PanopticTarget } from \"./panoptic-target.type\";\nimport type { Panoptic, PanopticOptions } from \"./panoptic.type\";\n\n/**\n * Terminal `*.completed` events of every core primitive that carries the\n * finalized `result` (and therefore the `report` tree). These fire once\n * per run regardless of outcome — the matching `*.error` event fires\n * first on failure, then `*.completed` still fires — so subscribing here\n * captures completed, failed, and cancelled runs alike.\n *\n * The orchestrator is intentionally absent: its `orchestrator.turn.*`\n * events carry only session identity, not a result. Collect an\n * orchestrator turn via {@link Panoptic.collect} with\n * `result.report` instead.\n */\nconst DEFAULT_COMPLETED_EVENTS = [\n \"agent.completed\",\n \"workflow.completed\",\n \"supervisor.completed\",\n] as const;\n\n/**\n * The Panoptic subscriber — binds a collector + its exporters to the\n * three feed paths (events, middleware, direct). Instantiated via\n * {@link panoptic}; callers never see `new`.\n */\nclass PanopticSubscriber implements Panoptic {\n public readonly collector: CollectorContract;\n\n private readonly completedEvents: string[];\n\n private readonly middlewareName: string;\n\n public constructor(options: PanopticOptions = {}) {\n this.collector =\n options.collector ??\n createCollector({\n captureContent: options.captureContent,\n redactContent: options.redactContent,\n fullHistory: options.fullHistory,\n onError: options.onError,\n });\n\n for (const exporter of options.exporters ?? []) {\n this.collector.use(exporter);\n }\n\n this.completedEvents = options.completedEvents ?? [...DEFAULT_COMPLETED_EVENTS];\n this.middlewareName = options.middlewareName ?? \"panoptic\";\n }\n\n public use(exporter: ExporterContract): Panoptic {\n this.collector.use(exporter);\n\n return this;\n }\n\n public attach(target: PanopticTarget): () => void {\n const unsubscribes: Array<() => void> = [];\n\n for (const event of this.completedEvents) {\n const unsubscribe = target.on(event, (payload) => {\n this.handleCompleted(payload);\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe();\n }\n };\n }\n\n public middleware(): AgentMiddleware {\n return createPanopticMiddleware(this.collector, this.middlewareName);\n }\n\n public async collect(report: BaseReport): Promise<void> {\n await this.collector.collect(report);\n }\n\n public toTrace(report: BaseReport): Trace {\n return this.collector.toTrace(report);\n }\n\n public async flush(): Promise<void> {\n await this.collector.flush();\n }\n\n public async shutdown(): Promise<void> {\n await this.collector.shutdown();\n }\n\n /**\n * Project one terminal `*.completed` payload's report into the\n * collector. The fan-out is fire-and-forget relative to the emitting\n * run: the core swallows handler errors, the collector isolates\n * exporter failures, and we additionally guard the rejection here so an\n * observability fault never escapes the event handler.\n */\n private handleCompleted(payload: unknown): void {\n const report = readReport(payload);\n\n if (!report) {\n return;\n }\n\n // The failing run's typed error lives on the result envelope\n // (`BaseResult.error`), never on the report tree — thread it so a\n // failed root span carries its error type/message.\n const rootError = readResultError(payload);\n\n void this.collector.collect(report, rootError).catch(() => {\n // Swallow — see the JSDoc above. Never surface on the run.\n });\n }\n}\n\n/**\n * Read the envelope error off a primitive's completed-event payload\n * (`{ result: { error } }`). The error rides on the result envelope, not\n * the report tree, so the collector needs it separately to populate a\n * failed root span. Returns `undefined` when the run succeeded.\n */\nfunction readResultError(payload: unknown): unknown {\n const result = (payload as Partial<CompletedEventPayload>)?.result;\n\n return (result as { error?: unknown })?.error;\n}\n\n/**\n * Read the `report` tree off a primitive's completed-event payload.\n * Structural (no concrete-type import) so it accepts every primitive's\n * result subtype; returns `undefined` when the payload isn't the\n * expected `{ result: { report } }` shape.\n */\nfunction readReport(payload: unknown): BaseReport | undefined {\n const result = (payload as Partial<CompletedEventPayload>)?.result;\n const report = (result as { report?: unknown })?.report;\n\n if (\n typeof report === \"object\" &&\n report !== null &&\n typeof (report as { runId?: unknown }).runId === \"string\" &&\n typeof (report as { rootRunId?: unknown }).rootRunId === \"string\"\n ) {\n return report as BaseReport;\n }\n\n return undefined;\n}\n\n/**\n * Create a Panoptic subscriber — the one-call entry point that wires the\n * observability pipeline. Pass the exporters you want and Panoptic\n * builds a collector, registers them, and hands back a subscriber you can\n * `attach()` to any agent/workflow/supervisor, install as agent\n * `middleware()`, or feed reports to directly with `collect()`.\n *\n * @example\n * // Attach to a primitive's event stream (captures every run):\n * const observe = panoptic({\n * exporters: [consoleExporter(), otelExporter({ tracerName: \"app\" })],\n * });\n *\n * const agent = ai.agent({ model });\n * const detach = observe.attach(agent);\n *\n * await agent.execute(\"Summarize this\");\n * // ...later, on shutdown:\n * await observe.shutdown();\n *\n * @example\n * // Or wire it through the agent middleware pipeline:\n * const observe = panoptic({ exporters: [langfuseExporter({ ... })] });\n * const agent = ai.agent({ model, middleware: [observe.middleware()] });\n *\n * @example\n * // Orchestrator turns carry no result-bearing event — collect directly:\n * const result = await orchestrator.execute(input, { sessionId });\n * await observe.collect(result.report);\n */\nexport function panoptic(options: PanopticOptions = {}): Panoptic {\n return new PanopticSubscriber(options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqBA,MAAM,2BAA2B;CAC/B;CACA;CACA;AACF;;;;;;AAOA,IAAM,qBAAN,MAA6C;CAC3C,AAAgB;CAEhB,AAAiB;CAEjB,AAAiB;CAEjB,AAAO,YAAY,UAA2B,CAAC,GAAG;EAChD,KAAK,YACH,QAAQ,aACR,gBAAgB;GACd,gBAAgB,QAAQ;GACxB,eAAe,QAAQ;GACvB,aAAa,QAAQ;GACrB,SAAS,QAAQ;EACnB,CAAC;EAEH,KAAK,MAAM,YAAY,QAAQ,aAAa,CAAC,GAC3C,KAAK,UAAU,IAAI,QAAQ;EAG7B,KAAK,kBAAkB,QAAQ,mBAAmB,CAAC,GAAG,wBAAwB;EAC9E,KAAK,iBAAiB,QAAQ,kBAAkB;CAClD;CAEA,AAAO,IAAI,UAAsC;EAC/C,KAAK,UAAU,IAAI,QAAQ;EAE3B,OAAO;CACT;CAEA,AAAO,OAAO,QAAoC;EAChD,MAAM,eAAkC,CAAC;EAEzC,KAAK,MAAM,SAAS,KAAK,iBAAiB;GACxC,MAAM,cAAc,OAAO,GAAG,QAAQ,YAAY;IAChD,KAAK,gBAAgB,OAAO;GAC9B,CAAC;GAED,aAAa,KAAK,WAAW;EAC/B;EAEA,aAAa;GACX,KAAK,MAAM,eAAe,cACxB,YAAY;EAEhB;CACF;CAEA,AAAO,aAA8B;EACnC,OAAO,yBAAyB,KAAK,WAAW,KAAK,cAAc;CACrE;CAEA,MAAa,QAAQ,QAAmC;EACtD,MAAM,KAAK,UAAU,QAAQ,MAAM;CACrC;CAEA,AAAO,QAAQ,QAA2B;EACxC,OAAO,KAAK,UAAU,QAAQ,MAAM;CACtC;CAEA,MAAa,QAAuB;EAClC,MAAM,KAAK,UAAU,MAAM;CAC7B;CAEA,MAAa,WAA0B;EACrC,MAAM,KAAK,UAAU,SAAS;CAChC;;;;;;;;CASA,AAAQ,gBAAgB,SAAwB;EAC9C,MAAM,SAAS,WAAW,OAAO;EAEjC,IAAI,CAAC,QACH;EAMF,MAAM,YAAY,gBAAgB,OAAO;EAEzC,AAAK,KAAK,UAAU,QAAQ,QAAQ,SAAS,CAAC,CAAC,YAAY,CAE3D,CAAC;CACH;AACF;;;;;;;AAQA,SAAS,gBAAgB,SAA2B;CAGlD,QAFgB,SAA4C,OAE9C,EAA0B;AAC1C;;;;;;;AAQA,SAAS,WAAW,SAA0C;CAE5D,MAAM,UADU,SAA4C,OACtC,EAA2B;CAEjD,IACE,OAAO,WAAW,YAClB,WAAW,QACX,OAAQ,OAA+B,UAAU,YACjD,OAAQ,OAAmC,cAAc,UAEzD,OAAO;AAIX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,SAAS,UAA2B,CAAC,GAAa;CAChE,OAAO,IAAI,mBAAmB,OAAO;AACvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-trace-store.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/cache-trace-store.ts"],"mappings":";;;;;;;AAoBA;;;;;;;;;KAAY,gBAAA,GAIR,WAAA,oBAEO,WAAA,aAAwB,OAAA,CAAQ,WAAA;;;;KAK/B,sBAAA;EAL0C;AAKtD;;;;EAME,MAAA;
|
|
1
|
+
{"version":3,"file":"cache-trace-store.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/cache-trace-store.ts"],"mappings":";;;;;;;AAoBA;;;;;;;;;KAAY,gBAAA,GAIR,WAAA,oBAEO,WAAA,aAAwB,OAAA,CAAQ,WAAA;;;;KAK/B,sBAAA;EAL0C;AAKtD;;;;EAME,MAAA;EA8YU;;;;;EAxYV,QAAQ;AAAA;;;;;;;KAwYE,qBAAA,GAAwB,kBAAA,GAClC,gBAAA;EAoCc;;;;;EA9BZ,KAAA,IAAS,OAAA;AAAA;;;;;;;;;;AAiCW;;;;;;;;;;;;;;;;;;iBAHR,qBAAA,CACd,KAAA,EAAO,gBAAA,EACP,OAAA,GAAS,sBAAA;EAA2B,OAAA,IAAW,KAAA;AAAA,IAC9C,qBAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-trace-store.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/cache-trace-store.ts"],"sourcesContent":["import type { CacheDriver } from \"@warlock.js/cache\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { matchTrace } from \"./match-trace\";\nimport { emptyUsage, sumUsage } from \"./sum-usage\";\nimport type { TraceAggregate } from \"./trace-aggregate.type\";\nimport type { TraceQuery } from \"./trace-query.type\";\nimport type { TraceStoreContract } from \"./trace-store.contract\";\n\n/**\n * A {@link CacheDriver} instance, or a (possibly async) factory that\n * yields one on first use. The factory form lets production defer an\n * expensive connect (e.g. the Redis handshake) until the first trace is\n * actually written, and keeps the dashboard wiring free of a live driver\n * at module-import time.\n *\n * Typed `CacheDriver<any, any>` because the store only ever touches the\n * driver's `get` / `set` / `remove` surface and is agnostic to the\n * concrete client + options of whichever driver backs it.\n */\nexport type CacheDriverInput =\n // The store is driver-agnostic; it only uses get/set/remove, so the\n // concrete client/options generics are intentionally unconstrained.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | CacheDriver<any, any>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | (() => CacheDriver<any, any> | Promise<CacheDriver<any, any>>);\n\n/**\n * Options for {@link createCacheTraceStore}.\n */\nexport type CacheTraceStoreOptions = {\n /**\n * Key prefix every cache entry this store writes is namespaced under.\n * Per-trace keys are `${prefix}:trace:${traceId}`; the newest-first\n * index lives at `${prefix}:index`. Default `\"panoptic\"`.\n */\n prefix?: string;\n /**\n * Maximum number of traces to retain. When set and exceeded, the\n * oldest-ingested trace is evicted from both the cache and the in-memory\n * mirror (insertion-order FIFO). Absent / `0` = unbounded.\n */\n capacity?: number;\n};\n\n/** One entry in the persisted newest-first index. */\ntype IndexEntry = {\n /** The trace's `traceId` — the suffix of its `${prefix}:trace:` key. */\n id: string;\n /**\n * Monotonic insertion order from an internal counter — NOT a wall clock.\n * Used only to keep the index in stable insertion order across a restart\n * so FIFO eviction stays honest.\n */\n addedAt: number;\n};\n\nconst DEFAULT_PREFIX = \"panoptic\";\n\n/**\n * Cache-backed {@link TraceStoreContract} with a **write-through** design\n * that reconciles the synchronous store contract with an asynchronous\n * cache driver.\n *\n * **How the sync/async tension is resolved.** The contract's `get` /\n * `query` / `aggregate` / `size` are synchronous (the dashboard polls them\n * on every request and the in-memory store answers instantly). A cache\n * driver is async. So this store keeps an **in-memory read mirror** — the\n * same insertion-ordered `Map<traceId, Trace>` the in-memory store uses —\n * and serves every read from it synchronously. Writes go **through** to the\n * cache: `add` updates the mirror immediately, then asynchronously persists\n * the trace + index to the cache (errors are swallowed via an optional\n * `onError` hook so a flaky cache never throws into the collector's hot\n * path). On process restart, {@link CacheTraceStore.ready} re-hydrates the\n * mirror from the cache so traces survive the restart.\n *\n * **Durability is best-effort.** Reads never wait on the cache; the mirror\n * is the source of truth at runtime and the cache is the durable backing\n * store. A write that the cache rejects is still visible in the mirror for\n * the life of the process — it just won't survive a restart.\n *\n * **Lazy driver resolution.** The driver (or its async factory) is resolved\n * on first use and memoized, so a production deployment can defer the Redis\n * connect until the first trace is collected, and the dashboard can be\n * wired with a factory at import time without a live connection.\n *\n * Doubles as an {@link ExporterContract} (`export` ≡ `add`), so it drops\n * straight into a collector via `collector.use(store)`.\n *\n * Instantiated via {@link createCacheTraceStore}; callers never see `new`.\n */\nclass CacheTraceStore implements TraceStoreContract, ExporterContract {\n /** Stable exporter id so a collector can dedupe / log this sink. */\n public readonly name = \"cache-trace-store\";\n\n /**\n * In-memory read mirror keyed by `traceId`. A `Map` preserves insertion\n * order, which newest-first `query` ordering and FIFO eviction both rely\n * on. Every read is served from here synchronously.\n */\n private readonly mirror = new Map<string, Trace>();\n\n private readonly prefix: string;\n\n private readonly capacity: number;\n\n /**\n * Monotonic insertion counter — the source of `IndexEntry.addedAt`.\n * Deliberately NOT `Date.now()`: an internal counter guarantees a stable\n * total order for the index even when many traces land in the same\n * millisecond.\n */\n private addCounter = 0;\n\n /** The optional input — a driver, a factory, or `undefined`. */\n private readonly input: CacheDriverInput;\n\n // The store is driver-agnostic; only get/set/remove are used.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private resolvedDriver?: CacheDriver<any, any>;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private driverPending?: Promise<CacheDriver<any, any>>;\n\n private readonly onError?: (error: unknown) => void;\n\n public constructor(\n input: CacheDriverInput,\n options: CacheTraceStoreOptions & { onError?: (error: unknown) => void } = {},\n ) {\n this.input = input;\n this.prefix = options.prefix ?? DEFAULT_PREFIX;\n this.capacity = options.capacity ?? 0;\n this.onError = options.onError;\n }\n\n public get size(): number {\n return this.mirror.size;\n }\n\n /**\n * Hydrate the in-memory mirror from the cache. Idempotent-safe to call\n * once at startup (the dashboard / config wiring awaits it). Reads the\n * persisted index, fetches each referenced trace, and replays them into\n * the mirror in insertion order so newest-first ordering + eviction stay\n * correct after a restart. A cache failure is routed to `onError` and\n * leaves the mirror empty rather than throwing.\n */\n public async ready(): Promise<void> {\n try {\n const driver = await this.resolveDriver();\n const index = await driver.get<IndexEntry[]>(this.indexKey());\n\n if (!Array.isArray(index)) {\n return;\n }\n\n // Oldest-first replay so the mirror's insertion order matches the\n // original ingestion order.\n const ordered = [...index].sort((left, right) => left.addedAt - right.addedAt);\n\n for (const entry of ordered) {\n const trace = await driver.get<Trace>(this.traceKey(entry.id));\n\n if (trace !== null && trace !== undefined) {\n this.mirror.delete(trace.traceId);\n this.mirror.set(trace.traceId, trace);\n }\n\n if (entry.addedAt >= this.addCounter) {\n this.addCounter = entry.addedAt + 1;\n }\n }\n\n this.evictOverflow();\n } catch (error) {\n this.reportError(error);\n }\n }\n\n public add(trace: Trace): void {\n // Mirror update is synchronous and authoritative for runtime reads.\n this.mirror.delete(trace.traceId);\n this.mirror.set(trace.traceId, trace);\n\n const addedAt = this.addCounter;\n this.addCounter += 1;\n\n const evicted = this.evictOverflow();\n\n // Write through to the cache fire-and-forget; reads never wait on this.\n void this.persist(trace, addedAt, evicted);\n }\n\n /**\n * `ExporterContract.export` — a collector dispatches a completed trace\n * here, which is exactly an `add`.\n */\n public export(trace: Trace): void {\n this.add(trace);\n }\n\n public get(traceId: string): Trace | undefined {\n return this.mirror.get(traceId);\n }\n\n public query(filter?: TraceQuery): Trace[] {\n const matched: Trace[] = [];\n\n for (const trace of this.mirror.values()) {\n if (matchTrace(trace, filter)) {\n matched.push(trace);\n }\n }\n\n return this.sortNewestFirst(matched);\n }\n\n public aggregate(filter?: TraceQuery): TraceAggregate {\n const aggregate: TraceAggregate = {\n traces: 0,\n completed: 0,\n failed: 0,\n cancelled: 0,\n usage: emptyUsage(),\n totalDuration: 0,\n };\n\n for (const trace of this.mirror.values()) {\n if (!matchTrace(trace, filter)) {\n continue;\n }\n\n aggregate.traces += 1;\n aggregate.totalDuration += trace.duration;\n aggregate.usage = sumUsage(aggregate.usage, trace.usage);\n\n this.countStatus(aggregate, trace);\n }\n\n if (aggregate.usage.cost !== undefined) {\n aggregate.cost = aggregate.usage.cost;\n }\n\n return aggregate;\n }\n\n public clear(): void {\n const ids = [...this.mirror.keys()];\n this.mirror.clear();\n\n void this.purge(ids);\n }\n\n /**\n * Persist one trace + the rebuilt index to the cache, optionally removing\n * a trace evicted by the capacity cap. Best-effort: any cache failure is\n * routed to `onError`, never thrown — the mirror already reflects the\n * write so runtime reads are unaffected.\n */\n private async persist(\n trace: Trace,\n addedAt: number,\n evictedId: string | undefined,\n ): Promise<void> {\n try {\n const driver = await this.resolveDriver();\n\n await driver.set(this.traceKey(trace.traceId), trace);\n\n if (evictedId !== undefined) {\n await driver.remove(this.traceKey(evictedId));\n }\n\n await driver.set(this.indexKey(), this.buildIndex(addedAt));\n } catch (error) {\n this.reportError(error);\n }\n }\n\n /** Best-effort removal of every persisted trace + the index on `clear`. */\n private async purge(ids: string[]): Promise<void> {\n try {\n const driver = await this.resolveDriver();\n\n for (const id of ids) {\n await driver.remove(this.traceKey(id));\n }\n\n await driver.remove(this.indexKey());\n } catch (error) {\n this.reportError(error);\n }\n }\n\n /**\n * Rebuild the newest-first index from the current mirror. The mirror's\n * `Map` iteration is oldest-first insertion order; we walk it and assign\n * `addedAt` from the surviving counter span so the persisted order\n * matches the in-memory one. The freshest entry uses `latestAddedAt`.\n */\n private buildIndex(latestAddedAt: number): IndexEntry[] {\n const ids = [...this.mirror.keys()];\n const base = latestAddedAt - (ids.length - 1);\n\n return ids.map((id, offset) => ({ id, addedAt: base + offset }));\n }\n\n /**\n * Resolve the driver once and memoize. Supports a bare driver, a sync\n * factory, and an async factory. Concurrent first-callers share one\n * in-flight resolution.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private async resolveDriver(): Promise<CacheDriver<any, any>> {\n if (this.resolvedDriver !== undefined) {\n return this.resolvedDriver;\n }\n\n if (this.driverPending !== undefined) {\n return this.driverPending;\n }\n\n const candidate =\n typeof this.input === \"function\"\n ? (this.input as () =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | CacheDriver<any, any>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | Promise<CacheDriver<any, any>>)()\n : this.input;\n\n this.driverPending = Promise.resolve(candidate);\n\n try {\n this.resolvedDriver = await this.driverPending;\n\n return this.resolvedDriver;\n } finally {\n this.driverPending = undefined;\n }\n }\n\n /** `${prefix}:trace:${traceId}` — the per-trace cache key. */\n private traceKey(traceId: string): string {\n return `${this.prefix}:trace:${traceId}`;\n }\n\n /** `${prefix}:index` — the newest-first index cache key. */\n private indexKey(): string {\n return `${this.prefix}:index`;\n }\n\n /** Route a swallowed cache error to the optional handler. */\n private reportError(error: unknown): void {\n if (this.onError !== undefined) {\n this.onError(error);\n }\n }\n\n /**\n * Increment the matching terminal-status counter for one trace.\n * Mirrors the in-memory store: non-terminal statuses are counted in\n * `traces` but tracked by none of the three headline counters.\n */\n private countStatus(aggregate: TraceAggregate, trace: Trace): void {\n switch (trace.root.status) {\n case \"completed\": {\n aggregate.completed += 1;\n break;\n }\n\n case \"failed\": {\n aggregate.failed += 1;\n break;\n }\n\n case \"cancelled\": {\n aggregate.cancelled += 1;\n break;\n }\n\n default: {\n break;\n }\n }\n }\n\n /**\n * Sort matched traces newest-started first. A copy is sorted so the\n * mirror's insertion order (which eviction depends on) is never disturbed.\n */\n private sortNewestFirst(traces: Trace[]): Trace[] {\n return traces.sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt));\n }\n\n /**\n * Evict oldest-inserted traces from the mirror until within `capacity`.\n * Returns the id of the single evicted trace (the common case — one `add`\n * pushes at most one over the cap) so the caller can remove it from the\n * cache too. No-op + `undefined` when unbounded or within the cap.\n */\n private evictOverflow(): string | undefined {\n if (this.capacity <= 0) {\n return undefined;\n }\n\n let evicted: string | undefined;\n\n while (this.mirror.size > this.capacity) {\n const oldest = this.mirror.keys().next().value;\n\n if (oldest === undefined) {\n return evicted;\n }\n\n this.mirror.delete(oldest);\n evicted = oldest;\n }\n\n return evicted;\n }\n}\n\n/**\n * The concrete store type returned by {@link createCacheTraceStore} — the\n * standard {@link TraceStoreContract} + {@link ExporterContract} surface,\n * plus a `ready()` to hydrate the in-memory mirror from the cache on\n * startup so traces survive a process restart.\n */\nexport type CacheTraceStoreHandle = TraceStoreContract &\n ExporterContract & {\n /**\n * Hydrate the in-memory mirror from the cache. Await once at startup\n * (the dashboard wiring does this for you) so previously-persisted\n * traces are queryable after a restart.\n */\n ready(): Promise<void>;\n };\n\n/**\n * Create a cache-backed trace store. Reads are served synchronously from an\n * in-memory mirror; writes go through to the cache, and {@link\n * CacheTraceStoreHandle.ready} re-hydrates the mirror on startup so traces\n * survive a restart. See {@link CacheTraceStore} for the full write-through\n * design.\n *\n * @param cache a {@link CacheDriver}, or a (possibly async) factory that\n * yields one on first use — resolved lazily and memoized so a production\n * Redis connect can be deferred until the first trace is collected.\n * @param options `prefix` (default `\"panoptic\"`), `capacity` (FIFO cap),\n * and an optional `onError` hook for swallowed cache write failures.\n *\n * @example\n * import { RedisCacheDriver } from \"@warlock.js/cache\";\n *\n * // Lazy async factory — defers the Redis connect until first use.\n * const store = createCacheTraceStore(async () => {\n * const driver = new RedisCacheDriver();\n * await driver.connect();\n * return driver;\n * });\n *\n * await store.ready(); // hydrate from a prior run\n * collector.use(store); // fills as traces complete\n * const failed = store.query({ status: \"failed\" });\n */\nexport function createCacheTraceStore(\n cache: CacheDriverInput,\n options: CacheTraceStoreOptions & { onError?: (error: unknown) => void } = {},\n): CacheTraceStoreHandle {\n return new CacheTraceStore(cache, options);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cache-trace-store.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/cache-trace-store.ts"],"sourcesContent":["import type { CacheDriver } from \"@warlock.js/cache\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { matchTrace } from \"./match-trace\";\nimport { emptyUsage, sumUsage } from \"./sum-usage\";\nimport type { TraceAggregate } from \"./trace-aggregate.type\";\nimport type { TraceQuery } from \"./trace-query.type\";\nimport type { TraceStoreContract } from \"./trace-store.contract\";\n\n/**\n * A {@link CacheDriver} instance, or a (possibly async) factory that\n * yields one on first use. The factory form lets production defer an\n * expensive connect (e.g. the Redis handshake) until the first trace is\n * actually written, and keeps the dashboard wiring free of a live driver\n * at module-import time.\n *\n * Typed `CacheDriver<any, any>` because the store only ever touches the\n * driver's `get` / `set` / `remove` surface and is agnostic to the\n * concrete client + options of whichever driver backs it.\n */\nexport type CacheDriverInput =\n // The store is driver-agnostic; it only uses get/set/remove, so the\n // concrete client/options generics are intentionally unconstrained.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | CacheDriver<any, any>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | (() => CacheDriver<any, any> | Promise<CacheDriver<any, any>>);\n\n/**\n * Options for {@link createCacheTraceStore}.\n */\nexport type CacheTraceStoreOptions = {\n /**\n * Key prefix every cache entry this store writes is namespaced under.\n * Per-trace keys are `${prefix}:trace:${traceId}`; the newest-first\n * index lives at `${prefix}:index`. Default `\"panoptic\"`.\n */\n prefix?: string;\n /**\n * Maximum number of traces to retain. When set and exceeded, the\n * oldest-ingested trace is evicted from both the cache and the in-memory\n * mirror (insertion-order FIFO). Absent / `0` = unbounded.\n */\n capacity?: number;\n};\n\n/** One entry in the persisted newest-first index. */\ntype IndexEntry = {\n /** The trace's `traceId` — the suffix of its `${prefix}:trace:` key. */\n id: string;\n /**\n * Monotonic insertion order from an internal counter — NOT a wall clock.\n * Used only to keep the index in stable insertion order across a restart\n * so FIFO eviction stays honest.\n */\n addedAt: number;\n};\n\nconst DEFAULT_PREFIX = \"panoptic\";\n\n/**\n * Cache-backed {@link TraceStoreContract} with a **write-through** design\n * that reconciles the synchronous store contract with an asynchronous\n * cache driver.\n *\n * **How the sync/async tension is resolved.** The contract's `get` /\n * `query` / `aggregate` / `size` are synchronous (the dashboard polls them\n * on every request and the in-memory store answers instantly). A cache\n * driver is async. So this store keeps an **in-memory read mirror** — the\n * same insertion-ordered `Map<traceId, Trace>` the in-memory store uses —\n * and serves every read from it synchronously. Writes go **through** to the\n * cache: `add` updates the mirror immediately, then asynchronously persists\n * the trace + index to the cache (errors are swallowed via an optional\n * `onError` hook so a flaky cache never throws into the collector's hot\n * path). On process restart, {@link CacheTraceStore.ready} re-hydrates the\n * mirror from the cache so traces survive the restart.\n *\n * **Durability is best-effort.** Reads never wait on the cache; the mirror\n * is the source of truth at runtime and the cache is the durable backing\n * store. A write that the cache rejects is still visible in the mirror for\n * the life of the process — it just won't survive a restart.\n *\n * **Lazy driver resolution.** The driver (or its async factory) is resolved\n * on first use and memoized, so a production deployment can defer the Redis\n * connect until the first trace is collected, and the dashboard can be\n * wired with a factory at import time without a live connection.\n *\n * Doubles as an {@link ExporterContract} (`export` ≡ `add`), so it drops\n * straight into a collector via `collector.use(store)`.\n *\n * Instantiated via {@link createCacheTraceStore}; callers never see `new`.\n */\nclass CacheTraceStore implements TraceStoreContract, ExporterContract {\n /** Stable exporter id so a collector can dedupe / log this sink. */\n public readonly name = \"cache-trace-store\";\n\n /**\n * In-memory read mirror keyed by `traceId`. A `Map` preserves insertion\n * order, which newest-first `query` ordering and FIFO eviction both rely\n * on. Every read is served from here synchronously.\n */\n private readonly mirror = new Map<string, Trace>();\n\n private readonly prefix: string;\n\n private readonly capacity: number;\n\n /**\n * Monotonic insertion counter — the source of `IndexEntry.addedAt`.\n * Deliberately NOT `Date.now()`: an internal counter guarantees a stable\n * total order for the index even when many traces land in the same\n * millisecond.\n */\n private addCounter = 0;\n\n /** The optional input — a driver, a factory, or `undefined`. */\n private readonly input: CacheDriverInput;\n\n // The store is driver-agnostic; only get/set/remove are used.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private resolvedDriver?: CacheDriver<any, any>;\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private driverPending?: Promise<CacheDriver<any, any>>;\n\n private readonly onError?: (error: unknown) => void;\n\n public constructor(\n input: CacheDriverInput,\n options: CacheTraceStoreOptions & { onError?: (error: unknown) => void } = {},\n ) {\n this.input = input;\n this.prefix = options.prefix ?? DEFAULT_PREFIX;\n this.capacity = options.capacity ?? 0;\n this.onError = options.onError;\n }\n\n public get size(): number {\n return this.mirror.size;\n }\n\n /**\n * Hydrate the in-memory mirror from the cache. Idempotent-safe to call\n * once at startup (the dashboard / config wiring awaits it). Reads the\n * persisted index, fetches each referenced trace, and replays them into\n * the mirror in insertion order so newest-first ordering + eviction stay\n * correct after a restart. A cache failure is routed to `onError` and\n * leaves the mirror empty rather than throwing.\n */\n public async ready(): Promise<void> {\n try {\n const driver = await this.resolveDriver();\n const index = await driver.get<IndexEntry[]>(this.indexKey());\n\n if (!Array.isArray(index)) {\n return;\n }\n\n // Oldest-first replay so the mirror's insertion order matches the\n // original ingestion order.\n const ordered = [...index].sort((left, right) => left.addedAt - right.addedAt);\n\n for (const entry of ordered) {\n const trace = await driver.get<Trace>(this.traceKey(entry.id));\n\n if (trace !== null && trace !== undefined) {\n this.mirror.delete(trace.traceId);\n this.mirror.set(trace.traceId, trace);\n }\n\n if (entry.addedAt >= this.addCounter) {\n this.addCounter = entry.addedAt + 1;\n }\n }\n\n this.evictOverflow();\n } catch (error) {\n this.reportError(error);\n }\n }\n\n public add(trace: Trace): void {\n // Mirror update is synchronous and authoritative for runtime reads.\n this.mirror.delete(trace.traceId);\n this.mirror.set(trace.traceId, trace);\n\n const addedAt = this.addCounter;\n this.addCounter += 1;\n\n const evicted = this.evictOverflow();\n\n // Write through to the cache fire-and-forget; reads never wait on this.\n void this.persist(trace, addedAt, evicted);\n }\n\n /**\n * `ExporterContract.export` — a collector dispatches a completed trace\n * here, which is exactly an `add`.\n */\n public export(trace: Trace): void {\n this.add(trace);\n }\n\n public get(traceId: string): Trace | undefined {\n return this.mirror.get(traceId);\n }\n\n public query(filter?: TraceQuery): Trace[] {\n const matched: Trace[] = [];\n\n for (const trace of this.mirror.values()) {\n if (matchTrace(trace, filter)) {\n matched.push(trace);\n }\n }\n\n return this.sortNewestFirst(matched);\n }\n\n public aggregate(filter?: TraceQuery): TraceAggregate {\n const aggregate: TraceAggregate = {\n traces: 0,\n completed: 0,\n failed: 0,\n cancelled: 0,\n usage: emptyUsage(),\n totalDuration: 0,\n };\n\n for (const trace of this.mirror.values()) {\n if (!matchTrace(trace, filter)) {\n continue;\n }\n\n aggregate.traces += 1;\n aggregate.totalDuration += trace.duration;\n aggregate.usage = sumUsage(aggregate.usage, trace.usage);\n\n this.countStatus(aggregate, trace);\n }\n\n if (aggregate.usage.cost !== undefined) {\n aggregate.cost = aggregate.usage.cost;\n }\n\n return aggregate;\n }\n\n public clear(): void {\n const ids = [...this.mirror.keys()];\n this.mirror.clear();\n\n void this.purge(ids);\n }\n\n /**\n * Persist one trace + the rebuilt index to the cache, optionally removing\n * a trace evicted by the capacity cap. Best-effort: any cache failure is\n * routed to `onError`, never thrown — the mirror already reflects the\n * write so runtime reads are unaffected.\n */\n private async persist(\n trace: Trace,\n addedAt: number,\n evictedId: string | undefined,\n ): Promise<void> {\n try {\n const driver = await this.resolveDriver();\n\n await driver.set(this.traceKey(trace.traceId), trace);\n\n if (evictedId !== undefined) {\n await driver.remove(this.traceKey(evictedId));\n }\n\n await driver.set(this.indexKey(), this.buildIndex(addedAt));\n } catch (error) {\n this.reportError(error);\n }\n }\n\n /** Best-effort removal of every persisted trace + the index on `clear`. */\n private async purge(ids: string[]): Promise<void> {\n try {\n const driver = await this.resolveDriver();\n\n for (const id of ids) {\n await driver.remove(this.traceKey(id));\n }\n\n await driver.remove(this.indexKey());\n } catch (error) {\n this.reportError(error);\n }\n }\n\n /**\n * Rebuild the newest-first index from the current mirror. The mirror's\n * `Map` iteration is oldest-first insertion order; we walk it and assign\n * `addedAt` from the surviving counter span so the persisted order\n * matches the in-memory one. The freshest entry uses `latestAddedAt`.\n */\n private buildIndex(latestAddedAt: number): IndexEntry[] {\n const ids = [...this.mirror.keys()];\n const base = latestAddedAt - (ids.length - 1);\n\n return ids.map((id, offset) => ({ id, addedAt: base + offset }));\n }\n\n /**\n * Resolve the driver once and memoize. Supports a bare driver, a sync\n * factory, and an async factory. Concurrent first-callers share one\n * in-flight resolution.\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n private async resolveDriver(): Promise<CacheDriver<any, any>> {\n if (this.resolvedDriver !== undefined) {\n return this.resolvedDriver;\n }\n\n if (this.driverPending !== undefined) {\n return this.driverPending;\n }\n\n const candidate =\n typeof this.input === \"function\"\n ? (\n this.input as () =>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | CacheDriver<any, any>\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n | Promise<CacheDriver<any, any>>\n )()\n : this.input;\n\n this.driverPending = Promise.resolve(candidate);\n\n try {\n this.resolvedDriver = await this.driverPending;\n\n return this.resolvedDriver;\n } finally {\n this.driverPending = undefined;\n }\n }\n\n /** `${prefix}:trace:${traceId}` — the per-trace cache key. */\n private traceKey(traceId: string): string {\n return `${this.prefix}:trace:${traceId}`;\n }\n\n /** `${prefix}:index` — the newest-first index cache key. */\n private indexKey(): string {\n return `${this.prefix}:index`;\n }\n\n /** Route a swallowed cache error to the optional handler. */\n private reportError(error: unknown): void {\n if (this.onError !== undefined) {\n this.onError(error);\n }\n }\n\n /**\n * Increment the matching terminal-status counter for one trace.\n * Mirrors the in-memory store: non-terminal statuses are counted in\n * `traces` but tracked by none of the three headline counters.\n */\n private countStatus(aggregate: TraceAggregate, trace: Trace): void {\n switch (trace.root.status) {\n case \"completed\": {\n aggregate.completed += 1;\n break;\n }\n\n // An incomplete agent run (its answer was cut off) errored.\n case \"failed\":\n case \"incomplete\": {\n aggregate.failed += 1;\n break;\n }\n\n case \"cancelled\": {\n aggregate.cancelled += 1;\n break;\n }\n\n default: {\n break;\n }\n }\n }\n\n /**\n * Sort matched traces newest-started first. A copy is sorted so the\n * mirror's insertion order (which eviction depends on) is never disturbed.\n */\n private sortNewestFirst(traces: Trace[]): Trace[] {\n return traces.sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt));\n }\n\n /**\n * Evict oldest-inserted traces from the mirror until within `capacity`.\n * Returns the id of the single evicted trace (the common case — one `add`\n * pushes at most one over the cap) so the caller can remove it from the\n * cache too. No-op + `undefined` when unbounded or within the cap.\n */\n private evictOverflow(): string | undefined {\n if (this.capacity <= 0) {\n return undefined;\n }\n\n let evicted: string | undefined;\n\n while (this.mirror.size > this.capacity) {\n const oldest = this.mirror.keys().next().value;\n\n if (oldest === undefined) {\n return evicted;\n }\n\n this.mirror.delete(oldest);\n evicted = oldest;\n }\n\n return evicted;\n }\n}\n\n/**\n * The concrete store type returned by {@link createCacheTraceStore} — the\n * standard {@link TraceStoreContract} + {@link ExporterContract} surface,\n * plus a `ready()` to hydrate the in-memory mirror from the cache on\n * startup so traces survive a process restart.\n */\nexport type CacheTraceStoreHandle = TraceStoreContract &\n ExporterContract & {\n /**\n * Hydrate the in-memory mirror from the cache. Await once at startup\n * (the dashboard wiring does this for you) so previously-persisted\n * traces are queryable after a restart.\n */\n ready(): Promise<void>;\n };\n\n/**\n * Create a cache-backed trace store. Reads are served synchronously from an\n * in-memory mirror; writes go through to the cache, and {@link\n * CacheTraceStoreHandle.ready} re-hydrates the mirror on startup so traces\n * survive a restart. See {@link CacheTraceStore} for the full write-through\n * design.\n *\n * @param cache a {@link CacheDriver}, or a (possibly async) factory that\n * yields one on first use — resolved lazily and memoized so a production\n * Redis connect can be deferred until the first trace is collected.\n * @param options `prefix` (default `\"panoptic\"`), `capacity` (FIFO cap),\n * and an optional `onError` hook for swallowed cache write failures.\n *\n * @example\n * import { RedisCacheDriver } from \"@warlock.js/cache\";\n *\n * // Lazy async factory — defers the Redis connect until first use.\n * const store = createCacheTraceStore(async () => {\n * const driver = new RedisCacheDriver();\n * await driver.connect();\n * return driver;\n * });\n *\n * await store.ready(); // hydrate from a prior run\n * collector.use(store); // fills as traces complete\n * const failed = store.query({ status: \"failed\" });\n */\nexport function createCacheTraceStore(\n cache: CacheDriverInput,\n options: CacheTraceStoreOptions & { onError?: (error: unknown) => void } = {},\n): CacheTraceStoreHandle {\n return new CacheTraceStore(cache, options);\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-memory-trace-store.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/in-memory-trace-store.ts"],"mappings":";;;;;;AAWA;KAAY,yBAAA;;;AAOF;
|
|
1
|
+
{"version":3,"file":"in-memory-trace-store.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/in-memory-trace-store.ts"],"mappings":";;;;;;AAWA;KAAY,yBAAA;;;AAOF;AAsLV;;;EAtLE,QAAQ;AAAA;;;;;;;;;AAwL8B;;;;iBAFxB,wBAAA,CACd,OAAA,GAAU,yBAAA,GACT,kBAAA,GAAqB,gBAAA"}
|
|
@@ -82,7 +82,8 @@ var InMemoryTraceStore = class {
|
|
|
82
82
|
* Non-terminal statuses (`awaiting-input`, `max-iterations`) are
|
|
83
83
|
* counted in `traces` but tracked by none of the three headline
|
|
84
84
|
* counters — intentional, those three answer the common
|
|
85
|
-
* "succeeded / errored / aborted" question.
|
|
85
|
+
* "succeeded / errored / aborted" question. An `incomplete` agent run
|
|
86
|
+
* (its answer was cut off) errored, so it counts as `failed`.
|
|
86
87
|
*/
|
|
87
88
|
countStatus(aggregate, trace) {
|
|
88
89
|
switch (trace.root.status) {
|
|
@@ -90,6 +91,7 @@ var InMemoryTraceStore = class {
|
|
|
90
91
|
aggregate.completed += 1;
|
|
91
92
|
break;
|
|
92
93
|
case "failed":
|
|
94
|
+
case "incomplete":
|
|
93
95
|
aggregate.failed += 1;
|
|
94
96
|
break;
|
|
95
97
|
case "cancelled":
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"in-memory-trace-store.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/in-memory-trace-store.ts"],"sourcesContent":["import type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { matchTrace } from \"./match-trace\";\nimport { emptyUsage, sumUsage } from \"./sum-usage\";\nimport type { TraceAggregate } from \"./trace-aggregate.type\";\nimport type { TraceQuery } from \"./trace-query.type\";\nimport type { TraceStoreContract } from \"./trace-store.contract\";\n\n/**\n * Options for {@link createInMemoryTraceStore}.\n */\nexport type InMemoryTraceStoreOptions = {\n /**\n * Maximum number of traces to retain. When set and exceeded, the\n * oldest-ingested trace is evicted (insertion-order FIFO) so the\n * store stays bounded for long-lived processes. Absent / `0` =\n * unbounded (keep everything until `clear`).\n */\n capacity?: number;\n};\n\n/**\n * In-memory {@link TraceStoreContract} that doubles as an\n * {@link ExporterContract} — register it on a collector\n * (`collector.use(store)`) and it fills as traces complete, then query\n * or aggregate it after the fact.\n *\n * Backed by an insertion-ordered `Map` keyed by `traceId`, giving O(1)\n * `get` / `add` / overwrite and O(n) scans for `query` / `aggregate`\n * (the price of an in-memory store with no secondary indexes — fine for\n * the dev/test and modest-volume runtime use this targets). When a\n * `capacity` is configured, ingesting past the cap evicts the oldest\n * trace.\n *\n * Instantiated fresh per store via {@link createInMemoryTraceStore};\n * callers never see `new`.\n */\nclass InMemoryTraceStore implements TraceStoreContract, ExporterContract {\n /** Stable exporter id so a collector can dedupe / log this sink. */\n public readonly name = \"in-memory-trace-store\";\n\n /**\n * Retained traces keyed by `traceId`. A `Map` preserves insertion\n * order, which is what FIFO eviction and newest-first `query` ordering\n * both rely on.\n */\n private readonly traces = new Map<string, Trace>();\n\n private readonly capacity: number;\n\n public constructor(options?: InMemoryTraceStoreOptions) {\n this.capacity = options?.capacity ?? 0;\n }\n\n public get size(): number {\n return this.traces.size;\n }\n\n public add(trace: Trace): void {\n // Re-insert so an overwrite also refreshes insertion position —\n // keeps \"oldest\" honest for FIFO eviction.\n this.traces.delete(trace.traceId);\n this.traces.set(trace.traceId, trace);\n\n this.evictOverflow();\n }\n\n /**\n * `ExporterContract.export` — a collector dispatches a completed\n * trace here, which is exactly an `add`. Lets the store be wired into\n * a collector as a sink without an adapter.\n */\n public export(trace: Trace): void {\n this.add(trace);\n }\n\n public get(traceId: string): Trace | undefined {\n return this.traces.get(traceId);\n }\n\n public query(filter?: TraceQuery): Trace[] {\n const matched: Trace[] = [];\n\n for (const trace of this.traces.values()) {\n if (matchTrace(trace, filter)) {\n matched.push(trace);\n }\n }\n\n return this.sortNewestFirst(matched);\n }\n\n public aggregate(filter?: TraceQuery): TraceAggregate {\n const aggregate: TraceAggregate = {\n traces: 0,\n completed: 0,\n failed: 0,\n cancelled: 0,\n usage: emptyUsage(),\n totalDuration: 0,\n };\n\n for (const trace of this.traces.values()) {\n if (!matchTrace(trace, filter)) {\n continue;\n }\n\n aggregate.traces += 1;\n aggregate.totalDuration += trace.duration;\n aggregate.usage = sumUsage(aggregate.usage, trace.usage);\n\n this.countStatus(aggregate, trace);\n }\n\n if (aggregate.usage.cost !== undefined) {\n aggregate.cost = aggregate.usage.cost;\n }\n\n return aggregate;\n }\n\n public clear(): void {\n this.traces.clear();\n }\n\n /**\n * Increment the matching terminal-status counter for one trace.\n * Non-terminal statuses (`awaiting-input`, `max-iterations`) are\n * counted in `traces` but tracked by none of the three headline\n * counters — intentional, those three answer the common\n * \"succeeded / errored / aborted\" question
|
|
1
|
+
{"version":3,"file":"in-memory-trace-store.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/in-memory-trace-store.ts"],"sourcesContent":["import type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { matchTrace } from \"./match-trace\";\nimport { emptyUsage, sumUsage } from \"./sum-usage\";\nimport type { TraceAggregate } from \"./trace-aggregate.type\";\nimport type { TraceQuery } from \"./trace-query.type\";\nimport type { TraceStoreContract } from \"./trace-store.contract\";\n\n/**\n * Options for {@link createInMemoryTraceStore}.\n */\nexport type InMemoryTraceStoreOptions = {\n /**\n * Maximum number of traces to retain. When set and exceeded, the\n * oldest-ingested trace is evicted (insertion-order FIFO) so the\n * store stays bounded for long-lived processes. Absent / `0` =\n * unbounded (keep everything until `clear`).\n */\n capacity?: number;\n};\n\n/**\n * In-memory {@link TraceStoreContract} that doubles as an\n * {@link ExporterContract} — register it on a collector\n * (`collector.use(store)`) and it fills as traces complete, then query\n * or aggregate it after the fact.\n *\n * Backed by an insertion-ordered `Map` keyed by `traceId`, giving O(1)\n * `get` / `add` / overwrite and O(n) scans for `query` / `aggregate`\n * (the price of an in-memory store with no secondary indexes — fine for\n * the dev/test and modest-volume runtime use this targets). When a\n * `capacity` is configured, ingesting past the cap evicts the oldest\n * trace.\n *\n * Instantiated fresh per store via {@link createInMemoryTraceStore};\n * callers never see `new`.\n */\nclass InMemoryTraceStore implements TraceStoreContract, ExporterContract {\n /** Stable exporter id so a collector can dedupe / log this sink. */\n public readonly name = \"in-memory-trace-store\";\n\n /**\n * Retained traces keyed by `traceId`. A `Map` preserves insertion\n * order, which is what FIFO eviction and newest-first `query` ordering\n * both rely on.\n */\n private readonly traces = new Map<string, Trace>();\n\n private readonly capacity: number;\n\n public constructor(options?: InMemoryTraceStoreOptions) {\n this.capacity = options?.capacity ?? 0;\n }\n\n public get size(): number {\n return this.traces.size;\n }\n\n public add(trace: Trace): void {\n // Re-insert so an overwrite also refreshes insertion position —\n // keeps \"oldest\" honest for FIFO eviction.\n this.traces.delete(trace.traceId);\n this.traces.set(trace.traceId, trace);\n\n this.evictOverflow();\n }\n\n /**\n * `ExporterContract.export` — a collector dispatches a completed\n * trace here, which is exactly an `add`. Lets the store be wired into\n * a collector as a sink without an adapter.\n */\n public export(trace: Trace): void {\n this.add(trace);\n }\n\n public get(traceId: string): Trace | undefined {\n return this.traces.get(traceId);\n }\n\n public query(filter?: TraceQuery): Trace[] {\n const matched: Trace[] = [];\n\n for (const trace of this.traces.values()) {\n if (matchTrace(trace, filter)) {\n matched.push(trace);\n }\n }\n\n return this.sortNewestFirst(matched);\n }\n\n public aggregate(filter?: TraceQuery): TraceAggregate {\n const aggregate: TraceAggregate = {\n traces: 0,\n completed: 0,\n failed: 0,\n cancelled: 0,\n usage: emptyUsage(),\n totalDuration: 0,\n };\n\n for (const trace of this.traces.values()) {\n if (!matchTrace(trace, filter)) {\n continue;\n }\n\n aggregate.traces += 1;\n aggregate.totalDuration += trace.duration;\n aggregate.usage = sumUsage(aggregate.usage, trace.usage);\n\n this.countStatus(aggregate, trace);\n }\n\n if (aggregate.usage.cost !== undefined) {\n aggregate.cost = aggregate.usage.cost;\n }\n\n return aggregate;\n }\n\n public clear(): void {\n this.traces.clear();\n }\n\n /**\n * Increment the matching terminal-status counter for one trace.\n * Non-terminal statuses (`awaiting-input`, `max-iterations`) are\n * counted in `traces` but tracked by none of the three headline\n * counters — intentional, those three answer the common\n * \"succeeded / errored / aborted\" question. An `incomplete` agent run\n * (its answer was cut off) errored, so it counts as `failed`.\n */\n private countStatus(aggregate: TraceAggregate, trace: Trace): void {\n switch (trace.root.status) {\n case \"completed\": {\n aggregate.completed += 1;\n break;\n }\n\n case \"failed\":\n case \"incomplete\": {\n aggregate.failed += 1;\n break;\n }\n\n case \"cancelled\": {\n aggregate.cancelled += 1;\n break;\n }\n\n default: {\n break;\n }\n }\n }\n\n /**\n * Sort matched traces newest-started first. A copy is sorted so the\n * underlying insertion order (which eviction depends on) is never\n * disturbed.\n */\n private sortNewestFirst(traces: Trace[]): Trace[] {\n return traces.sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt));\n }\n\n /**\n * Evict oldest-inserted traces until the store is within `capacity`.\n * No-op when unbounded. The `Map` iterator yields keys in insertion\n * order, so the first key is always the oldest.\n */\n private evictOverflow(): void {\n if (this.capacity <= 0) {\n return;\n }\n\n while (this.traces.size > this.capacity) {\n const oldest = this.traces.keys().next().value;\n\n if (oldest === undefined) {\n return;\n }\n\n this.traces.delete(oldest);\n }\n }\n}\n\n/**\n * Create an in-memory trace store. Optionally bound it with `capacity`\n * for long-lived processes; leave it unset for dev/test where you want\n * every trace retained.\n *\n * @example\n * const store = createInMemoryTraceStore({ capacity: 1000 });\n * collector.use(store);\n * // later:\n * const recentFailures = store.query({ status: \"failed\" });\n * const sessionSpend = store.aggregate({ sessionId });\n */\nexport function createInMemoryTraceStore(\n options?: InMemoryTraceStoreOptions,\n): TraceStoreContract & ExporterContract {\n return new InMemoryTraceStore(options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqCA,IAAM,qBAAN,MAAyE;;CAEvE,AAAgB,OAAO;;;;;;CAOvB,AAAiB,yBAAS,IAAI,IAAmB;CAEjD,AAAiB;CAEjB,AAAO,YAAY,SAAqC;EACtD,KAAK,WAAW,SAAS,YAAY;CACvC;CAEA,IAAW,OAAe;EACxB,OAAO,KAAK,OAAO;CACrB;CAEA,AAAO,IAAI,OAAoB;EAG7B,KAAK,OAAO,OAAO,MAAM,OAAO;EAChC,KAAK,OAAO,IAAI,MAAM,SAAS,KAAK;EAEpC,KAAK,cAAc;CACrB;;;;;;CAOA,AAAO,OAAO,OAAoB;EAChC,KAAK,IAAI,KAAK;CAChB;CAEA,AAAO,IAAI,SAAoC;EAC7C,OAAO,KAAK,OAAO,IAAI,OAAO;CAChC;CAEA,AAAO,MAAM,QAA8B;EACzC,MAAM,UAAmB,CAAC;EAE1B,KAAK,MAAM,SAAS,KAAK,OAAO,OAAO,GACrC,IAAI,WAAW,OAAO,MAAM,GAC1B,QAAQ,KAAK,KAAK;EAItB,OAAO,KAAK,gBAAgB,OAAO;CACrC;CAEA,AAAO,UAAU,QAAqC;EACpD,MAAM,YAA4B;GAChC,QAAQ;GACR,WAAW;GACX,QAAQ;GACR,WAAW;GACX,OAAO,WAAW;GAClB,eAAe;EACjB;EAEA,KAAK,MAAM,SAAS,KAAK,OAAO,OAAO,GAAG;GACxC,IAAI,CAAC,WAAW,OAAO,MAAM,GAC3B;GAGF,UAAU,UAAU;GACpB,UAAU,iBAAiB,MAAM;GACjC,UAAU,QAAQ,SAAS,UAAU,OAAO,MAAM,KAAK;GAEvD,KAAK,YAAY,WAAW,KAAK;EACnC;EAEA,IAAI,UAAU,MAAM,SAAS,QAC3B,UAAU,OAAO,UAAU,MAAM;EAGnC,OAAO;CACT;CAEA,AAAO,QAAc;EACnB,KAAK,OAAO,MAAM;CACpB;;;;;;;;;CAUA,AAAQ,YAAY,WAA2B,OAAoB;EACjE,QAAQ,MAAM,KAAK,QAAnB;GACE,KAAK;IACH,UAAU,aAAa;IACvB;GAGF,KAAK;GACL,KAAK;IACH,UAAU,UAAU;IACpB;GAGF,KAAK;IACH,UAAU,aAAa;IACvB;GAGF,SACE;EAEJ;CACF;;;;;;CAOA,AAAQ,gBAAgB,QAA0B;EAChD,OAAO,OAAO,MAAM,MAAM,UAAU,KAAK,MAAM,MAAM,SAAS,IAAI,KAAK,MAAM,KAAK,SAAS,CAAC;CAC9F;;;;;;CAOA,AAAQ,gBAAsB;EAC5B,IAAI,KAAK,YAAY,GACnB;EAGF,OAAO,KAAK,OAAO,OAAO,KAAK,UAAU;GACvC,MAAM,SAAS,KAAK,OAAO,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;GAEzC,IAAI,WAAW,QACb;GAGF,KAAK,OAAO,OAAO,MAAM;EAC3B;CACF;AACF;;;;;;;;;;;;;AAcA,SAAgB,yBACd,SACuC;CACvC,OAAO,IAAI,mBAAmB,OAAO;AACvC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sum-usage.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/sum-usage.ts"],"sourcesContent":["import { accumulateCost, type Usage } from \"@warlock.js/ai\";\n\n/**\n * Fold a child {@link Usage} into a running accumulator. Token channels\n * (`input` / `output` / `total`) always sum; the optional cache /\n * reasoning channels (`cachedTokens` / `cacheWriteTokens` /\n * `reasoningTokens`) sum only when at least one side reported them, so\n * a provider that never meters a channel doesn't fabricate a `0` for\n * it. The `cost` breakdown is merged with the core framework's\n * {@link accumulateCost}, keeping cost-rollup semantics identical to a\n * native report tree — an unpriced contributor never erases a priced\n * one.\n *\n * Pure: returns a fresh `Usage`, never mutates either argument. Seed an\n * aggregation with {@link emptyUsage}.\n *\n * @example\n * let total = emptyUsage();\n * for (const trace of traces) {\n * total = sumUsage(total, trace.usage);\n * }\n */\nexport function sumUsage(accumulator: Usage, next: Usage): Usage {\n const merged: Usage = {\n input: accumulator.input + next.input,\n output: accumulator.output + next.output,\n total: accumulator.total + next.total,\n };\n\n const cachedTokens = sumOptional(accumulator.cachedTokens, next.cachedTokens);\n if (cachedTokens !== undefined) {\n merged.cachedTokens = cachedTokens;\n }\n\n const cacheWriteTokens = sumOptional(accumulator.cacheWriteTokens, next.cacheWriteTokens);\n if (cacheWriteTokens !== undefined) {\n merged.cacheWriteTokens = cacheWriteTokens;\n }\n\n const reasoningTokens = sumOptional(accumulator.reasoningTokens, next.reasoningTokens);\n if (reasoningTokens !== undefined) {\n merged.reasoningTokens = reasoningTokens;\n }\n\n const cost = accumulateCost(accumulator.cost, next.cost);\n if (cost !== undefined) {\n merged.cost = cost;\n }\n\n return merged;\n}\n\n/**\n * A zero-valued {@link Usage} to seed an aggregation. Only the required\n * token channels are set; optional channels stay absent until a\n * contributor reports them, preserving the \"never reported\" vs\n * \"reported as 0\" distinction.\n */\nexport function emptyUsage(): Usage {\n return {\n input: 0,\n output: 0,\n total: 0,\n };\n}\n\n/**\n * Add two optional token counts, treating either side's `undefined` as\n * zero — but return `undefined` when both are absent, so an unreported\n * channel stays unreported rather than collapsing to `0`.\n */\nfunction sumOptional(accumulator: number | undefined
|
|
1
|
+
{"version":3,"file":"sum-usage.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/sum-usage.ts"],"sourcesContent":["import { accumulateCost, type Usage } from \"@warlock.js/ai\";\n\n/**\n * Fold a child {@link Usage} into a running accumulator. Token channels\n * (`input` / `output` / `total`) always sum; the optional cache /\n * reasoning channels (`cachedTokens` / `cacheWriteTokens` /\n * `reasoningTokens`) sum only when at least one side reported them, so\n * a provider that never meters a channel doesn't fabricate a `0` for\n * it. The `cost` breakdown is merged with the core framework's\n * {@link accumulateCost}, keeping cost-rollup semantics identical to a\n * native report tree — an unpriced contributor never erases a priced\n * one.\n *\n * Pure: returns a fresh `Usage`, never mutates either argument. Seed an\n * aggregation with {@link emptyUsage}.\n *\n * @example\n * let total = emptyUsage();\n * for (const trace of traces) {\n * total = sumUsage(total, trace.usage);\n * }\n */\nexport function sumUsage(accumulator: Usage, next: Usage): Usage {\n const merged: Usage = {\n input: accumulator.input + next.input,\n output: accumulator.output + next.output,\n total: accumulator.total + next.total,\n };\n\n const cachedTokens = sumOptional(accumulator.cachedTokens, next.cachedTokens);\n if (cachedTokens !== undefined) {\n merged.cachedTokens = cachedTokens;\n }\n\n const cacheWriteTokens = sumOptional(accumulator.cacheWriteTokens, next.cacheWriteTokens);\n if (cacheWriteTokens !== undefined) {\n merged.cacheWriteTokens = cacheWriteTokens;\n }\n\n const reasoningTokens = sumOptional(accumulator.reasoningTokens, next.reasoningTokens);\n if (reasoningTokens !== undefined) {\n merged.reasoningTokens = reasoningTokens;\n }\n\n const cost = accumulateCost(accumulator.cost, next.cost);\n if (cost !== undefined) {\n merged.cost = cost;\n }\n\n return merged;\n}\n\n/**\n * A zero-valued {@link Usage} to seed an aggregation. Only the required\n * token channels are set; optional channels stay absent until a\n * contributor reports them, preserving the \"never reported\" vs\n * \"reported as 0\" distinction.\n */\nexport function emptyUsage(): Usage {\n return {\n input: 0,\n output: 0,\n total: 0,\n };\n}\n\n/**\n * Add two optional token counts, treating either side's `undefined` as\n * zero — but return `undefined` when both are absent, so an unreported\n * channel stays unreported rather than collapsing to `0`.\n */\nfunction sumOptional(\n accumulator: number | undefined,\n next: number | undefined,\n): number | undefined {\n if (accumulator === undefined && next === undefined) {\n return undefined;\n }\n\n return (accumulator ?? 0) + (next ?? 0);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,SAAS,aAAoB,MAAoB;CAC/D,MAAM,SAAgB;EACpB,OAAO,YAAY,QAAQ,KAAK;EAChC,QAAQ,YAAY,SAAS,KAAK;EAClC,OAAO,YAAY,QAAQ,KAAK;CAClC;CAEA,MAAM,eAAe,YAAY,YAAY,cAAc,KAAK,YAAY;CAC5E,IAAI,iBAAiB,QACnB,OAAO,eAAe;CAGxB,MAAM,mBAAmB,YAAY,YAAY,kBAAkB,KAAK,gBAAgB;CACxF,IAAI,qBAAqB,QACvB,OAAO,mBAAmB;CAG5B,MAAM,kBAAkB,YAAY,YAAY,iBAAiB,KAAK,eAAe;CACrF,IAAI,oBAAoB,QACtB,OAAO,kBAAkB;CAG3B,MAAM,OAAO,eAAe,YAAY,MAAM,KAAK,IAAI;CACvD,IAAI,SAAS,QACX,OAAO,OAAO;CAGhB,OAAO;AACT;;;;;;;AAQA,SAAgB,aAAoB;CAClC,OAAO;EACL,OAAO;EACP,QAAQ;EACR,OAAO;CACT;AACF;;;;;;AAOA,SAAS,YACP,aACA,MACoB;CACpB,IAAI,gBAAgB,UAAa,SAAS,QACxC;CAGF,QAAQ,eAAe,MAAM,QAAQ;AACvC"}
|
|
@@ -19,15 +19,11 @@ import { ModelPricing, Usage } from "@warlock.js/ai";
|
|
|
19
19
|
* @example
|
|
20
20
|
* const stats = store.aggregate({ sessionId: "session-42" });
|
|
21
21
|
* console.log(`${stats.traces} runs, ${stats.usage.total} tokens`);
|
|
22
|
-
* const totalUsd =
|
|
23
|
-
* (stats.cost?.input ?? 0) +
|
|
24
|
-
* (stats.cost?.output ?? 0) +
|
|
25
|
-
* (stats.cost?.cachedInput ?? 0) +
|
|
26
|
-
* (stats.cost?.cachedOutput ?? 0);
|
|
22
|
+
* const totalUsd = totalCost(stats.cost) ?? 0; // `totalCost` from @warlock.js/ai
|
|
27
23
|
*/
|
|
28
24
|
type TraceAggregate = {
|
|
29
25
|
/** Number of traces matched by the query. */traces: number; /** Count of matched traces whose root status was `"completed"`. */
|
|
30
|
-
completed: number; /** Count of matched traces whose root status was `"failed"`. */
|
|
26
|
+
completed: number; /** Count of matched traces whose root status was `"failed"` or `"incomplete"`. */
|
|
31
27
|
failed: number; /** Count of matched traces whose root status was `"cancelled"`. */
|
|
32
28
|
cancelled: number;
|
|
33
29
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace-aggregate.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/trace-aggregate.type.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"trace-aggregate.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/trace-aggregate.type.ts"],"mappings":";;;;;AAsBA;;;;;;;;;;;;;;AAwBe;;;;KAxBH,cAAA;+CAEV,MAAA;EAEA,SAAA;EAEA,MAAA;EAEA,SAAA;;;;;;;EAOA,KAAA,EAAO,KAAA;;;;;;;EAOP,IAAA,GAAO,YAAY;EAEnB,aAAA;AAAA"}
|