@warlock.js/ai-panoptic 5.0.2 → 5.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/cjs/index.cjs.map +1 -1
- package/esm/collector/collector.mjs.map +1 -1
- 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.map +1 -1
- package/esm/dashboard/serve.mjs.map +1 -1
- package/esm/dashboard/trace-filter.mjs.map +1 -1
- package/esm/dashboard/ui.html.mjs.map +1 -1
- package/esm/exporters/console/format-span-io.mjs.map +1 -1
- package/esm/exporters/file/file-exporter.mjs.map +1 -1
- package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
- package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
- package/esm/panoptic/panoptic-middleware.mjs.map +1 -1
- package/esm/panoptic/panoptic.mjs.map +1 -1
- package/esm/register.mjs.map +1 -1
- package/esm/store/cache-trace-store.mjs.map +1 -1
- package/esm/store/in-memory-trace-store.mjs.map +1 -1
- package/esm/store/sum-usage.d.mts.map +1 -1
- package/package.json +4 -4
package/cjs/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["EXPORTER_NAME","EXPORTER_NAME","EXPORTER_NAME","isModuleExists","loadingPromise","log"],"sources":["../../../../../../ai-panoptic/src/exporters/utils/walk-spans.ts","../../../../../../ai-panoptic/src/collector/normalize-error.ts","../../../../../../ai-panoptic/src/collector/extract-span-attributes.ts","../../../../../../ai-panoptic/src/collector/report-to-span.ts","../../../../../../ai-panoptic/src/collector/report-to-trace.ts","../../../../../../ai-panoptic/src/collector/collector.ts","../../../../../../ai-panoptic/src/store/match-trace.ts","../../../../../../ai-panoptic/src/store/sum-usage.ts","../../../../../../ai-panoptic/src/store/cache-trace-store.ts","../../../../../../ai-panoptic/src/store/in-memory-trace-store.ts","../../../../../../ai-panoptic/src/exporters/utils/total-cost.ts","../../../../../../ai-panoptic/src/exporters/utils/gen-ai-attributes.ts","../../../../../../ai-panoptic/src/exporters/console/format-span-io.ts","../../../../../../ai-panoptic/src/exporters/console/format-span-line.ts","../../../../../../ai-panoptic/src/exporters/console/console-exporter.ts","../../../../../../ai-panoptic/src/exporters/file/file-exporter.ts","../../../../../../ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts","../../../../../../ai-panoptic/src/exporters/otel/otel-exporter.ts","../../../../../../ai-panoptic/src/panoptic/panoptic-middleware.ts","../../../../../../ai-panoptic/src/panoptic/panoptic.ts","../../../../../../ai-panoptic/src/evaluate/evaluate-system-prompt.ts","../../../../../../ai-panoptic/src/evaluate/extract-last-system-prompt.ts","../../../../../../ai-panoptic/src/evaluate/find-span-by-id.ts","../../../../../../ai-panoptic/src/dashboard/parse-query.ts","../../../../../../ai-panoptic/src/dashboard/warlock-logo.ts","../../../../../../ai-panoptic/src/dashboard/ui.html.ts","../../../../../../ai-panoptic/src/dashboard/serve.ts","../../../../../../ai-panoptic/src/dashboard/dashboard.ts","../../../../../../ai-panoptic/src/dashboard/trace-filter.ts","../../../../../../ai-panoptic/src/config/apply-panoptic-config.ts","../../../../../../ai-panoptic/src/register.ts"],"sourcesContent":["import type { TraceSpan } from \"../../contracts\";\n\n/**\n * Depth-first pre-order traversal of a {@link TraceSpan} tree, yielding\n * the root first and then each descendant in `children` (invocation)\n * order. Exporters that emit a flat span stream — OpenTelemetry, the\n * console table — walk the tree once with this instead of re-writing the\n * recursion in every exporter.\n *\n * @example\n * for (const span of walkSpans(trace.root)) {\n * emit(span);\n * }\n */\nexport function* walkSpans(root: TraceSpan): Generator<TraceSpan> {\n yield root;\n\n for (const child of root.children) {\n yield* walkSpans(child);\n }\n}\n","import { redact, scrubSecrets } from \"@warlock.js/ai\";\nimport type { TraceSpanError } from \"../contracts/trace.type\";\n\n/**\n * Project a captured execution error onto the structural\n * {@link TraceSpanError} shape used on a failed / cancelled span.\n *\n * Source errors are typically `AIError` instances (every error surfaced\n * by `@warlock.js/ai` is one), but the collector never depends on the\n * concrete class — it reads only the structural surface (`name` / `code`\n * / `message` / `stack`) so a plain `Error`, an `AIError`, or any\n * thrown value all normalize identically. The result is a JSON-safe\n * plain object so it survives serialization to a backend collector\n * unchanged.\n *\n * The error `type` prefers the stable `code` (e.g. `\"RATE_LIMIT\"`) over\n * the class `name`, falling back to `name` and finally to the generic\n * `\"Error\"` so the field is always populated.\n *\n * @example\n * const spanError = normalizeError(report.error);\n * // { type: \"RATE_LIMIT\", message: \"429 Too Many Requests\", stack: \"...\" }\n */\nexport function normalizeError(error: unknown): TraceSpanError | undefined {\n if (error === undefined || error === null) {\n return undefined;\n }\n\n if (typeof error !== \"object\") {\n return {\n type: \"Error\",\n message: scrubSecrets(String(error)),\n };\n }\n\n const candidate = error as {\n code?: unknown;\n name?: unknown;\n message?: unknown;\n stack?: unknown;\n cause?: unknown;\n };\n\n const type = pickString(candidate.code) ?? pickString(candidate.name) ?? \"Error\";\n const message = pickString(candidate.message) ?? \"\";\n const stack = pickString(candidate.stack);\n\n // Scrub free-text secrets (Bearer tokens, api keys) from the message and\n // stack, and deep-redact the cause (a raw provider SDK error can carry\n // auth/cookie headers) before either is stored or exported (S4).\n const normalized: TraceSpanError = {\n type,\n message: scrubSecrets(message),\n };\n\n if (stack !== undefined) {\n normalized.stack = scrubSecrets(stack);\n }\n\n if (candidate.cause !== undefined && candidate.cause !== null) {\n normalized.cause =\n typeof candidate.cause === \"object\"\n ? redact(candidate.cause)\n : scrubSecrets(String(candidate.cause));\n }\n\n return normalized;\n}\n\n/**\n * Return the value when it is a non-empty string, otherwise `undefined`.\n * Keeps `normalizeError` from promoting empty / non-string fields.\n */\nfunction pickString(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n\n return undefined;\n}\n","import type { BaseReport } from \"@warlock.js/ai\";\n\n/**\n * Per-primitive `BaseReport` extension fields the collector surfaces as\n * span attributes. Declared as a structural superset (every field\n * optional) so a single reader can pull whatever the concrete report\n * carried without narrowing on `type` first — a `BaseReport` widened to\n * this shape exposes `undefined` for the fields its primitive doesn't\n * set, and the extractor simply skips those.\n *\n * Mirrors the public per-primitive report types in `@warlock.js/ai`\n * (`AgentReport`, `WorkflowReport`, `SupervisorReport`,\n * `OrchestratorReport`, `ToolCall`) — kept structural rather than a\n * union import so adding a new optional report field upstream is a\n * non-breaking read here.\n */\ntype ReportExtensions = {\n model?: { name?: string; provider?: string };\n trips?: unknown[];\n promptName?: string;\n promptVersion?: string;\n workflowName?: string;\n supervisorName?: string;\n signature?: string;\n terminatedBy?: string;\n iterations?: number;\n steps?: Record<string, unknown>;\n turns?: unknown[];\n turnIndex?: number;\n tripIndex?: number;\n recoveredFrom?: string;\n};\n\n/**\n * Build the free-form `TraceSpan.attributes` bag for one report node.\n *\n * The collector keeps the first-class span fields (identity, timing,\n * status, usage, error) on the span itself and routes everything\n * primitive-specific here — trip/step/iteration counts, the model\n * identity an agent ran against, the tool's originating trip index, a\n * supervisor's termination reason. Exporters forward this verbatim as\n * backend span attributes (OTel attributes, Langfuse metadata).\n *\n * Only populated keys are emitted; the function returns `undefined`\n * when the node carried no extra detail, so the optional\n * `TraceSpan.attributes` field stays absent rather than holding an\n * empty object (matches the contract's \"absent when empty\" note).\n *\n * Retry count is surfaced from the shared `BaseReport.attempts` for\n * every primitive so cost dashboards see the real call count.\n *\n * @example\n * const attributes = extractSpanAttributes(agentReport);\n * // { \"agent.trips\": 3, \"agent.model.name\": \"gpt-4o\", \"agent.model.provider\": \"openai\" }\n */\nexport function extractSpanAttributes(report: BaseReport): Record<string, unknown> | undefined {\n const extensions = report as BaseReport & ReportExtensions;\n const attributes: Record<string, unknown> = {};\n\n if (report.attempts !== undefined && report.attempts.length > 0) {\n attributes[\"retries\"] = report.attempts.length;\n }\n\n switch (report.type) {\n case \"agent\": {\n addAgentAttributes(attributes, extensions);\n break;\n }\n\n case \"workflow\": {\n addWorkflowAttributes(attributes, extensions);\n break;\n }\n\n case \"supervisor\":\n case \"team\": {\n // ai.team reuses the supervisor engine, so a team report carries the\n // same supervisorName / terminatedBy / iterations fields.\n addSupervisorAttributes(attributes, extensions);\n break;\n }\n\n case \"orchestrator\": {\n addOrchestratorAttributes(attributes, extensions);\n break;\n }\n\n case \"tool\": {\n addToolAttributes(attributes, extensions);\n break;\n }\n\n default: {\n break;\n }\n }\n\n if (Object.keys(attributes).length === 0) {\n return undefined;\n }\n\n return attributes;\n}\n\nfunction addAgentAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (Array.isArray(extensions.trips)) {\n attributes[\"agent.trips\"] = extensions.trips.length;\n }\n\n if (extensions.model?.name !== undefined) {\n attributes[\"agent.model.name\"] = extensions.model.name;\n }\n\n if (extensions.model?.provider !== undefined) {\n attributes[\"agent.model.provider\"] = extensions.model.provider;\n }\n\n // Prompt-version linkage (core `AgentReport.promptName` / `promptVersion`).\n // Present only when the agent ran against a *named* `ai.prompts` builder;\n // surfaced so the dashboard can group / filter runs by `name@version` and\n // attribute behavior shifts to a specific prompt revision.\n if (extensions.promptName !== undefined) {\n attributes[\"agent.promptName\"] = extensions.promptName;\n }\n\n if (extensions.promptVersion !== undefined) {\n attributes[\"agent.promptVersion\"] = extensions.promptVersion;\n }\n}\n\nfunction addWorkflowAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (extensions.workflowName !== undefined) {\n attributes[\"workflow.name\"] = extensions.workflowName;\n }\n\n if (extensions.signature !== undefined) {\n attributes[\"workflow.signature\"] = extensions.signature;\n }\n\n if (extensions.steps !== undefined) {\n attributes[\"workflow.steps\"] = Object.keys(extensions.steps).length;\n }\n}\n\nfunction addSupervisorAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (extensions.supervisorName !== undefined) {\n attributes[\"supervisor.name\"] = extensions.supervisorName;\n }\n\n if (extensions.terminatedBy !== undefined) {\n attributes[\"supervisor.terminatedBy\"] = extensions.terminatedBy;\n }\n\n if (extensions.iterations !== undefined) {\n attributes[\"supervisor.iterations\"] = extensions.iterations;\n }\n}\n\nfunction addOrchestratorAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (extensions.turnIndex !== undefined) {\n attributes[\"orchestrator.turnIndex\"] = extensions.turnIndex;\n }\n\n if (extensions.signature !== undefined) {\n attributes[\"orchestrator.signature\"] = extensions.signature;\n }\n\n if (Array.isArray(extensions.turns)) {\n attributes[\"orchestrator.turns\"] = extensions.turns.length;\n }\n}\n\nfunction addToolAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (extensions.tripIndex !== undefined) {\n attributes[\"tool.tripIndex\"] = extensions.tripIndex;\n }\n\n if (extensions.recoveredFrom !== undefined) {\n attributes[\"tool.recoveredFrom\"] = extensions.recoveredFrom;\n }\n}\n","import type { BaseReport } from \"@warlock.js/ai\";\nimport type { TraceSpan } from \"../contracts/trace.type\";\nimport type { ContentCaptureOptions, ContentRedactor } from \"./content-capture.type\";\nimport { extractSpanAttributes } from \"./extract-span-attributes\";\nimport { normalizeError } from \"./normalize-error\";\n\n/**\n * Project one {@link BaseReport} node — and its entire subtree — into a\n * {@link TraceSpan}. Pure and recursive: identity, timing, status, and\n * the rolled-up `usage` map across 1:1 from the report; lineage maps\n * `runId → spanId`, `parentRunId → parentSpanId`, `rootRunId → traceId`;\n * children recurse in invocation order so the span tree mirrors the\n * report tree exactly.\n *\n * The error is normalized to the JSON-safe {@link\n * import(\"../contracts/trace.type\").TraceSpanError} shape only when the\n * node carried one (failed / cancelled). Primitive-specific detail that\n * has no first-class span field (trip / step / iteration counts, model\n * identity, tool trip index) is routed into the optional `attributes`\n * bag via {@link extractSpanAttributes}.\n *\n * When {@link ContentCaptureOptions.captureContent} is set, the raw\n * prompt/response (agents) and args/result (tools) are additionally\n * copied onto `span.input` / `span.output` — off by default because\n * payloads are large and often sensitive. A {@link ContentRedactor} can\n * mask each value first.\n *\n * No external lookup is needed — a `BaseReport` already carries\n * everything a span requires, so a collector can flatten a tree without\n * consulting any other source.\n *\n * @example\n * const root = reportToSpan(result.report);\n * console.log(root.spanId, root.traceId, root.children.length);\n */\nexport function reportToSpan(report: BaseReport, options?: ContentCaptureOptions): TraceSpan {\n const span: TraceSpan = {\n spanId: report.runId,\n traceId: report.rootRunId,\n name: report.name,\n type: report.type,\n status: report.status,\n startedAt: report.startedAt,\n endedAt: report.endedAt,\n duration: report.duration,\n usage: report.usage,\n children: report.children.map((child) => reportToSpan(child, options)),\n };\n\n if (report.parentRunId !== undefined) {\n span.parentSpanId = report.parentRunId;\n }\n\n if (report.sessionId !== undefined) {\n span.sessionId = report.sessionId;\n }\n\n if (report.version !== undefined) {\n span.version = report.version;\n }\n\n const error = normalizeError((report as { error?: unknown }).error);\n if (error !== undefined) {\n span.error = error;\n }\n\n const attributes = extractSpanAttributes(report);\n if (attributes !== undefined) {\n span.attributes = attributes;\n }\n\n if (options?.captureContent) {\n captureContent(span, report, options);\n }\n\n return span;\n}\n\n/** Per-primitive content fields read off a widened report node. */\ntype ContentReport = {\n input?: unknown;\n output?: unknown;\n systemPrompt?: string;\n trips?: Array<{ input?: unknown; output?: unknown }>;\n /**\n * The full assembled conversation an agent run captured when its\n * `captureMessages` option was set (core `AgentReport.messages`).\n * Present only on opted-in agent runs; absent otherwise.\n */\n messages?: unknown;\n};\n\n/**\n * Copy the node's raw content onto `span.input` / `span.output`.\n *\n * - Tools carry the call arguments + return value directly on the report\n * (`ToolCall.input` / `ToolCall.output`).\n * - Agents carry a `trips[]` history plus the resolved `systemPrompt`. The\n * input is emitted as a `[system, user]` chat array — so backends like\n * Langfuse render the full prompt as sent — or the bare user string when\n * there's no system prompt. The output is the last NON-EMPTY trip\n * `output` (the final response text); a failed / max-trips run can end on\n * a trip whose `output` is `\"\"` or tool-call-only, so we scan back for the\n * last one that carried text. Intermediate trips store a `\"[tool results]\"`\n * placeholder upstream.\n * - When {@link ContentCaptureOptions.fullHistory} is on AND the agent run\n * opted into `captureMessages` (so the report carries a `messages`\n * array), the *whole* `CapturedMessage[]` is emitted as `span.input`\n * instead of the `[system, user]` first-trip array — every role, every\n * trip. The output stays the last non-empty trip output. When `messages`\n * is absent the branch falls back to today's first-trip logic, so a run\n * that didn't opt in degrades gracefully.\n *\n * Each value is passed through the optional {@link ContentRedactor};\n * a redactor returning `undefined` drops the field. Under `fullHistory`\n * the redactor receives the full array as a single value.\n */\nfunction captureContent(\n span: TraceSpan,\n report: BaseReport,\n options: ContentCaptureOptions,\n): void {\n const node = report as BaseReport & ContentReport;\n const redact = options.redactContent;\n\n let input: unknown;\n let output: unknown;\n\n if (report.type === \"tool\") {\n input = node.input;\n output = node.output;\n } else if (\n options.fullHistory &&\n Array.isArray(node.messages) &&\n node.messages.length > 0\n ) {\n // Full-history capture: the entire assembled conversation as a single\n // input value. Output remains the agent's final response text.\n input = node.messages;\n output = Array.isArray(node.trips) ? lastNonEmptyOutput(node.trips) : undefined;\n } else if (Array.isArray(node.trips) && node.trips.length > 0) {\n const userInput = node.trips[0]?.input;\n // Emit a [system, user] chat array when the agent carried a system\n // prompt, so backends (Langfuse) render the full prompt as sent;\n // otherwise keep the bare user string.\n input =\n typeof node.systemPrompt === \"string\" && node.systemPrompt.length > 0\n ? [\n { role: \"system\", content: node.systemPrompt },\n { role: \"user\", content: userInput },\n ]\n : userInput;\n output = lastNonEmptyOutput(node.trips);\n }\n\n if (input !== undefined) {\n const value = redact ? redact(input, { name: span.name, type: span.type, field: \"input\" }) : input;\n if (value !== undefined) {\n span.input = value;\n }\n }\n\n if (output !== undefined) {\n const value = redact ? redact(output, { name: span.name, type: span.type, field: \"output\" }) : output;\n if (value !== undefined) {\n span.output = value;\n }\n }\n}\n\n/**\n * The last trip output that actually carries text — the agent's final\n * response on the happy path. A failed or max-trips run can end on a trip\n * whose `output` is `\"\"` or tool-call-only, so we scan backwards for the\n * last trip that produced text rather than blindly taking the final trip\n * (which would surface an empty string). Returns `undefined` when no trip\n * produced any output.\n */\nfunction lastNonEmptyOutput(trips: Array<{ output?: unknown }>): unknown {\n for (let i = trips.length - 1; i >= 0; i -= 1) {\n const out = trips[i]?.output;\n const hasText = typeof out === \"string\" ? out.length > 0 : out !== undefined;\n\n if (hasText) {\n return out;\n }\n }\n\n return undefined;\n}\n","import type { BaseReport } from \"@warlock.js/ai\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport type { ContentCaptureOptions } from \"./content-capture.type\";\nimport { normalizeError } from \"./normalize-error\";\nimport { reportToSpan } from \"./report-to-span\";\n\n/**\n * Project an outermost {@link BaseReport} (one whole `.execute()` /\n * `.invoke()` run) into a {@link Trace} — the root {@link\n * import(\"../contracts/trace.type\").TraceSpan} plus the trace-wide\n * rollups exporters need without re-walking the tree.\n *\n * The trace-level identity and rollups all read off the root span the\n * projection already built (`traceId`, `usage`, timing), so the trace\n * envelope never disagrees with its own root. `reportSchemaVersion` is\n * mirrored from the root report when present (it is stamped only on\n * root nodes upstream) so exporters can branch on the source shape.\n *\n * Pure — the same input always yields the same trace. The collector\n * exposes this as `toTrace` so callers can inspect the normalized shape\n * without dispatching to exporters.\n *\n * The optional `rootError` threads the failing run's envelope error\n * (`BaseResult.error`) onto the root span — a fallback for callers that\n * hold the result envelope (the `attach`/middleware path). Root primitives\n * now also stamp their terminal error onto the report itself\n * (`BaseReport.error`), so the observe path — which delivers only the\n * report, never the envelope — still surfaces a failed root's error.\n * `rootError` is applied only when the projected root span carries none of\n * its own; the subtree projection stays pure (each child surfaces its own\n * report-level error, if any).\n *\n * @example\n * const trace = reportToTrace(result.report, result.error);\n * console.log(trace.traceId, trace.usage.total, trace.duration);\n */\nexport function reportToTrace(\n report: BaseReport,\n rootError?: unknown,\n options?: ContentCaptureOptions,\n): Trace {\n const root = reportToSpan(report, options);\n\n if (root.error === undefined) {\n const error = normalizeError(rootError);\n\n if (error !== undefined) {\n root.error = error;\n }\n }\n\n const trace: Trace = {\n traceId: root.traceId,\n root,\n startedAt: root.startedAt,\n endedAt: root.endedAt,\n duration: root.duration,\n usage: root.usage,\n };\n\n if (root.sessionId !== undefined) {\n trace.sessionId = root.sessionId;\n }\n\n if (report.reportSchemaVersion !== undefined) {\n trace.reportSchemaVersion = report.reportSchemaVersion;\n }\n\n return trace;\n}\n","import type { BaseReport } from \"@warlock.js/ai\";\nimport type { CollectorContract } from \"../contracts/collector.contract\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { walkSpans } from \"../exporters/utils/walk-spans\";\nimport type { ContentCaptureOptions } from \"./content-capture.type\";\nimport { reportToTrace } from \"./report-to-trace\";\n\n/**\n * Notified when a registered exporter throws during `collect` / `flush` /\n * `shutdown`. The failure stays isolated (the originating run never sees\n * it) — this is purely a chance to surface it (route to your logger,\n * bump a metric). Receives the exporter's `name` and the thrown error.\n */\nexport type ExporterErrorHandler = (exporterName: string, error: unknown) => void;\n\n/** Options for {@link createCollector}. */\nexport type CollectorOptions = ContentCaptureOptions & {\n /**\n * Called when an exporter throws (and is isolated). Overrides the default\n * one-time `console.warn` per exporter — route the failure to your own\n * logger / metrics instead. Errors thrown by the handler itself are\n * swallowed so observability never crashes the run.\n */\n onError?: ExporterErrorHandler;\n};\n\n/**\n * Drive the source end of the Panoptic pipeline: ingest core\n * `@warlock.js/ai` {@link BaseReport} trees, project them into {@link\n * Trace}s, and fan each trace out to every registered exporter.\n *\n * Owns exporter registration (deduped by `ExporterContract.name`), the\n * report→trace projection, and graceful shutdown so exporters drain\n * before exit. Instantiated fresh per collector via {@link\n * createCollector}; callers never see `new`.\n *\n * **Failure isolation.** An exporter that throws never propagates back\n * into the originating run — `collect`, `flush`, and `shutdown` settle\n * every exporter independently (mirrors how the core event hooks\n * swallow consumer errors). One broken exporter can neither crash the\n * agent loop nor stop sibling exporters from receiving the trace. The\n * failure is still **surfaced** — via the `onError` option, or a one-time\n * `console.warn` per exporter — so a misconfiguration (e.g. a missing\n * optional peer like `langfuse`) doesn't fail silently.\n */\nclass Collector implements CollectorContract {\n /**\n * Registered exporters in insertion order. A `Map` keyed by\n * `ExporterContract.name` gives O(1) dedupe on `use` while preserving\n * registration order for deterministic fan-out.\n */\n private readonly exporters = new Map<string, ExporterContract>();\n\n /**\n * Exporters already warned about on the default error path — so a\n * persistent misconfiguration surfaces once, not on every trace.\n */\n private readonly warnedExporters = new Set<string>();\n\n /**\n * Collector options: content capture threaded into every `toTrace`\n * projection (`captureContent` populates span `input` / `output`), plus\n * an optional `onError` for isolated exporter failures.\n */\n public constructor(private readonly options: CollectorOptions = {}) {}\n\n public use(exporter: ExporterContract): this {\n if (!this.exporters.has(exporter.name)) {\n this.exporters.set(exporter.name, exporter);\n }\n\n return this;\n }\n\n public toTrace(report: BaseReport, rootError?: unknown): Trace {\n return reportToTrace(report, rootError, this.options);\n }\n\n public async collect(report: BaseReport, rootError?: unknown): Promise<void> {\n const trace = this.toTrace(report, rootError);\n\n await this.dispatch(trace);\n }\n\n public async flush(): Promise<void> {\n await this.settleAll((exporter) => exporter.flush?.());\n }\n\n public async shutdown(): Promise<void> {\n await this.flush();\n\n await this.settleAll((exporter) => exporter.shutdown?.());\n\n this.exporters.clear();\n }\n\n /**\n * Fan one trace out to every exporter and, when an exporter advertises\n * the per-span hook, deliver every span in the finalized tree to it as\n * well. `exportSpan` is a post-completion per-span hook (not a live /\n * streaming feed — the trace is already finalized): we walk the tree in\n * pre-order with {@link walkSpans} so the exporter sees the root and\n * every descendant exactly once. Every invocation is isolated so a\n * throwing exporter can't abort the dispatch to its siblings or escape\n * into the originating run.\n */\n private async dispatch(trace: Trace): Promise<void> {\n await this.settleAll(async (exporter) => {\n await exporter.export(trace);\n\n if (exporter.exportSpan !== undefined) {\n for (const span of walkSpans(trace.root)) {\n await exporter.exportSpan(span);\n }\n }\n });\n }\n\n /**\n * Run `task` against every registered exporter and wait for all of\n * them to settle, swallowing individual rejections. `Promise.allSettled`\n * guarantees one failure neither rejects the batch nor blocks the\n * others — the contract's failure-isolation requirement.\n */\n private async settleAll(\n task: (exporter: ExporterContract) => void | Promise<void>,\n ): Promise<void> {\n const runs = [...this.exporters.entries()].map(([name, exporter]) =>\n Promise.resolve()\n .then(() => task(exporter))\n .catch((error: unknown) => this.reportExporterError(name, error)),\n );\n\n await Promise.allSettled(runs);\n }\n\n /**\n * Surface an isolated exporter failure. The originating run never sees it\n * (the isolation guarantee holds), but a silent failure is the wrong\n * default for a config error — e.g. a missing optional peer like\n * `langfuse` would otherwise drop every trace with no signal. The\n * supplied `onError` is called, or — by default — a `console.warn` is\n * emitted ONCE per exporter so the cause is visible without spamming.\n */\n private reportExporterError(name: string, error: unknown): void {\n if (this.options.onError) {\n try {\n this.options.onError(name, error);\n } catch {\n // Never let the error handler itself escape into the run.\n }\n return;\n }\n\n if (this.warnedExporters.has(name)) {\n return;\n }\n\n this.warnedExporters.add(name);\n const message = error instanceof Error ? error.message : String(error);\n console.warn(`[panoptic] exporter \"${name}\" failed and was isolated: ${message}`);\n }\n}\n\n/**\n * Create a Panoptic collector — the single integration point an app\n * wires into its agents/workflows (typically via the `onComplete`\n * report hook). Register exporters with `use`, then feed finalized root\n * reports to `collect`.\n *\n * @example\n * const collector = createCollector().use(otelExporter).use(langfuseExporter);\n * agent.on(\"onComplete\", ({ result }) => collector.collect(result.report));\n * // on shutdown:\n * await collector.shutdown();\n */\nexport function createCollector(options: CollectorOptions = {}): CollectorContract {\n return new Collector(options);\n}\n","import type { ReportStatus } from \"@warlock.js/ai\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport type { TraceQuery } from \"./trace-query.type\";\n\n/**\n * Test whether one trace satisfies a {@link TraceQuery}. Every declared\n * filter field must match (logical AND); absent fields are ignored, so\n * an empty / undefined filter matches every trace.\n *\n * Time bounds compare against the trace's root `startedAt`, parsed to\n * an epoch once per call, inclusive on both ends. Status accepts a\n * single value or an array (membership test). Identity fields are exact\n * string equality.\n *\n * Pure — used by the store's `query` and `aggregate` so both share one\n * matching definition.\n */\nexport function matchTrace(trace: Trace, filter?: TraceQuery): boolean {\n if (!filter) {\n return true;\n }\n\n if (filter.traceId !== undefined && trace.traceId !== filter.traceId) {\n return false;\n }\n\n if (filter.sessionId !== undefined && trace.sessionId !== filter.sessionId) {\n return false;\n }\n\n if (filter.status !== undefined && !statusMatches(trace.root.status, filter.status)) {\n return false;\n }\n\n const startedAt = Date.parse(trace.startedAt);\n\n if (filter.startedAfter !== undefined && startedAt < toEpoch(filter.startedAfter)) {\n return false;\n }\n\n if (filter.startedBefore !== undefined && startedAt > toEpoch(filter.startedBefore)) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Membership test for the status filter — true when `status` equals the\n * single wanted value, or is one of the wanted array.\n */\nfunction statusMatches(status: ReportStatus, wanted: ReportStatus | ReportStatus[]): boolean {\n if (Array.isArray(wanted)) {\n return wanted.includes(status);\n }\n\n return status === wanted;\n}\n\n/**\n * Normalize a time bound (ISO string or `Date`) to epoch milliseconds\n * for comparison against a parsed `startedAt`.\n */\nfunction toEpoch(bound: string | Date): number {\n if (bound instanceof Date) {\n return bound.getTime();\n }\n\n return Date.parse(bound);\n}\n","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, next: number | undefined): number | undefined {\n if (accumulator === undefined && next === undefined) {\n return undefined;\n }\n\n return (accumulator ?? 0) + (next ?? 0);\n}\n","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","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.\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 * 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(options?: InMemoryTraceStoreOptions): TraceStoreContract & ExporterContract {\n return new InMemoryTraceStore(options);\n}\n","import 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 field. Mirrors the formula documented on\n * `Usage.cost` (input + output + cachedInput + cachedOutput), and also\n * folds in `reasoning` for forward-safety when a provider prices\n * reasoning tokens as a separate channel. 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 const cost = usage.cost;\n\n if (!cost) {\n return undefined;\n }\n\n return (\n (cost.input ?? 0) +\n (cost.output ?? 0) +\n (cost.cachedInput ?? 0) +\n (cost.cachedOutput ?? 0) +\n (cost.reasoning ?? 0)\n );\n}\n","import type { TraceSpan } from \"../../contracts\";\nimport { totalCostUsd } from \"./total-cost\";\n\n/**\n * Subset of the OpenTelemetry GenAI semantic-convention attribute keys\n * Panoptic emits. Kept as a named constant map (not inline string\n * literals scattered through the mapper) so the convention names live in\n * one place and a convention bump is a single edit.\n *\n * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/\n */\nexport const GEN_AI_ATTRIBUTES = {\n operationName: \"gen_ai.operation.name\",\n system: \"gen_ai.system\",\n requestModel: \"gen_ai.request.model\",\n responseModel: \"gen_ai.response.model\",\n usageInputTokens: \"gen_ai.usage.input_tokens\",\n usageOutputTokens: \"gen_ai.usage.output_tokens\",\n conversationId: \"gen_ai.conversation.id\",\n /** Captured prompt/input (set only under content capture). */\n prompt: \"gen_ai.prompt\",\n /** Captured completion/output (set only under content capture). */\n completion: \"gen_ai.completion\",\n} as const;\n\n/**\n * Panoptic-specific attribute keys that have no GenAI-convention\n * equivalent. Namespaced under `warlock.*` so they never collide with a\n * future `gen_ai.*` key the spec might add.\n */\nexport const WARLOCK_ATTRIBUTES = {\n reportType: \"warlock.report.type\",\n version: \"warlock.version\",\n durationMs: \"warlock.duration_ms\",\n totalTokens: \"gen_ai.usage.total_tokens\",\n cachedTokens: \"gen_ai.usage.cached_tokens\",\n reasoningTokens: \"gen_ai.usage.reasoning_tokens\",\n costUsd: \"warlock.cost.usd\",\n} as const;\n\n/**\n * Span attribute values an OpenTelemetry / Langfuse backend accepts.\n * GenAI attributes are scalars; the framework's free-form\n * `TraceSpan.attributes` may also carry these.\n */\nexport type AttributeValue = string | number | boolean;\n\n/**\n * Project a {@link TraceSpan} onto the OpenTelemetry GenAI\n * semantic-convention attribute set.\n *\n * The vendor-neutral {@link TraceSpan} carries identity, timing, status,\n * and rolled-up `usage` as first-class fields; model identity and other\n * provider detail live in the free-form `attributes` bag the collector\n * populated. This mapper folds both into a flat `gen_ai.*` /\n * `warlock.*` attribute map ready to set on an OTel span or hand to a\n * Langfuse generation.\n *\n * - `gen_ai.operation.name` / `gen_ai.system` / `gen_ai.request.model`\n * are forwarded from the span's `attributes` when the collector set\n * them; never invented here.\n * - Token counts come from the span's typed `usage` rollup.\n * - The free-form `attributes` are merged last so an explicit collector\n * value wins over a derived one.\n *\n * @example\n * const attributes = toGenAiAttributes(span);\n * // { \"gen_ai.usage.input_tokens\": 150, \"gen_ai.usage.output_tokens\": 320, ... }\n */\nexport function toGenAiAttributes(span: TraceSpan): Record<string, AttributeValue> {\n const attributes: Record<string, AttributeValue> = {\n [WARLOCK_ATTRIBUTES.reportType]: span.type,\n [WARLOCK_ATTRIBUTES.durationMs]: span.duration,\n [WARLOCK_ATTRIBUTES.totalTokens]: span.usage.total,\n [GEN_AI_ATTRIBUTES.usageInputTokens]: span.usage.input,\n [GEN_AI_ATTRIBUTES.usageOutputTokens]: span.usage.output,\n };\n\n if (span.version !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.version] = span.version;\n }\n\n if (span.sessionId !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.conversationId] = span.sessionId;\n }\n\n if (span.usage.cachedTokens !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.cachedTokens] = span.usage.cachedTokens;\n }\n\n if (span.usage.reasoningTokens !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.reasoningTokens] = span.usage.reasoningTokens;\n }\n\n const cost = totalCostUsd(span.usage);\n\n if (cost !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.costUsd] = cost;\n }\n\n mergeScalarAttributes(attributes, span.attributes);\n\n return attributes;\n}\n\n/**\n * Copy the scalar entries of a free-form attribute bag onto the target\n * map. Non-scalar values (objects, arrays, functions) are skipped — OTel\n * and Langfuse attribute values must be primitives, and the collector's\n * bag may legitimately hold nested digests that don't belong on a span\n * attribute. Explicit collector values overwrite derived ones.\n */\nfunction mergeScalarAttributes(\n target: Record<string, AttributeValue>,\n source: Record<string, unknown> | undefined,\n): void {\n if (!source) {\n return;\n }\n\n for (const [key, value] of Object.entries(source)) {\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n target[key] = value;\n }\n }\n}\n","import type { TraceSpan } from \"../../contracts\";\n\n/** Default cap on how many characters of captured content a console line shows. */\nconst DEFAULT_IO_MAX_CHARS = 500;\n\n/**\n * Render a span's captured content ({@link TraceSpan.input} /\n * {@link TraceSpan.output}) as extra indented console lines, one level\n * below the span's own line:\n *\n * ```text\n * ok agent \"market-research\" — 1794ms, 224 tok, $0.0008\n * in: Research the market for Acme Coffee Roasters …\n * out: Demand is steady; specialty buyers skew premium …\n * ```\n *\n * Returns `[]` when the span carries no content — capture disabled, or a\n * composite node with no own I/O. Each value is stringified (JSON for\n * non-strings), whitespace-collapsed to stay scannable, and truncated to\n * `maxChars` (default {@link DEFAULT_IO_MAX_CHARS}) with an ellipsis. Use\n * the file exporter for the full, untruncated payload.\n *\n * @example\n * formatSpanIO(toolSpan, 1);\n * // [' in: {\"query\":\"specialty coffee demand\"}', ' out: {\"results\":[…]}']\n */\nexport function formatSpanIO(span: TraceSpan, depth = 0, maxChars = DEFAULT_IO_MAX_CHARS): string[] {\n const lines: string[] = [];\n const indent = \" \".repeat(depth + 1);\n\n if (span.input !== undefined) {\n lines.push(`${indent}in: ${preview(span.input, maxChars)}`);\n }\n\n if (span.output !== undefined) {\n lines.push(`${indent}out: ${preview(span.output, maxChars)}`);\n }\n\n return lines;\n}\n\n/**\n * One-line, length-capped preview of a captured value. Strings pass\n * through; everything else is JSON-stringified (falling back to\n * `String()` on a circular / unstringifiable value). Internal whitespace\n * is collapsed so the preview never breaks the tree layout.\n */\nfunction preview(value: unknown, maxChars: number): string {\n const text = typeof value === \"string\" ? value : stringify(value);\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n\n return collapsed.length > maxChars ? `${collapsed.slice(0, maxChars)}…` : collapsed;\n}\n\nfunction stringify(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n","import type { TraceSpan } from \"../../contracts\";\nimport { totalCostUsd } from \"../utils\";\n\n/**\n * Render a single {@link TraceSpan} as one scannable console line:\n * `<status> <type> \"<name>\" — <duration>ms, <tokens> tok[, $<cost>]`.\n * `depth` controls leading indentation when printing a tree. Pure — no\n * side effects — so it is trivially testable and reused by both the\n * per-trace summary and the per-span streaming line.\n *\n * @example\n * formatSpanLine(span, 1);\n * // ' ok agent \"router\" — 1240ms, 470 tok, $0.0021'\n */\nexport function formatSpanLine(span: TraceSpan, depth = 0): string {\n const indent = \" \".repeat(depth);\n const marker = statusMarker(span.status);\n const cost = totalCostUsd(span.usage);\n const costSuffix = cost === undefined ? \"\" : `, $${cost.toFixed(4)}`;\n\n let line = `${indent}${marker} ${span.type} \"${span.name}\" — ${span.duration}ms, ${span.usage.total} tok${costSuffix}`;\n\n if (span.error) {\n line += ` [${span.error.type}: ${span.error.message}]`;\n }\n\n return line;\n}\n\n/**\n * Short ASCII marker for a span's terminal status. Plain ASCII (no\n * emoji/color codes) so output stays clean in log aggregators and CI.\n */\nfunction statusMarker(status: TraceSpan[\"status\"]): string {\n switch (status) {\n case \"completed\":\n return \"ok\";\n case \"failed\":\n return \"ERR\";\n case \"cancelled\":\n return \"cancel\";\n case \"max-iterations\":\n return \"max-iter\";\n case \"awaiting-input\":\n return \"await\";\n default:\n return status;\n }\n}\n","import type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport { walkSpans } from \"../utils\";\nimport type { ConsoleExporterOptions, ConsoleLike } from \"./console-exporter.type\";\nimport { formatSpanIO } from \"./format-span-io\";\nimport { formatSpanLine } from \"./format-span-line\";\n\nconst EXPORTER_NAME = \"console\";\n\n/**\n * Zero-dependency {@link ExporterContract} that prints traces to a\n * console-like sink. The simplest exporter — useful in development and\n * as the reference implementation of the contract.\n *\n * By default it prints one summary line per completed trace. Set\n * `tree: true` to print the full indented span tree, `io: true` to also\n * print each span's captured `input` / `output` (needs the collector's\n * `captureContent`), and `streaming: true` to print each span the moment\n * it finalizes (via the optional `exportSpan` hook).\n *\n * @example\n * collector.use(consoleExporter());\n * // ok workflow \"checkout\" — 2103ms, 1820 tok, $0.0094\n *\n * @example\n * collector.use(consoleExporter({ tree: true }));\n *\n * @example\n * // Full content trace — prompts, responses, and tool I/O:\n * const observe = panoptic({\n * captureContent: true,\n * exporters: [consoleExporter({ tree: true, io: true })],\n * });\n */\nexport function consoleExporter(options: ConsoleExporterOptions = {}): ExporterContract {\n const sink: ConsoleLike = options.console ?? console;\n const tree = options.tree ?? false;\n const io = options.io ?? false;\n const ioMaxChars = options.ioMaxChars;\n\n const exporter: ExporterContract = {\n name: EXPORTER_NAME,\n export(trace: Trace): void {\n writeTrace(sink, trace, tree, io, ioMaxChars);\n },\n };\n\n if (options.streaming) {\n exporter.exportSpan = (span: TraceSpan): void => {\n sink.log(formatSpanLine(span));\n\n if (io) {\n for (const line of formatSpanIO(span, 0, ioMaxChars)) {\n sink.log(line);\n }\n }\n };\n }\n\n return exporter;\n}\n\n/**\n * Write a completed trace — either a single root summary line or the\n * full indented tree, each span optionally followed by its captured\n * `input` / `output`. Failed / cancelled spans route to `console.error`\n * so they surface at the right severity in log aggregators.\n */\nfunction writeTrace(\n sink: ConsoleLike,\n trace: Trace,\n tree: boolean,\n io: boolean,\n ioMaxChars: number | undefined,\n): void {\n if (!tree) {\n writeSpan(sink, trace.root, 0, io, ioMaxChars);\n return;\n }\n\n for (const span of walkSpans(trace.root)) {\n const depth = spanDepth(trace.root, span.spanId);\n writeSpan(sink, span, depth, io, ioMaxChars);\n }\n}\n\n/**\n * Write one span's line and — when `io` is on — its captured content,\n * all routed at the span's own severity so a failed span keeps its\n * content beside it in the error stream.\n */\nfunction writeSpan(\n sink: ConsoleLike,\n span: TraceSpan,\n depth: number,\n io: boolean,\n ioMaxChars: number | undefined,\n): void {\n writeAtSeverity(sink, span.status, formatSpanLine(span, depth));\n\n if (io) {\n for (const line of formatSpanIO(span, depth, ioMaxChars)) {\n writeAtSeverity(sink, span.status, line);\n }\n }\n}\n\n/**\n * Route a line to `error` when the span failed/cancelled, otherwise to\n * `log`. Keeps healthy traces out of the error stream.\n */\nfunction writeAtSeverity(sink: ConsoleLike, status: TraceSpan[\"status\"], line: string): void {\n if (status === \"failed\" || status === \"cancelled\") {\n sink.error(line);\n return;\n }\n\n sink.log(line);\n}\n\n/**\n * Depth of `targetSpanId` below `root` for indentation. Walks the tree\n * once; returns 0 when the span is the root or not found.\n */\nfunction spanDepth(root: TraceSpan, targetSpanId: string, depth = 0): number {\n if (root.spanId === targetSpanId) {\n return depth;\n }\n\n for (const child of root.children) {\n const found = spanDepth(child, targetSpanId, depth + 1);\n\n if (found > 0) {\n return found;\n }\n }\n\n return 0;\n}\n","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\n ? JSON.stringify(record, undefined, 2)\n : JSON.stringify(record);\n\n return `${json}\\n`;\n }\n}\n","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","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","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(\n (result as { report?: unknown }).report,\n (result as { error?: unknown }).error,\n );\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","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 =\n 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","import { judgePromptBody } from \"@warlock.js/ai\";\nimport type { EvaluateConfig, EvaluateVerdict } from \"./evaluate.type\";\n\n/**\n * Grade `systemPrompt` with the configured judge model, reusing\n * `ai.prompts().validate()`'s own `judgePromptBody` — never a second\n * judging implementation. `instructionsOverride` (the dashboard's\n * per-run textarea) wins over `config.instructions`; with neither, the\n * judge falls back to `judgePromptBody`'s built-in prompt-quality rubric.\n *\n * The judge itself never throws (`judgePromptBody` degrades to an\n * issues-only outcome on failure) — the only thing that CAN throw here is\n * resolving `config.model` (a factory constructing an SDK client), which\n * the caller (the dashboard route) is expected to catch.\n */\nexport async function evaluateSystemPrompt(\n systemPrompt: string,\n config: EvaluateConfig,\n instructionsOverride?: string,\n): Promise<EvaluateVerdict> {\n const model = typeof config.model === \"function\" ? await config.model() : config.model;\n const instructions = instructionsOverride?.trim() || config.instructions;\n\n return judgePromptBody(systemPrompt, model, instructions);\n}\n","import type { TraceSpan } from \"../contracts/trace.type\";\n\n/**\n * Pull the LAST `{role: \"system\"}` message off a span's captured `input`.\n *\n * Only present under `captureContent` (off by default), and only shaped\n * this way for non-tool spans — either the `[system, user]` first-trip pair\n * or, under `fullHistory`, the full `CapturedMessage[]` conversation (see\n * `collector/report-to-span.ts`). \"Last\" (not \"only\") matters for\n * `fullHistory`: a long-running agent can carry more than one system-role\n * turn, and the most recent one is the one actually in effect. Returns\n * `undefined` for a tool span, an agent with no system prompt, or when\n * content capture is off — the caller treats that as \"nothing to evaluate.\"\n */\nexport function extractLastSystemPrompt(span: TraceSpan): string | undefined {\n if (!Array.isArray(span.input)) {\n return undefined;\n }\n\n for (let index = span.input.length - 1; index >= 0; index -= 1) {\n const entry = span.input[index] as { role?: unknown; content?: unknown };\n\n if (entry && typeof entry === \"object\" && entry.role === \"system\") {\n return typeof entry.content === \"string\" ? entry.content : undefined;\n }\n }\n\n return undefined;\n}\n","import type { TraceSpan } from \"../contracts/trace.type\";\n\n/** Depth-first search for the span with `spanId` inside a trace's span tree. */\nexport function findSpanById(root: TraceSpan, spanId: string): TraceSpan | undefined {\n if (root.spanId === spanId) {\n return root;\n }\n\n for (const child of root.children) {\n const found = findSpanById(child, spanId);\n\n if (found !== undefined) {\n return found;\n }\n }\n\n return undefined;\n}\n","import type { ReportStatus } from \"@warlock.js/ai\";\nimport type { TraceQuery } from \"../store/trace-query.type\";\n\n/**\n * The set of terminal statuses a stored trace can carry. Used to keep\n * `parseQuery` from forwarding arbitrary `?status=` junk into the store.\n */\nconst KNOWN_STATUSES: readonly ReportStatus[] = [\n \"completed\",\n \"failed\",\n \"cancelled\",\n \"max-iterations\",\n \"awaiting-input\",\n \"awaiting-approval\",\n] as const;\n\n/**\n * Map a request's query string onto a {@link TraceQuery} the trace store\n * understands. Every field is optional — an absent param is \"don't care\",\n * so an empty query string yields an empty filter that matches every\n * stored trace.\n *\n * - `traceId` / `sessionId` — passed through verbatim (first value wins).\n * - `status` — **repeatable**: `?status=failed&status=cancelled` becomes\n * `[\"failed\", \"cancelled\"]`; a single value stays a scalar. Unknown\n * status tokens are dropped so a typo never silently matches nothing in\n * a confusing way (it simply isn't filtered on).\n * - `startedAfter` / `startedBefore` — forwarded as ISO strings; the\n * store accepts either a string or `Date`, and `matchTrace` does the\n * inclusive bound comparison.\n *\n * Unknown params are ignored. Pure — takes a `URLSearchParams`, returns a\n * plain object — so it's trivially testable without a live server.\n *\n * @example\n * parseQuery(new URLSearchParams(\"status=failed&status=cancelled&sessionId=s1\"));\n * // → { status: [\"failed\", \"cancelled\"], sessionId: \"s1\" }\n */\nexport function parseQuery(params: URLSearchParams): TraceQuery {\n const query: TraceQuery = {};\n\n const traceId = params.get(\"traceId\");\n if (traceId !== null && traceId.length > 0) {\n query.traceId = traceId;\n }\n\n const sessionId = params.get(\"sessionId\");\n if (sessionId !== null && sessionId.length > 0) {\n query.sessionId = sessionId;\n }\n\n const statuses = params\n .getAll(\"status\")\n .filter((value): value is ReportStatus => (KNOWN_STATUSES as readonly string[]).includes(value));\n if (statuses.length === 1) {\n query.status = statuses[0];\n } else if (statuses.length > 1) {\n query.status = statuses;\n }\n\n const startedAfter = params.get(\"startedAfter\");\n if (startedAfter !== null && startedAfter.length > 0) {\n query.startedAfter = startedAfter;\n }\n\n const startedBefore = params.get(\"startedBefore\");\n if (startedBefore !== null && startedBefore.length > 0) {\n query.startedBefore = startedBefore;\n }\n\n return query;\n}\n","/**\n * The Warlock logo as an inlined base64 PNG data URI (64x64, ~9KB).\n * Inlined so the dashboard stays a single self-contained, offline-capable\n * page with no external asset request. Regenerate from\n * @warlock.js/docs/public/logo.png via:\n * magick logo.png -resize 64x64 -strip out.png && base64 -w0 out.png\n */\nexport const WARLOCK_LOGO_DATA_URI =\n \"data:image/png;base64,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\";\n","import { WARLOCK_LOGO_DATA_URI } from \"./warlock-logo\";\n\n/**\n * Build the single self-contained dashboard HTML page. No external\n * assets, no bundler, no framework — one inlined string that polls the\n * read-only JSON API (`{basePath}api/aggregate`, `{basePath}api/traces`,\n * `{basePath}api/traces/:id`) and renders:\n *\n * - the {@link TraceAggregate} headline counts (traces / completed /\n * failed / cancelled / tokens in·out·total / cost);\n * - a newest-first trace list as clickable master rows, each led by a\n * colour-coded, title-cased type label (Supervisor / Agent / Tool / …);\n * - a two-pane drawer: a collapsible call tree on the left (the nested\n * span hierarchy) and the selected node's detail on the right — rich\n * input/output, token breakdown, and a metadata panel (session id,\n * ids, version, attributes).\n *\n * Span input/output is rendered structurally (chat bubbles / key-value /\n * Markdown), durations in seconds, tokens as ↓input · ↑output · total.\n * Theme is light / dark / system (persisted to localStorage). All UI\n * state — selected trace, selected span, collapsed nodes — lives in JS,\n * NOT the DOM, so the 2s poll never disturbs an open drawer.\n *\n * The list is filterable entirely client-side over the polled traces: a\n * free-text search (name + session), status / type / session filter\n * chips, an \"errors only\" header toggle, and an optional group-by-session\n * view with collapsible headers. Each tree node and trace row carries a\n * cost heatmap accent scaled to the trace's most expensive node, with a\n * small legend. The drawer's left pane toggles between the nested call\n * TREE and a Gantt TIMELINE (span offset from root start + duration,\n * critical path highlighted). The selected trace and span are reflected\n * in the URL hash (`#trace=&span=`) and re-opened from it on load — so a\n * drawer view is shareable/bookmarkable. A live socket tail is a noted\n * follow-up; this pass stays on the 2s poll.\n *\n * `basePath` and `title` are baked in at serve time. The page is\n * intentionally dependency-free vanilla JS so it works offline.\n *\n * @param basePath Normalized mount path ending in `/` (e.g. `\"/\"`).\n * @param title Header title shown in the page.\n */\nexport function dashboardHtml(\n basePath: string,\n title: string,\n evaluateEnabled: boolean = false,\n evaluateDefaultInstructions: string = \"\",\n): string {\n const apiBase = `${basePath}api`;\n const safeTitle = escapeHtml(title);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>${safeTitle}</title>\n<script>\n(function () {\n try {\n var m = localStorage.getItem(\"panoptic-theme\") || \"system\";\n var light = m === \"light\" || (m === \"system\" && window.matchMedia && window.matchMedia(\"(prefers-color-scheme: light)\").matches);\n document.documentElement.setAttribute(\"data-theme\", light ? \"light\" : \"dark\");\n } catch (e) {}\n})();\n</script>\n<style>\n :root {\n color-scheme: dark;\n --bg: #0b0d10; --surface: #101317; --surface2: #14171c; --panel: #0d1014;\n --border: #23272e; --border2: #2d333b;\n --text: #e6e8eb; --text2: #adbac7; --dim: #8b949e;\n --sel-bg: #0f1722; --sel-border: #316dca;\n --ty-agent: #539bf5; --ty-tool: #c297ff; --ty-model: #4cc2b0; --ty-prim: #e3b341; --ty-other: #adbac7;\n --tok-in: #58a6ff; --tok-out: #3fb950; --tok-total: #b899ff; --cost: #e3b341;\n --ok: #56d364; --ok-bg: #0f2e1d; --fail: #f85149; --fail-bg: #3a1416; --cancel: #d29922; --cancel-bg: #332701; --other: #8b949e; --other-bg: #1c2128;\n --code-bg: #0b0d10; --inline-bg: #1c2128; --link: #539bf5;\n }\n :root[data-theme=\"light\"] {\n color-scheme: light;\n --bg: #ffffff; --surface: #f6f8fa; --surface2: #eef1f4; --panel: #ffffff;\n --border: #d0d7de; --border2: #afb8c1;\n --text: #1f2328; --text2: #3b4350; --dim: #636c76;\n --sel-bg: #ddf4ff; --sel-border: #0969da;\n --ty-agent: #0969da; --ty-tool: #8250df; --ty-model: #0f7d6b; --ty-prim: #9a6700; --ty-other: #57606a;\n --tok-in: #0969da; --tok-out: #1a7f37; --tok-total: #8250df; --cost: #9a6700;\n --ok: #1a7f37; --ok-bg: #dafbe1; --fail: #cf222e; --fail-bg: #ffebe9; --cancel: #9a6700; --cancel-bg: #fff8c5; --other: #57606a; --other-bg: #eaeef2;\n --code-bg: #f6f8fa; --inline-bg: #eaeef2; --link: #0969da;\n }\n * { box-sizing: border-box; }\n body { margin: 0; font: 14px/1.5 ui-sans-serif, system-ui, sans-serif; background: var(--bg); color: var(--text); }\n header { padding: 14px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }\n header img.logo { height: 26px; width: 26px; display: block; flex: none; }\n header h1 { font-size: 16px; margin: 0; font-weight: 600; }\n header .meta { color: var(--dim); font-size: 12px; }\n .theme { margin-left: auto; display: flex; gap: 2px; border: 1px solid var(--border); border-radius: 8px; padding: 2px; }\n .theme button { background: transparent; border: none; color: var(--dim); cursor: pointer; font-size: 14px; line-height: 1; padding: 4px 8px; border-radius: 6px; }\n .theme button:hover { color: var(--text); }\n .theme button.active { background: var(--surface2); color: var(--text); }\n .stats { display: flex; flex-wrap: wrap; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border); }\n .stat { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; min-width: 84px; }\n .stat .label { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }\n .stat .value { font-size: 18px; font-weight: 600; }\n main { padding: 12px 20px 40px; }\n\n .trace-row { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; background: var(--surface); padding: 9px 12px; display: flex; align-items: center; gap: 9px; cursor: pointer; transition: background .12s ease, border-color .12s ease; }\n .trace-row:hover { background: var(--surface2); border-color: var(--border2); }\n .trace-row.selected { border-color: var(--sel-border); background: var(--sel-bg); }\n .rname { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .rprompt { font-size: 11px; color: var(--ty-agent); border: 1px solid var(--border2); border-radius: 6px; padding: 1px 6px; white-space: nowrap; }\n .rright { margin-left: auto; display: flex; align-items: center; gap: 6px; white-space: nowrap; }\n .chev { color: var(--dim); font-size: 16px; font-style: normal; }\n\n .tylabel { font-weight: 600; flex: none; }\n .ty-agent { color: var(--ty-agent); } .ty-tool { color: var(--ty-tool); } .ty-model { color: var(--ty-model); } .ty-prim { color: var(--ty-prim); } .ty-other { color: var(--ty-other); }\n .badge { font-size: 11px; padding: 2px 8px; border-radius: 999px; font-weight: 600; flex: none; }\n .badge.completed { background: var(--ok-bg); color: var(--ok); }\n .badge.failed { background: var(--fail-bg); color: var(--fail); }\n .badge.cancelled { background: var(--cancel-bg); color: var(--cancel); }\n .badge.other { background: var(--other-bg); color: var(--other); }\n .sdot { width: 7px; height: 7px; border-radius: 999px; display: inline-block; flex: none; }\n .sdot-completed { background: var(--ok); } .sdot-failed { background: var(--fail); } .sdot-cancelled { background: var(--cancel); } .sdot-other { background: var(--other); }\n .dim { color: var(--dim); font-size: 12px; }\n .tok { white-space: nowrap; font-size: 12px; }\n .tok-in { color: var(--tok-in); } .tok-out { color: var(--tok-out); } .tok-total { color: var(--tok-total); }\n .cost { color: var(--cost); font-weight: 600; font-size: 12px; white-space: nowrap; }\n\n .io-body { margin-bottom: 6px; }\n .piol { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; margin: 11px 0 3px; }\n .messages { display: flex; flex-direction: column; gap: 8px; }\n .msg-collapse { border: 1px solid var(--border); border-radius: 8px; background: var(--surface); }\n .msg-collapse > summary { cursor: pointer; padding: 7px 10px; list-style: none; font-size: 12px; display: flex; align-items: center; gap: 6px; }\n .msg-collapse > summary::-webkit-details-marker { display: none; }\n .msg-collapse > summary::before { content: \"\\\\25B8\"; color: var(--dim); }\n .msg-collapse[open] > summary::before { content: \"\\\\25BE\"; }\n .msg-collapse[open] > summary { border-bottom: 1px solid var(--border); }\n .msg-collapse .messages { padding: 8px; }\n .msg-collapse .msg-count { font-weight: 600; color: var(--text2); }\n .msg { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--surface); }\n .msg-role { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; padding: 4px 10px; color: var(--text2); background: var(--surface2); border-bottom: 1px solid var(--border); }\n .msg-role.role-system { color: var(--cancel); } .msg-role.role-user { color: var(--ty-agent); } .msg-role.role-assistant { color: var(--ok); } .msg-role.role-tool { color: var(--ty-tool); }\n .msg-content { padding: 8px 10px; }\n .kv { display: grid; grid-template-columns: max-content 1fr; gap: 2px 12px; align-items: start; }\n .kv-row { display: contents; }\n .kv-k { color: var(--dim); font-family: ui-monospace, monospace; font-size: 12px; padding: 2px 0; white-space: nowrap; }\n .kv-v { font-size: 13px; min-width: 0; overflow: auto; padding: 1px 0; word-break: break-word; }\n .part { border-left: 2px solid var(--border2); padding-left: 8px; margin: 4px 0; }\n .part-label { font-size: 11px; text-transform: uppercase; color: var(--dim); margin-bottom: 2px; }\n .md-h { font-weight: 700; margin: 8px 0 4px; }\n .md-h1 { font-size: 16px; } .md-h2 { font-size: 14px; } .md-h3 { font-size: 13px; color: var(--text2); } .md-h4 { font-size: 12px; color: var(--dim); }\n .md-p { margin: 4px 0; white-space: pre-wrap; word-break: break-word; }\n .io-body ul, .msg-content ul { margin: 4px 0; padding-left: 18px; }\n pre.code { background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px; overflow: auto; max-height: 320px; font-size: 12px; white-space: pre; margin: 6px 0; }\n pre.mini { background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; overflow: auto; max-height: 200px; font-size: 12px; margin: 0; }\n code { background: var(--inline-bg); border-radius: 4px; padding: 1px 4px; font-family: ui-monospace, monospace; font-size: 12px; }\n a { color: var(--link); }\n .empty { color: var(--dim); padding: 30px; text-align: center; }\n\n .backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); opacity: 0; pointer-events: none; transition: opacity .18s ease; z-index: 40; }\n .backdrop.open { opacity: 1; pointer-events: auto; }\n .drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(760px, 96vw); background: var(--panel); border-left: 1px solid var(--border); transform: translateX(100%); transition: transform .18s ease; z-index: 50; display: flex; flex-direction: column; box-shadow: -16px 0 40px rgba(0,0,0,.4); }\n .drawer.open { transform: translateX(0); }\n .drawer-head { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }\n .drawer-close { margin-left: auto; background: transparent; border: 1px solid var(--border); color: var(--dim); border-radius: 6px; cursor: pointer; font-size: 13px; line-height: 1; padding: 5px 9px; }\n .drawer-close:hover { color: var(--text); border-color: var(--border2); }\n .drawer-body { flex: 1; overflow: hidden; }\n .dsplit { display: flex; height: 100%; }\n .dtree { flex: 0 0 44%; overflow: auto; padding: 8px 6px; border-right: 1px solid var(--border); }\n .ddetail { flex: 1; overflow: auto; padding: 10px 14px; min-width: 0; }\n .tnode { display: flex; align-items: center; gap: 7px; padding: 5px 7px; border-radius: 6px; cursor: pointer; font-size: 13px; border: 1px solid transparent; }\n .tnode:hover { background: var(--surface); }\n .tnode.selected { background: var(--sel-bg); border-color: var(--sel-border); }\n .tname { color: var(--text2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .tmeta { margin-left: auto; color: var(--dim); font-size: 12px; white-space: nowrap; }\n .twisty { color: var(--dim); font-size: 11px; width: 12px; text-align: center; flex: none; }\n .tkids { margin-left: 10px; padding-left: 9px; border-left: 1px solid var(--border); }\n .crumb { font-size: 12px; color: var(--dim); margin-bottom: 6px; word-break: break-all; }\n .crumb .sep { color: var(--border2); }\n .dhead-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }\n .meta-sec { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 8px; }\n .meta-title { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--dim); margin-bottom: 6px; }\n\n /* Toolbar: search box + filter chips over the trace list. */\n .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 20px; border-bottom: 1px solid var(--border); }\n .search { flex: 1 1 220px; min-width: 160px; background: var(--surface2); border: 1px solid var(--border); color: var(--text); border-radius: 8px; padding: 7px 10px; font: inherit; }\n .search:focus { outline: none; border-color: var(--sel-border); }\n .chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n .chip { background: var(--surface2); border: 1px solid var(--border); color: var(--text2); border-radius: 999px; padding: 4px 11px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease; }\n .chip:hover { color: var(--text); border-color: var(--border2); }\n .chip.active { background: var(--sel-bg); border-color: var(--sel-border); color: var(--text); }\n .chip-clear { color: var(--dim); border-style: dashed; }\n .chip-group { display: inline-flex; gap: 6px; align-items: center; }\n .chip-group .gl { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--dim); }\n .toggle { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: var(--text2); user-select: none; }\n .toggle input { accent-color: var(--sel-border); }\n .toggle select { background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; font-size: 12px; cursor: pointer; }\n .toggle select:hover { border-color: var(--border2); }\n\n /* Session grouping headers. */\n .sgroup { margin-bottom: 10px; }\n .sgroup-head { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 8px; background: var(--surface2); border: 1px solid var(--border); cursor: pointer; margin-bottom: 6px; }\n .sgroup-head:hover { border-color: var(--border2); }\n .sgroup-tw { color: var(--dim); font-size: 11px; width: 12px; text-align: center; flex: none; }\n .sgroup-id { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .sgroup-count { margin-left: auto; color: var(--dim); font-size: 12px; }\n .sgroup-body { padding-left: 6px; }\n\n /* Per-type aggregate stats panel (a CSS-grid table above the trace list). */\n .stats-table { margin: 0 0 14px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; font-size: 12px; }\n .strow { display: grid; grid-template-columns: 1.5fr 0.7fr 1fr 0.8fr 0.8fr 1fr 1fr; gap: 10px; align-items: center; padding: 6px 12px; border-bottom: 1px solid var(--border); }\n .strow:last-child { border-bottom: none; }\n .strow.sthead { background: var(--surface2); color: var(--dim); font-weight: 600; }\n .strow > span { text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .strow > .stc-type { text-align: left; }\n .stc-fail.fail { color: var(--fail); }\n\n /* Cost heatmap: a left accent bar tinted by relative rollup cost. */\n .trace-row { position: relative; }\n .tnode { position: relative; }\n .heat { position: absolute; left: 0; top: 3px; bottom: 3px; width: 3px; border-radius: 2px; background: var(--cost); }\n\n /* Timeline / waterfall view in the drawer. */\n .dview { display: flex; gap: 4px; padding: 6px 8px; border-bottom: 1px solid var(--border); }\n .dview button { background: transparent; border: 1px solid var(--border); color: var(--dim); cursor: pointer; font-size: 12px; padding: 4px 10px; border-radius: 6px; }\n .dview button:hover { color: var(--text); }\n .dview button.active { background: var(--surface2); color: var(--text); border-color: var(--border2); }\n .legend { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dim); }\n .legend .grad { width: 64px; height: 8px; border-radius: 4px; background: linear-gradient(90deg, var(--surface2), var(--cost)); border: 1px solid var(--border); }\n .gantt { padding: 8px 10px; }\n\n /* Evaluate — the drawer's one write action (config-gated). */\n .eval-btn { background: transparent; border: 1px solid var(--border); color: var(--text2); cursor: pointer; font-size: 12px; padding: 4px 10px; border-radius: 6px; margin: 10px 0 0; }\n .eval-btn:hover:not(:disabled) { color: var(--text); border-color: var(--border2); }\n .eval-btn:disabled { opacity: .6; cursor: default; }\n .eval-panel { margin-top: 8px; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface2); }\n .eval-panel textarea { width: 100%; min-height: 64px; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 8px; font: 12px/1.5 ui-sans-serif, system-ui, sans-serif; resize: vertical; }\n .eval-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; }\n .eval-error { color: var(--fail); font-size: 12px; }\n .eval-result { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 12px; }\n .eval-score { font-weight: 600; }\n .eval-issues { margin: 6px 0 0; padding-left: 18px; }\n .grow { display: flex; align-items: center; gap: 8px; padding: 2px 0; font-size: 12px; cursor: pointer; border-radius: 4px; }\n .grow:hover { background: var(--surface); }\n .grow.selected { background: var(--sel-bg); }\n .glabel { flex: 0 0 38%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text2); }\n .gtrack { position: relative; flex: 1; height: 14px; background: var(--surface2); border-radius: 4px; overflow: hidden; }\n .gbar { position: absolute; top: 2px; bottom: 2px; border-radius: 3px; background: var(--ty-agent); min-width: 2px; }\n .gbar.crit { background: var(--fail); }\n .gbar.bar-completed { background: var(--ty-agent); } .gbar.bar-failed { background: var(--fail); } .gbar.bar-cancelled { background: var(--cancel); } .gbar.bar-other { background: var(--other); }\n .gdur { flex: 0 0 auto; color: var(--dim); white-space: nowrap; min-width: 44px; text-align: right; }\n .gantt-legend { display: flex; gap: 14px; margin-top: 8px; font-size: 11px; color: var(--dim); }\n .gantt-legend .ck { display: inline-flex; align-items: center; gap: 5px; }\n .gantt-legend .sw { width: 12px; height: 8px; border-radius: 2px; display: inline-block; }\n\n @media (max-width: 560px) {\n .dsplit { flex-direction: column; }\n .dtree { flex: none; max-height: 42vh; border-right: none; border-bottom: 1px solid var(--border); }\n .glabel { flex-basis: 30%; }\n }\n</style>\n</head>\n<body>\n<header>\n <img class=\"logo\" src=\"${WARLOCK_LOGO_DATA_URI}\" alt=\"Warlock\" />\n <h1>${safeTitle}</h1>\n <span class=\"meta\" id=\"meta\">connecting…</span>\n <div class=\"theme\" id=\"theme\" role=\"group\" aria-label=\"Theme\" style=\"margin-left:auto\">\n <button type=\"button\" data-theme-set=\"light\" title=\"Light\" aria-label=\"Light theme\">☀</button>\n <button type=\"button\" data-theme-set=\"dark\" title=\"Dark\" aria-label=\"Dark theme\">☾</button>\n <button type=\"button\" data-theme-set=\"system\" title=\"System\" aria-label=\"System theme\">◐</button>\n </div>\n</header>\n<div class=\"stats\" id=\"stats\"></div>\n<div class=\"toolbar\" id=\"toolbar\">\n <input class=\"search\" id=\"search\" type=\"search\" placeholder=\"Search name or session…\" aria-label=\"Search traces\" autocomplete=\"off\" />\n <div class=\"chips\" id=\"status-chips\" role=\"group\" aria-label=\"Filter by status\"></div>\n <div class=\"chips\" id=\"type-chips\" role=\"group\" aria-label=\"Filter by type\"></div>\n <div class=\"chips\" id=\"session-chips\" role=\"group\" aria-label=\"Filter by session\"></div>\n <div class=\"chips\" id=\"prompt-chips\" role=\"group\" aria-label=\"Filter by prompt version\"></div>\n <label class=\"toggle\" id=\"group-wrap\" title=\"Group the trace list (mutually exclusive)\">\n Group\n <select id=\"group-by\" aria-label=\"Group the trace list\">\n <option value=\"\">None</option>\n <option value=\"session\">Session</option>\n <option value=\"prompt\">Prompt</option>\n <option value=\"type\">Type</option>\n </select>\n </label>\n <label class=\"toggle\" id=\"stats-wrap\" title=\"Show a per-type aggregate stats panel (count, failure rate, p50/p95 latency, tokens, cost)\">\n <input type=\"checkbox\" id=\"show-stats\" /> Stats\n </label>\n <button class=\"chip chip-clear\" id=\"clear-filters\" type=\"button\" title=\"Clear all filters\">Clear</button>\n</div>\n<div id=\"stats-panel\"></div>\n<main id=\"traces\"><div class=\"empty\">Loading…</div></main>\n\n<div class=\"backdrop\" id=\"backdrop\"></div>\n<aside class=\"drawer\" id=\"drawer\" aria-hidden=\"true\" aria-label=\"Trace detail\">\n <div class=\"drawer-head\" id=\"drawer-head\"></div>\n <div class=\"drawer-body\" id=\"drawer-body\"></div>\n</aside>\n\n<script>\n(function () {\n var API = ${JSON.stringify(apiBase)};\n var EVALUATE_ENABLED = ${JSON.stringify(evaluateEnabled)};\n var BT = String.fromCharCode(96);\n var EVALUATE_DEFAULT_INSTRUCTIONS = ${encodeForInlineScript(evaluateDefaultInstructions)};\n\n // Carry the ?token= the page itself was loaded with onto every\n // subsequent poll as an Authorization header — otherwise the API calls\n // below inherit no auth and 401 forever once authToken is configured\n // (the initial page load is the only request the URL's query string\n // naturally reaches; the server only honors ?token= on that one route,\n // see serve.ts). The token itself is kept in its own nested closure,\n // not a plain var sitting alongside the rest of this file's top-level\n // state, so it isn't trivially reachable from other code sharing this\n // script's outer scope; only the fetchAuthed function it returns is.\n var fetchAuthed = (function () {\n var TOKEN = new URLSearchParams(window.location.search).get(\"token\");\n return function fetchAuthed(url, options) {\n var opts = options || {};\n var headers = opts.headers || {};\n if (TOKEN) headers = Object.assign({}, headers, { Authorization: \"Bearer \" + TOKEN });\n return fetch(url, Object.assign({}, opts, { headers: headers }));\n };\n })();\n\n var state = {\n traces: [], selectedId: null, selectedSpanId: null, collapsed: {}, sig: null,\n // Client-side filter state (search box + chips + errors-only header toggle).\n filter: { text: \"\", statuses: {}, types: {}, sessionId: null, promptKey: null, errorsOnly: false },\n groupBySession: false, // session-grouping list toggle\n groupByPrompt: false, // prompt-version-grouping list toggle\n groupByType: false, // root-type-grouping list toggle\n showStats: false, // per-type aggregate-stats panel toggle (independent of grouping)\n collapsedGroups: {}, // collapsed group headers (session, prompt, or type)\n view: \"tree\", // drawer left pane: \"tree\" | \"timeline\"\n hashApplied: false, // guards one-time deep-link open on load\n evaluate: {} // per-span evaluate UI state, keyed by spanId\n };\n\n var STATUS_FILTERS = [\"completed\", \"failed\", \"cancelled\"];\n var TYPE_FILTERS = [\"agent\", \"tool\", \"model\", \"supervisor\", \"team\", \"workflow\", \"orchestrator\", \"planner\", \"batch\", \"callback\"];\n var ERROR_STATUSES = { failed: 1, cancelled: 1 };\n var NO_SESSION_KEY = \"(no session)\";\n var NO_PROMPT_KEY = \"(no prompt)\";\n var NO_TYPE_KEY = \"(no type)\";\n\n var statusClass = function (s) {\n if (s === \"completed\" || s === \"failed\" || s === \"cancelled\") return s;\n return \"other\";\n };\n var esc = function (v) {\n return String(v).replace(/[&<>\"']/g, function (c) {\n return { \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" }[c];\n });\n };\n var num = function (n) { return (n == null ? 0 : n).toLocaleString(); };\n var fmt = function (v) {\n if (v == null) return \"\";\n if (typeof v === \"string\") return v;\n try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }\n };\n\n function dur(ms) {\n if (ms == null) return \"—\";\n if (ms >= 1000) return (ms / 1000).toFixed(1) + \"s\";\n if (ms >= 1) return Math.round(ms) + \"ms\";\n return ms.toFixed(1) + \"ms\";\n }\n\n function tokens(u) {\n if (!u) return \"\";\n var i = u.input || 0, o = u.output || 0, t = (u.total != null) ? u.total : (i + o);\n if (i === 0 && o === 0 && t === 0) return \"\"; // nothing to show — hide the row\n return '<span class=\"tok\">'\n + '<span class=\"tok-in\" title=\"input tokens\">↓ ' + num(i) + \"</span> \"\n + '<span class=\"tok-out\" title=\"output tokens\">↑ ' + num(o) + \"</span> \"\n + '<span class=\"tok-total\" title=\"total tokens\">' + num(t) + \" total</span></span>\";\n }\n\n var TYPE_LABELS = {\n agent: \"Agent\", tool: \"Tool\", model: \"Model\", supervisor: \"Supervisor\",\n workflow: \"Workflow\", orchestrator: \"Orchestrator\", team: \"Team\",\n planner: \"Planner\", prompt: \"Prompt\", guardrail: \"Guardrail\"\n };\n var PRIMITIVES = { supervisor: 1, workflow: 1, orchestrator: 1, team: 1, planner: 1 };\n function typeClass(type) {\n if (PRIMITIVES[type]) return \"ty-prim\";\n if (type === \"agent\") return \"ty-agent\";\n if (type === \"tool\") return \"ty-tool\";\n if (type === \"model\") return \"ty-model\";\n return \"ty-other\";\n }\n function typeLabel(type) {\n var t = type || \"node\";\n var label = TYPE_LABELS[t] || (t.charAt(0).toUpperCase() + t.slice(1));\n return '<span class=\"tylabel ' + typeClass(t) + '\">' + esc(label) + \"</span>\";\n }\n // Plain-text (no span wrapper) capitalized labels for bare-text sites —\n // group headers, filter-chip labels — where the colored typeLabel span is\n // not wanted. Underlying keys/classes/filter values stay raw lowercase.\n function typeText(type) {\n var t = type || \"node\";\n return TYPE_LABELS[t] || (t.charAt(0).toUpperCase() + t.slice(1));\n }\n function statusText(s) {\n return s ? String(s).charAt(0).toUpperCase() + String(s).slice(1) : s;\n }\n function statusDot(s) { return '<span class=\"sdot sdot-' + statusClass(s) + '\" title=\"' + esc(s) + '\"></span>'; }\n\n function findTrace(id) {\n for (var i = 0; i < state.traces.length; i++) if (state.traces[i].traceId === id) return state.traces[i];\n return null;\n }\n function findSpan(span, id) {\n if (span.spanId === id) return span;\n var k = span.children || [];\n for (var i = 0; i < k.length; i++) { var r = findSpan(k[i], id); if (r) return r; }\n return null;\n }\n function findPath(span, id, acc) {\n var p = (acc || []).concat([span]);\n if (span.spanId === id) return p;\n var k = span.children || [];\n for (var i = 0; i < k.length; i++) { var r = findPath(k[i], id, p); if (r) return r; }\n return null;\n }\n function countSpans(span) {\n var n = 1, k = span.children || [];\n for (var i = 0; i < k.length; i++) n += countSpans(k[i]);\n return n;\n }\n function traceSig(t) { return t.root.status + \"|\" + countSpans(t.root) + \"|\" + t.duration + \"|\" + (t.usage && t.usage.total); }\n // Sum every priced lane of a cost object into one USD number.\n function costSumObj(c) {\n if (!c) return 0;\n return (c.input || 0) + (c.output || 0) + (c.cachedInput || 0) + (c.cachedOutput || 0) + (c.reasoning || 0);\n }\n // The cost a single span directly carries (on its rolled-up usage).\n function usageCost(usage) { return usage ? costSumObj(usage.cost) : 0; }\n // Rollup-aware subtree cost: take a node's own cost when it has one\n // (it already rolls up its trips); otherwise sum the children. This\n // avoids double-counting on wrapper nodes (workflow/supervisor roots\n // carry tokens but no cost, so we descend to the priced agent/model).\n function rollupCost(span) {\n var own = usageCost(span.usage);\n if (own > 0) return own;\n var sum = 0;\n (span.children || []).forEach(function (c) { sum += rollupCost(c); });\n return sum;\n }\n function traceCost(t) {\n var explicit = costSumObj(t.cost);\n return explicit > 0 ? explicit : rollupCost(t.root);\n }\n // Format a USD amount; tiny per-node costs need more decimals to read.\n function money(n) {\n if (!n) return \"$0\";\n return \"$\" + (n < 0.01 ? n.toFixed(6) : n.toFixed(4));\n }\n\n // --- Client-side filtering / grouping / heatmap ----------------------\n // These mirror the pure, unit-tested helpers in trace-filter.ts. Keep\n // the two in sync: trace-filter.ts is the spec, this is its inlined twin.\n function anySelected(map) {\n for (var k in map) { if (map[k]) return true; }\n return false;\n }\n function matchesFilter(t) {\n var f = state.filter, root = t.root;\n if (f.errorsOnly && !ERROR_STATUSES[root.status]) return false;\n if (anySelected(f.statuses) && !f.statuses[root.status]) return false;\n if (anySelected(f.types) && !f.types[root.type]) return false;\n if (f.sessionId && t.sessionId !== f.sessionId) return false;\n if (f.promptKey && tracePromptKey(t) !== f.promptKey) return false;\n var text = (f.text || \"\").trim().toLowerCase();\n if (text) {\n var hay = (String(root.name) + \" \" + (t.sessionId || \"\")).toLowerCase();\n if (hay.indexOf(text) === -1) return false;\n }\n return true;\n }\n function filteredTraces() {\n var out = [];\n for (var i = 0; i < state.traces.length; i++) if (matchesFilter(state.traces[i])) out.push(state.traces[i]);\n return out;\n }\n function groupBySession(list) {\n var order = [], byKey = {};\n for (var i = 0; i < list.length; i++) {\n var key = list[i].sessionId || NO_SESSION_KEY;\n if (!byKey[key]) { byKey[key] = []; order.push(key); }\n byKey[key].push(list[i]);\n }\n return order.map(function (k) { return { sessionId: k, traces: byKey[k] }; });\n }\n // Group by prompt version (name@version) — the second group-by dimension\n // beside session. Mirrors groupByPrompt in trace-filter.ts. Unlinked runs\n // bucket under NO_PROMPT_KEY so they stay visible.\n function groupByPrompt(list) {\n var order = [], byKey = {};\n for (var i = 0; i < list.length; i++) {\n var key = tracePromptKey(list[i]) || NO_PROMPT_KEY;\n if (!byKey[key]) { byKey[key] = []; order.push(key); }\n byKey[key].push(list[i]);\n }\n return order.map(function (k) { return { groupKey: k, traces: byKey[k] }; });\n }\n // Group by root type (agent/workflow/supervisor/planner/…) — the coarsest\n // group-by dimension. Mirrors groupByType in trace-filter.ts. root.type is\n // always present, so the NO_TYPE_KEY bucket is only a defensive fallback.\n function typeGroups(list) {\n var order = [], byKey = {};\n for (var i = 0; i < list.length; i++) {\n var key = (list[i].root && list[i].root.type) || NO_TYPE_KEY;\n if (!byKey[key]) { byKey[key] = []; order.push(key); }\n byKey[key].push(list[i]);\n }\n return order.map(function (k) { return { groupKey: k, traces: byKey[k] }; });\n }\n // Inlined twins of percentile + aggregateByType in trace-filter.ts (the\n // spec). Power the per-type stats panel from the same filtered list the\n // trace view renders, so the panel honors active filters with no API call.\n function percentile(values, p) {\n if (!values.length) return 0;\n var sorted = values.slice().sort(function (a, b) { return a - b; });\n var rank = Math.ceil((p / 100) * sorted.length) - 1;\n var index = Math.min(Math.max(rank, 0), sorted.length - 1);\n return sorted[index];\n }\n function aggregateByType(list) {\n var order = [], byKey = {};\n for (var i = 0; i < list.length; i++) {\n var key = (list[i].root && list[i].root.type) || NO_TYPE_KEY;\n if (!byKey[key]) { byKey[key] = []; order.push(key); }\n byKey[key].push(list[i]);\n }\n return order.map(function (k) {\n var bucket = byKey[k];\n var durations = [], failed = 0, tokens = 0, cost = 0;\n for (var j = 0; j < bucket.length; j++) {\n var t = bucket[j];\n durations.push(t.duration);\n if (ERROR_STATUSES[t.root.status]) failed += 1;\n tokens += (t.usage && t.usage.total) || 0;\n cost += traceCost(t);\n }\n return {\n type: k, count: bucket.length, failed: failed,\n failRate: bucket.length ? failed / bucket.length : 0,\n p50: percentile(durations, 50), p95: percentile(durations, 95),\n tokens: tokens, cost: cost\n };\n });\n }\n // Per-type aggregate panel above the trace list. Honors the active filters\n // (computed over filteredTraces) and re-renders on every poll/filter tick.\n function renderStatsPanel() {\n var host = document.getElementById(\"stats-panel\");\n if (!host) return;\n var stats = state.showStats ? aggregateByType(filteredTraces()) : [];\n if (!stats.length) { host.innerHTML = \"\"; return; }\n var head = '<div class=\"strow sthead\">'\n + '<span class=\"stc-type\">Type</span><span>Count</span><span>Failed</span>'\n + '<span>p50</span><span>p95</span><span>Tokens</span><span>Cost</span></div>';\n var rows = stats.map(function (s) {\n var failTxt = s.failed\n ? s.failed + \" (\" + Math.round(s.failRate * 100) + \"%)\"\n : \"0\";\n return '<div class=\"strow\">'\n + '<span class=\"stc-type\">' + typeLabel(s.type) + \"</span>\"\n + '<span>' + num(s.count) + \"</span>\"\n + '<span class=\"stc-fail' + (s.failed ? \" fail\" : \"\") + '\">' + failTxt + \"</span>\"\n + '<span class=\"dim\">' + dur(s.p50) + \"</span>\"\n + '<span class=\"dim\">' + dur(s.p95) + \"</span>\"\n + '<span>' + num(s.tokens) + \"</span>\"\n + '<span class=\"cost\">' + money(s.cost) + \"</span>\"\n + \"</div>\";\n }).join(\"\");\n host.innerHTML = '<div class=\"stats-table\">' + head + rows + \"</div>\";\n }\n // Largest single-node rollup cost in a subtree — heatmap denominator.\n function maxNodeCost(span) {\n var max = rollupCost(span);\n (span.children || []).forEach(function (c) { var m = maxNodeCost(c); if (m > max) max = m; });\n return max;\n }\n // Intensity in [0,1] of a node's cost vs the trace max. Free trace → 0.\n function heatIntensity(nodeCost, maxCost) {\n if (maxCost <= 0 || nodeCost <= 0) return 0;\n var r = nodeCost / maxCost;\n return r > 1 ? 1 : r;\n }\n // The distinct sessionIds present across the polled traces, first-seen\n // order, capped so the chip row never overflows the toolbar.\n function presentSessions() {\n var seen = {}, out = [];\n for (var i = 0; i < state.traces.length && out.length < 12; i++) {\n var s = state.traces[i].sessionId;\n if (s && !seen[s]) { seen[s] = 1; out.push(s); }\n }\n return out;\n }\n // The root types actually present in the polled traces — so the type filter\n // chips show only what exists (no dead \"Tool\"/\"Model\"/\"Batch\"/… chips), in\n // canonical TYPE_FILTERS order. An active-but-aged-out selection stays so\n // the filter is never stranded with no chip to clear it.\n function presentTypes() {\n var seen = {};\n for (var i = 0; i < state.traces.length; i++) {\n var t = state.traces[i].root && state.traces[i].root.type;\n if (t) seen[t] = 1;\n }\n for (var k in state.filter.types) { if (state.filter.types[k]) seen[k] = 1; }\n var out = [];\n for (var j = 0; j < TYPE_FILTERS.length; j++) {\n if (seen[TYPE_FILTERS[j]]) { out.push(TYPE_FILTERS[j]); delete seen[TYPE_FILTERS[j]]; }\n }\n for (var x in seen) { out.push(x); }\n return out;\n }\n // The distinct prompt name@version keys present across the polled traces,\n // first-seen order, capped so the chip row never overflows the toolbar.\n function presentPrompts() {\n var seen = {}, out = [];\n for (var i = 0; i < state.traces.length && out.length < 12; i++) {\n var p = tracePromptKey(state.traces[i]);\n if (p && !seen[p]) { seen[p] = 1; out.push(p); }\n }\n return out;\n }\n\n // (S5) Markdown link URLs come from captured prompt/tool text — attacker-\n // influenced content — so only http:, https:, mailto: and relative/anchor\n // URLs may become an href; javascript:, data:, vbscript:, etc. must not.\n // The check runs on the URL as the browser will act on it: undo the\n // entities esc() introduced (the HTML parser decodes them exactly once in\n // the attribute), strip the control chars / whitespace browsers ignore\n // when parsing a scheme (java\\\\tscript:), and lowercase. Returns the\n // original (still-escaped) URL when safe, or null to drop the link.\n function sanitizeHref(url) {\n var probe = url.replace(/&(amp|lt|gt|quot|#39);/g, function (m, name) {\n return { amp: \"&\", lt: \"<\", gt: \">\", quot: '\"', \"#39\": \"'\" }[name];\n });\n probe = probe.replace(/[\\\\u0000-\\\\u0020\\\\u007f]+/g, \"\").toLowerCase();\n if (/^[a-z][a-z0-9+.-]*:/.test(probe)) {\n return /^(https?|mailto):/.test(probe) ? url : null;\n }\n if (/^[\\\\/\\\\\\\\]{2}/.test(probe)) return null; // scheme-relative smuggles a foreign host\n return url;\n }\n function mdInline(s) {\n s = s.replace(/\\\\*\\\\*([^*]+)\\\\*\\\\*/g, \"<strong>$1</strong>\");\n var codeRe = new RegExp(BT + \"([^\" + BT + \"]+)\" + BT, \"g\");\n s = s.replace(codeRe, \"<code>$1</code>\");\n s = s.replace(/\\\\[([^\\\\]]+)\\\\]\\\\(([^)]+)\\\\)/g, function (m, label, url) {\n var href = sanitizeHref(url);\n if (href === null) return label; // unsafe scheme: label as plain text, no <a>\n return '<a href=\"' + href + '\" target=\"_blank\" rel=\"noopener\">' + label + \"</a>\";\n });\n return s;\n }\n function mdToHtml(raw) {\n var src = esc(String(raw));\n var fence = BT + BT + BT;\n var html = \"\", idx = 0;\n while (true) {\n var start = src.indexOf(fence, idx);\n if (start === -1) { html += mdBlocks(src.slice(idx)); break; }\n html += mdBlocks(src.slice(idx, start));\n var nl = src.indexOf(\"\\\\n\", start + 3);\n var bodyStart = (nl === -1) ? start + 3 : nl + 1;\n var end = src.indexOf(fence, bodyStart);\n if (end === -1) { html += mdBlocks(src.slice(start)); break; }\n html += '<pre class=\"code\">' + src.slice(bodyStart, end).replace(/\\\\n$/, \"\") + \"</pre>\";\n idx = end + 3;\n }\n return html;\n }\n function mdBlocks(src) {\n var lines = src.split(\"\\\\n\"), html = \"\", inList = false;\n function closeList() { if (inList) { html += \"</ul>\"; inList = false; } }\n for (var i = 0; i < lines.length; i++) {\n var ln = lines[i];\n var h = ln.match(/^(#{1,4})\\\\s+(.*)$/);\n if (h) { closeList(); html += '<div class=\"md-h md-h' + h[1].length + '\">' + mdInline(h[2]) + \"</div>\"; continue; }\n var li = ln.match(/^\\\\s*[-*]\\\\s+(.*)$/);\n if (li) { if (!inList) { html += \"<ul>\"; inList = true; } html += \"<li>\" + mdInline(li[1]) + \"</li>\"; continue; }\n if (ln.trim() === \"\") { closeList(); continue; }\n closeList();\n html += '<div class=\"md-p\">' + mdInline(ln) + \"</div>\";\n }\n closeList();\n return html;\n }\n function renderKv(obj) {\n var keys = Object.keys(obj);\n if (!keys.length) return '<span class=\"dim\">{}</span>';\n return '<div class=\"kv\">' + keys.map(function (k) {\n var v = obj[k], vs;\n if (v === null || v === undefined) vs = '<span class=\"dim\">null</span>';\n else if (typeof v === \"object\") vs = '<pre class=\"mini\">' + esc(fmt(v)) + \"</pre>\";\n else vs = esc(String(v));\n return '<div class=\"kv-row\"><span class=\"kv-k\">' + esc(k) + '</span><span class=\"kv-v\">' + vs + \"</span></div>\";\n }).join(\"\") + \"</div>\";\n }\n function isMessageArray(v) {\n return Array.isArray(v) && v.length > 0 && v.every(function (m) { return m && typeof m === \"object\" && typeof m.role === \"string\"; });\n }\n function renderContent(c) {\n if (c == null) return '<span class=\"dim\">—</span>';\n if (typeof c === \"string\") return mdToHtml(c);\n if (Array.isArray(c)) return c.map(renderPart).join(\"\");\n if (typeof c === \"object\") return renderKv(c);\n return esc(String(c));\n }\n function renderPart(p) {\n if (p == null) return \"\";\n if (typeof p === \"string\") return mdToHtml(p);\n if (p.type === \"text\" && typeof p.text === \"string\") return mdToHtml(p.text);\n return '<div class=\"part\"><div class=\"part-label\">' + esc(p.type || \"part\") + \"</div>\" + renderKv(p) + \"</div>\";\n }\n function previewText(c) {\n if (c == null) return \"\";\n if (typeof c === \"string\") return c.replace(/\\\\s+/g, \" \").slice(0, 70);\n if (Array.isArray(c)) {\n for (var i = 0; i < c.length; i++) {\n var p = c[i];\n if (typeof p === \"string\") return p.slice(0, 70);\n if (p && p.type === \"text\" && p.text) return String(p.text).slice(0, 70);\n }\n return \"\";\n }\n try { return JSON.stringify(c).slice(0, 70); } catch (e) { return \"\"; }\n }\n function renderMsg(m) {\n var role = m.role || \"msg\";\n var body = (m.content !== undefined) ? renderContent(m.content) : renderKv(m);\n return '<div class=\"msg\"><div class=\"msg-role role-' + esc(role) + '\">' + esc(role) + '</div><div class=\"msg-content\">' + body + \"</div></div>\";\n }\n function renderMessages(arr) {\n var inner = '<div class=\"messages\">' + arr.map(renderMsg).join(\"\") + \"</div>\";\n // Short threads render inline; a long history (10-15+ messages) collapses\n // behind a <details> so it doesn't blow up the detail pane — the summary\n // shows the count + a preview of the latest message; click to expand.\n if (arr.length <= 6) return inner;\n var last = arr[arr.length - 1] || {};\n var preview = (last.role ? last.role + \": \" : \"\") + previewText(last.content);\n return '<details class=\"msg-collapse\"><summary><span class=\"msg-count\">' + arr.length\n + ' messages</span> <span class=\"dim\">' + esc(preview) + \"</span></summary>\" + inner + \"</details>\";\n }\n function smartValue(v) {\n if (v === null || v === undefined) return '<span class=\"dim\">—</span>';\n if (typeof v === \"string\") {\n var t = v.trim();\n if (t.charAt(0) === \"{\" || t.charAt(0) === \"[\") { try { return smartValue(JSON.parse(t)); } catch (e) {} }\n return mdToHtml(v);\n }\n if (isMessageArray(v)) return renderMessages(v);\n if (Array.isArray(v)) {\n return '<div class=\"kv\">' + v.map(function (item, i) {\n var vs = (item && typeof item === \"object\") ? renderKv(item) : esc(String(item));\n return '<div class=\"kv-row\"><span class=\"kv-k\">' + i + '</span><span class=\"kv-v\">' + vs + \"</span></div>\";\n }).join(\"\") + \"</div>\";\n }\n if (typeof v === \"object\") return renderKv(v);\n return esc(String(v));\n }\n\n function renderStats(a) {\n var cost = a.cost ? (((a.cost.input || 0) + (a.cost.output || 0) + (a.cost.cachedInput || 0) + (a.cost.cachedOutput || 0)).toFixed(4)) : \"—\";\n var u = a.usage || {};\n var cells = [\n [\"traces\", num(a.traces), null],\n [\"completed\", num(a.completed), null],\n [\"failed\", num(a.failed), null],\n [\"cancelled\", num(a.cancelled), null],\n [\"↓ tokens in\", num(u.input), \"var(--tok-in)\"],\n [\"↑ tokens out\", num(u.output), \"var(--tok-out)\"],\n [\"tokens total\", num(u.total), \"var(--tok-total)\"],\n [\"cost (usd)\", cost, null]\n ];\n document.getElementById(\"stats\").innerHTML = cells.map(function (c) {\n var st = c[2] ? ' style=\"color:' + c[2] + '\"' : \"\";\n return '<div class=\"stat\"><div class=\"label\"' + st + \">\" + c[0] + '</div><div class=\"value\"' + st + \">\" + esc(c[1]) + \"</div></div>\";\n }).join(\"\");\n }\n\n // Per-row heat accent: tint the left edge by the trace's own cost\n // relative to the most expensive trace currently in the (filtered) list.\n function heatStyle(intensity) {\n if (intensity <= 0) return \"\";\n var op = (0.12 + intensity * 0.88).toFixed(3);\n return ' style=\"--heat-op:' + op + '\"';\n }\n function heatBar(intensity) {\n if (intensity <= 0) return \"\";\n var op = (0.12 + intensity * 0.88).toFixed(3);\n return '<span class=\"heat\" style=\"opacity:' + op + '\" title=\"relative cost\"></span>';\n }\n function renderRow(t, maxTraceCost) {\n var sel = t.traceId === state.selectedId ? \" selected\" : \"\";\n var intensity = heatIntensity(traceCost(t), maxTraceCost);\n var pk = tracePromptKey(t);\n return '<div class=\"trace-row' + sel + '\" data-id=\"' + esc(t.traceId) + '\">'\n + heatBar(intensity)\n + typeLabel(t.root.type)\n + '<span class=\"badge ' + statusClass(t.root.status) + '\">' + esc(statusText(t.root.status)) + \"</span>\"\n + '<span class=\"rname\">' + esc(t.root.name) + \"</span>\"\n + (pk ? '<span class=\"rprompt\" title=\"prompt version\">' + esc(pk) + \"</span>\" : \"\")\n + (t.sessionId ? '<span class=\"dim\">' + esc(t.sessionId) + \"</span>\" : \"\")\n + '<span class=\"rright\">' + tokens(t.usage) + '<span class=\"dim\">· ' + dur(t.duration) + \"</span></span>\"\n + '<i class=\"chev\">›</i>'\n + \"</div>\";\n }\n function maxTraceCostOf(list) {\n var max = 0;\n for (var i = 0; i < list.length; i++) { var c = traceCost(list[i]); if (c > max) max = c; }\n return max;\n }\n // Render grouped buckets. \"groups\" is the normalized list each group-by\n // dimension produces — a groupKey + its traces. Shared by session, prompt,\n // and type. Optional \"labelFn\" maps the raw groupKey to a display label and\n // switches the header to the compact \"Label (N)\" form (used by type, whose\n // keys are friendly enums); session/prompt omit it and keep their raw id\n // plus the right-aligned \"N trace(s)\" count. data-group stays the RAW key\n // so collapse state keys consistently regardless of the display label.\n function renderGroups(groups, maxTraceCost, labelFn) {\n return groups.map(function (g) {\n var collapsed = !!state.collapsedGroups[g.groupKey];\n var idAndCount = labelFn\n ? '<span class=\"sgroup-id\">' + esc(labelFn(g.groupKey)) + \" (\" + g.traces.length + \")</span>\"\n : '<span class=\"sgroup-id\">' + esc(g.groupKey) + \"</span>\"\n + '<span class=\"sgroup-count\">' + g.traces.length + \" trace(s)</span>\";\n var head = '<div class=\"sgroup-head\" data-group=\"' + esc(g.groupKey) + '\">'\n + '<span class=\"sgroup-tw\">' + (collapsed ? \"▸\" : \"▾\") + \"</span>\"\n + idAndCount + \"</div>\";\n var body = collapsed ? \"\" : '<div class=\"sgroup-body\">'\n + g.traces.map(function (t) { return renderRow(t, maxTraceCost); }).join(\"\") + \"</div>\";\n return '<div class=\"sgroup\">' + head + body + \"</div>\";\n }).join(\"\");\n }\n // Normalize a session group ({sessionId,…}) to the shared {groupKey,…} shape.\n function sessionGroups(list) {\n return groupBySession(list).map(function (g) { return { groupKey: g.sessionId, traces: g.traces }; });\n }\n function renderList() {\n var list = filteredTraces();\n // Keep the per-type stats panel in sync — runs on every poll + filter\n // change, before the empty-state early return below.\n renderStatsPanel();\n var host = document.getElementById(\"traces\");\n if (!list.length) {\n host.innerHTML = state.traces.length\n ? '<div class=\"empty\">No traces match the current filters.</div>'\n : '<div class=\"empty\">No traces yet. Run an observed flow and they will appear here.</div>';\n return;\n }\n var maxTraceCost = maxTraceCostOf(list);\n // One grouping dimension renders at a time. The toggles are kept mutually\n // exclusive in their change handlers, so this precedence chain (most\n // specific → coarsest: prompt → session → type) only ever matches one.\n var html;\n if (state.groupByPrompt) html = renderGroups(groupByPrompt(list), maxTraceCost);\n else if (state.groupBySession) html = renderGroups(sessionGroups(list), maxTraceCost);\n else if (state.groupByType) html = renderGroups(typeGroups(list), maxTraceCost, typeText);\n else html = list.map(function (t) { return renderRow(t, maxTraceCost); }).join(\"\");\n host.innerHTML = html;\n document.getElementById(\"meta\").textContent = list.length + \" of \" + state.traces.length + \" trace(s) · live\";\n }\n\n // --- Filter UI rendering ---------------------------------------------\n function renderChips() {\n var f = state.filter;\n document.getElementById(\"status-chips\").innerHTML =\n '<span class=\"chip-group\"><span class=\"gl\">status</span>'\n + STATUS_FILTERS.map(function (s) {\n return '<button type=\"button\" class=\"chip' + (f.statuses[s] ? \" active\" : \"\") + '\" data-status=\"' + esc(s) + '\">' + esc(statusText(s)) + \"</button>\";\n }).join(\"\")\n + '<button type=\"button\" class=\"chip' + (f.errorsOnly ? \" active\" : \"\") + '\" data-errors=\"1\" title=\"Show only failed / cancelled traces\">Errors only</button>'\n + \"</span>\";\n var types = presentTypes();\n document.getElementById(\"type-chips\").innerHTML = types.length\n ? '<span class=\"chip-group\"><span class=\"gl\">type</span>'\n + types.map(function (ty) {\n return '<button type=\"button\" class=\"chip' + (f.types[ty] ? \" active\" : \"\") + '\" data-type=\"' + esc(ty) + '\">' + esc(typeText(ty)) + \"</button>\";\n }).join(\"\") + \"</span>\"\n : \"\";\n var sessions = presentSessions();\n document.getElementById(\"session-chips\").innerHTML = sessions.length\n ? '<span class=\"chip-group\"><span class=\"gl\">session</span>'\n + sessions.map(function (s) {\n return '<button type=\"button\" class=\"chip' + (f.sessionId === s ? \" active\" : \"\") + '\" data-session=\"' + esc(s) + '\">' + esc(s) + \"</button>\";\n }).join(\"\") + \"</span>\"\n : \"\";\n // Prompt-version filter chips — one per distinct name@version seen.\n // Hidden entirely until a named-prompt run shows up, so the toolbar stays\n // clean for projects that don't use the ai.prompts registry.\n var prompts = presentPrompts();\n document.getElementById(\"prompt-chips\").innerHTML = prompts.length\n ? '<span class=\"chip-group\"><span class=\"gl\">prompt</span>'\n + prompts.map(function (p) {\n return '<button type=\"button\" class=\"chip' + (f.promptKey === p ? \" active\" : \"\") + '\" data-prompt=\"' + esc(p) + '\">' + esc(p) + \"</button>\";\n }).join(\"\") + \"</span>\"\n : \"\";\n }\n\n // Heatmap denominator for the currently rendered trace tree. Set by\n // renderDrawer before each renderTree pass so node accents are scaled\n // against the most expensive node in this trace.\n var currentTreeMax = 0;\n function renderTree(span) {\n var hasKids = span.children && span.children.length;\n var collapsed = !!state.collapsed[span.spanId];\n var sel = span.spanId === state.selectedSpanId ? \" selected\" : \"\";\n var tw = hasKids ? (collapsed ? \"▸\" : \"▾\") : \"·\";\n var node = '<div class=\"tnode' + sel + '\" data-span=\"' + esc(span.spanId) + '\">'\n + heatBar(heatIntensity(rollupCost(span), currentTreeMax))\n + '<span class=\"twisty\"' + (hasKids ? ' data-toggle=\"' + esc(span.spanId) + '\"' : \"\") + \">\" + tw + \"</span>\"\n + typeLabel(span.type)\n + '<span class=\"tname\">' + esc(span.name) + \"</span>\"\n + statusDot(span.status)\n + '<span class=\"tmeta\">' + dur(span.duration) + \"</span>\"\n + \"</div>\";\n var kids = (hasKids && !collapsed) ? '<div class=\"tkids\">' + span.children.map(renderTree).join(\"\") + \"</div>\" : \"\";\n return node + kids;\n }\n\n // --- Timeline / waterfall (Gantt) ------------------------------------\n // Flatten the span tree to a depth-first ordered list, each entry\n // carrying its offset (ms from root start) + duration, so we can lay\n // out concurrency without re-walking. parseTs tolerates a missing/bad\n // startedAt by falling back to 0 so a bad clock never NaNs the bars.\n function parseTs(s) { var n = Date.parse(s); return isNaN(n) ? 0 : n; }\n // Render an ISO timestamp for the detail drawer as a readable, locale-\n // unambiguous local time — \"28 Jun 2026 03:16 PM\" (named month so there is\n // no M/D vs D/M confusion; minute precision). Falls back to the raw string\n // on a bad clock.\n var MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n function pad2(n) { return (n < 10 ? \"0\" : \"\") + n; }\n function fmtTs(iso) {\n if (!iso) return \"—\";\n var d = new Date(iso);\n if (isNaN(d.getTime())) return String(iso);\n var h = d.getHours();\n var ampm = h >= 12 ? \"PM\" : \"AM\";\n var h12 = h % 12;\n if (h12 === 0) h12 = 12;\n return d.getDate() + \" \" + MONTHS[d.getMonth()] + \" \" + d.getFullYear()\n + \" \" + pad2(h12) + \":\" + pad2(d.getMinutes()) + \" \" + ampm;\n }\n function flattenSpans(root) {\n var base = parseTs(root.startedAt);\n var rows = [];\n (function walk(span, depth) {\n var start = parseTs(span.startedAt) - base;\n if (start < 0) start = 0;\n var d = (span.duration != null) ? span.duration : 0;\n rows.push({ span: span, depth: depth, offset: start, duration: d, end: start + d });\n (span.children || []).forEach(function (c) { walk(c, depth + 1); });\n })(root, 0);\n return rows;\n }\n // Critical path: from the root, repeatedly step into the child whose\n // end time is latest (the one that pushed the parent's finish). Marks\n // the spans that determine total wall-clock time.\n function criticalPath(root) {\n var crit = {};\n (function walk(span) {\n crit[span.spanId] = 1;\n var kids = span.children || [];\n if (!kids.length) return;\n var pick = null, pe = -1;\n for (var i = 0; i < kids.length; i++) {\n var e = parseTs(kids[i].startedAt) + ((kids[i].duration != null) ? kids[i].duration : 0);\n if (e > pe) { pe = e; pick = kids[i]; }\n }\n if (pick) walk(pick);\n })(root);\n return crit;\n }\n function renderGantt(root) {\n var rows = flattenSpans(root);\n var span0 = rows.length ? rows[0] : null;\n var total = 0;\n rows.forEach(function (r) { if (r.end > total) total = r.end; });\n if (total <= 0) total = (span0 && span0.duration) || 1;\n var crit = criticalPath(root);\n var body = rows.map(function (r) {\n var leftPct = (r.offset / total) * 100;\n var widthPct = Math.max((r.duration / total) * 100, 0.6);\n var sel = r.span.spanId === state.selectedSpanId ? \" selected\" : \"\";\n var isCrit = crit[r.span.spanId] ? \" crit\" : \"\";\n var barClass = \"gbar bar-\" + statusClass(r.span.status) + (isCrit ? \" crit\" : \"\");\n var pad = \"padding-left:\" + (r.depth * 10) + \"px\";\n return '<div class=\"grow' + sel + '\" data-span=\"' + esc(r.span.spanId) + '\">'\n + '<span class=\"glabel\" style=\"' + pad + '\" title=\"' + esc(r.span.name) + '\">' + esc(r.span.name) + \"</span>\"\n + '<span class=\"gtrack\"><span class=\"' + barClass + '\" style=\"left:' + leftPct.toFixed(2) + \"%;width:\" + widthPct.toFixed(2) + '%\"></span></span>'\n + '<span class=\"gdur\">' + dur(r.duration) + \"</span>\"\n + \"</div>\";\n }).join(\"\");\n var legend = '<div class=\"gantt-legend\">'\n + '<span class=\"ck\"><span class=\"sw\" style=\"background:var(--fail)\"></span> critical path</span>'\n + '<span class=\"ck\"><span class=\"sw\" style=\"background:var(--ty-agent)\"></span> span (offset + duration)</span>'\n + \"</div>\";\n return '<div class=\"gantt\">' + (body || '<div class=\"dim\">No spans.</div>') + legend + \"</div>\";\n }\n\n // The name@version of the named prompt this span's run resolved, read\n // from the collector's prompt-version-linkage attributes. Returns null when\n // the run carried no named prompt. Mirrors tracePromptKey in trace-filter.ts.\n function spanPromptKey(span) {\n var a = span && span.attributes;\n if (!a || typeof a !== \"object\") return null;\n var name = a[\"agent.promptName\"];\n if (typeof name !== \"string\" || !name.length) return null;\n var ver = a[\"agent.promptVersion\"];\n var vl = (typeof ver === \"string\" && ver.length) ? ver : \"1\";\n return name + \"@\" + vl;\n }\n function tracePromptKey(t) { return spanPromptKey(t.root); }\n\n // Humanize a metadata key for display: split dot.notation + camelCase,\n // Title-case each word, upcase \"id\". e.g. \"supervisor.terminatedBy\" →\n // \"Supervisor Terminated By\", \"span id\" → \"Span ID\", \"agent.trips\" →\n // \"Agent Trips\". Underlying attribute keys are untouched.\n // NB: this whole script is a template literal — regex backslash classes\n // MUST be double-escaped (\\\\s, not \\s) or \"\\\\s\" collapses to a literal \"s\".\n function humanizeKey(key) {\n return String(key).split(/[.\\\\s]+/).map(function (seg) {\n return seg.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\").split(/\\\\s+/).map(function (w) {\n if (!w) return w;\n if (w.toLowerCase() === \"id\") return \"ID\";\n return w.charAt(0).toUpperCase() + w.slice(1);\n }).join(\" \");\n }).join(\" \");\n }\n\n function renderMeta(span, trace) {\n var rows = [];\n // Absolute wall-clock span — the head shows only the elapsed duration, so\n // surface when this node actually started/ended for log correlation.\n rows.push([\"started\", fmtTs(span.startedAt)]);\n rows.push([\"ended\", fmtTs(span.endedAt)]);\n var sid = span.sessionId || trace.sessionId;\n if (sid) rows.push([\"session\", sid]);\n // Prompt-version linkage: surface the resolved named prompt as one clean\n // name@version row right under session, so the panel reads it as a\n // first-class dimension rather than two raw attribute keys.\n var pk = spanPromptKey(span);\n if (pk) rows.push([\"prompt\", pk]);\n if (span.version) rows.push([\"version\", span.version]);\n rows.push([\"span id\", span.spanId]);\n if (span.parentSpanId) rows.push([\"parent\", span.parentSpanId]);\n rows.push([\"trace id\", span.traceId || trace.traceId]);\n var attrs = span.attributes;\n // Skip the two raw prompt keys — already shown as the clean prompt row.\n if (attrs && typeof attrs === \"object\") Object.keys(attrs).forEach(function (k) {\n if (k === \"agent.promptName\" || k === \"agent.promptVersion\") return;\n rows.push([k, attrs[k]]);\n });\n var kv = rows.map(function (r) {\n var v = r[1];\n var vs = (v && typeof v === \"object\") ? '<pre class=\"mini\">' + esc(fmt(v)) + \"</pre>\" : esc(String(v));\n return '<div class=\"kv-row\"><span class=\"kv-k\">' + esc(humanizeKey(r[0])) + '</span><span class=\"kv-v\">' + vs + \"</span></div>\";\n }).join(\"\");\n return '<div class=\"meta-sec\"><div class=\"meta-title\">metadata</div><div class=\"kv\">' + kv + \"</div></div>\";\n }\n\n // --- Evaluate: grade a span's last captured system prompt -------------\n // Config-gated (EVALUATE_ENABLED) — the drawer's only write action, POSTing\n // to a route that itself only exists when the server was configured with\n // evaluate. UI state lives in state.evaluate, keyed by spanId, so it\n // survives a re-render (e.g. switching to a sibling span and back).\n function extractLastSystemPrompt(span) {\n if (!Array.isArray(span.input)) return null;\n for (var i = span.input.length - 1; i >= 0; i--) {\n var m = span.input[i];\n if (m && typeof m === \"object\" && m.role === \"system\" && typeof m.content === \"string\") return m.content;\n }\n return null;\n }\n function evalState(spanId) {\n return state.evaluate[spanId] || (state.evaluate[spanId] = {\n open: false, instructions: EVALUATE_DEFAULT_INSTRUCTIONS, status: \"idle\", result: null, error: null\n });\n }\n function evalResultHtml(result) {\n var score = typeof result.score === \"number\" ? Math.round(result.score * 100) + \"%\" : \"n/a\";\n var issues = (result.issues || []).map(function (i) { return \"<li>\" + esc(i) + \"</li>\"; }).join(\"\");\n return '<div class=\"eval-result\"><span class=\"eval-score\">Score: ' + score + \"</span>\"\n + (issues ? '<ul class=\"eval-issues\">' + issues + \"</ul>\" : \"\") + \"</div>\";\n }\n function evalSectionHtml(span) {\n if (!EVALUATE_ENABLED) return \"\";\n var sysPrompt = extractLastSystemPrompt(span);\n if (!sysPrompt) return \"\";\n var st = evalState(span.spanId);\n var btn = '<button type=\"button\" class=\"eval-btn\" data-evaluate-toggle=\"' + esc(span.spanId) + '\">'\n + (st.open ? \"Hide evaluate\" : \"Evaluate system prompt\") + \"</button>\";\n if (!st.open) return btn;\n var running = st.status === \"running\";\n return btn + '<div class=\"eval-panel\">'\n + '<textarea id=\"eval-instructions\" placeholder=\"Grading instructions (optional — falls back to the configured default)\"' + (running ? \" disabled\" : \"\") + \">\" + esc(st.instructions || \"\") + \"</textarea>\"\n + '<div class=\"eval-actions\">'\n + '<button type=\"button\" class=\"eval-btn\" data-evaluate-run=\"' + esc(span.spanId) + '\"' + (running ? \" disabled\" : \"\") + \">\" + (running ? \"Evaluating…\" : \"Run\") + \"</button>\"\n + (st.error ? '<span class=\"eval-error\">' + esc(st.error) + \"</span>\" : \"\")\n + \"</div>\"\n + (st.result ? evalResultHtml(st.result) : \"\")\n + \"</div>\";\n }\n function rerenderDetail() {\n var t = findTrace(state.selectedId);\n if (!t) return;\n var sel = findSpan(t.root, state.selectedSpanId) || t.root;\n document.getElementById(\"drawer-detail\").innerHTML = renderDetail(sel, t);\n }\n function toggleEvalPanel(spanId) {\n evalState(spanId).open = !evalState(spanId).open;\n rerenderDetail();\n }\n function runEvaluate(traceId, spanId) {\n var st = evalState(spanId);\n var textarea = document.getElementById(\"eval-instructions\");\n if (textarea) st.instructions = textarea.value;\n st.status = \"running\"; st.error = null;\n rerenderDetail();\n fetchAuthed(API + \"/traces/\" + encodeURIComponent(traceId) + \"/spans/\" + encodeURIComponent(spanId) + \"/evaluate\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ instructions: st.instructions })\n }).then(function (r) {\n return r.json().then(function (data) { return { ok: r.ok, data: data }; });\n }).then(function (res) {\n if (res.ok) { st.status = \"done\"; st.result = res.data; st.error = null; }\n else { st.status = \"error\"; st.error = (res.data && (res.data.message || res.data.error)) || \"evaluate failed\"; st.result = null; }\n rerenderDetail();\n }).catch(function () {\n st.status = \"error\"; st.error = \"network error\"; st.result = null;\n rerenderDetail();\n });\n }\n\n function renderDetail(span, trace) {\n var path = findPath(trace.root, span.spanId) || [span];\n var crumb = path.map(function (p, i) { return (i ? '<span class=\"sep\"> › </span>' : \"\") + \"<span>\" + esc(p.name) + \"</span>\"; }).join(\"\");\n var io = \"\";\n if (span.input !== undefined) io += '<div class=\"piol\" style=\"color:var(--tok-in)\">input</div><div class=\"io-body\">' + smartValue(span.input) + \"</div>\";\n if (span.output !== undefined) io += '<div class=\"piol\" style=\"color:var(--tok-out)\">output</div><div class=\"io-body\">' + smartValue(span.output) + \"</div>\";\n if (span.error) io += '<div class=\"piol\" style=\"color:var(--fail)\">error</div><div class=\"io-body\">' + smartValue(span.error) + \"</div>\";\n\n // Tokens + cost line — omitted entirely when the node has neither\n // (e.g. a free tool with zero usage), so the panel stays uncluttered.\n var tk = tokens(span.usage);\n var c = rollupCost(span);\n var metaLine = (tk || c > 0)\n ? '<div style=\"margin-bottom:4px\">' + tk + (c > 0 ? '<span class=\"cost\">' + (tk ? \" · \" : \"\") + money(c) + \"</span>\" : \"\") + \"</div>\"\n : \"\";\n\n return '<div class=\"crumb\">' + crumb + \"</div>\"\n + '<div class=\"dhead-row\">' + typeLabel(span.type) + '<span style=\"font-weight:600\">' + esc(span.name) + \"</span>\"\n + '<span class=\"badge ' + statusClass(span.status) + '\">' + esc(statusText(span.status)) + \"</span>\"\n + '<span class=\"dim\">· ' + dur(span.duration) + \"</span></div>\"\n + metaLine\n + io\n + evalSectionHtml(span, trace)\n + renderMeta(span, trace);\n }\n\n function headHtml(t) {\n var cost = traceCost(t);\n return typeLabel(t.root.type)\n + '<span class=\"badge ' + statusClass(t.root.status) + '\">' + esc(statusText(t.root.status)) + \"</span>\"\n + '<span style=\"font-weight:600\">' + esc(t.root.name) + \"</span>\"\n + '<span class=\"dim\">· ' + dur(t.duration) + \"</span>\"\n + tokens(t.usage)\n + (cost > 0 ? '<span class=\"cost\">· ' + money(cost) + \"</span>\" : \"\")\n + '<button class=\"drawer-close\" type=\"button\" title=\"Close (Esc)\">✕ Close</button>';\n }\n\n function viewSwitcher() {\n var tree = state.view === \"tree\" ? \" active\" : \"\";\n var tl = state.view === \"timeline\" ? \" active\" : \"\";\n return '<div class=\"dview\" id=\"drawer-view\">'\n + '<button type=\"button\" class=\"' + tree.trim() + '\" data-view=\"tree\">Tree</button>'\n + '<button type=\"button\" class=\"' + tl.trim() + '\" data-view=\"timeline\">Timeline</button>'\n + '<span class=\"legend\" title=\"Node colour = relative cost\"><span>cost</span><span class=\"grad\"></span></span>'\n + \"</div>\";\n }\n function leftPaneHtml(t) {\n if (state.view === \"timeline\") return renderGantt(t.root);\n currentTreeMax = maxNodeCost(t.root);\n return renderTree(t.root);\n }\n function renderDrawer(t) {\n document.getElementById(\"drawer-head\").innerHTML = headHtml(t);\n var sel = findSpan(t.root, state.selectedSpanId) || t.root;\n document.getElementById(\"drawer-body\").innerHTML =\n '<div class=\"dsplit\"><div class=\"dtree\" id=\"drawer-tree\">' + viewSwitcher() + leftPaneHtml(t) + \"</div>\"\n + '<div class=\"ddetail\" id=\"drawer-detail\">' + renderDetail(sel, t) + \"</div></div>\";\n }\n // Re-render only the left pane (after a view switch) without disturbing\n // the detail panel or scroll position of the detail side.\n function renderLeftPane() {\n var t = findTrace(state.selectedId);\n if (!t) return;\n document.getElementById(\"drawer-tree\").innerHTML = viewSwitcher() + leftPaneHtml(t);\n }\n\n function selectSpan(id) {\n state.selectedSpanId = id;\n var t = findTrace(state.selectedId);\n if (!t) return;\n document.getElementById(\"drawer-detail\").innerHTML = renderDetail(findSpan(t.root, id) || t.root, t);\n // Highlight in whichever left pane is active (tree nodes or Gantt rows).\n var nodes = document.querySelectorAll(\".tnode[data-span], .grow[data-span]\");\n for (var i = 0; i < nodes.length; i++) {\n if (nodes[i].getAttribute(\"data-span\") === id) nodes[i].classList.add(\"selected\");\n else nodes[i].classList.remove(\"selected\");\n }\n writeHash();\n }\n function toggleSpan(id) {\n state.collapsed[id] = !state.collapsed[id];\n var t = findTrace(state.selectedId);\n if (t) renderLeftPane();\n }\n function setView(v) {\n if (state.view === v) return;\n state.view = v;\n renderLeftPane();\n }\n\n function openTrace(id, spanId) {\n state.selectedId = id;\n var t = findTrace(id);\n if (t) {\n state.selectedSpanId = (spanId && findSpan(t.root, spanId)) ? spanId : t.root.spanId;\n state.collapsed = {};\n renderDrawer(t);\n state.sig = traceSig(t);\n }\n document.getElementById(\"drawer\").classList.add(\"open\");\n document.getElementById(\"drawer\").setAttribute(\"aria-hidden\", \"false\");\n document.getElementById(\"backdrop\").classList.add(\"open\");\n markSelectedRow();\n writeHash();\n }\n function closeDrawer() {\n state.selectedId = null; state.selectedSpanId = null; state.sig = null;\n document.getElementById(\"drawer\").classList.remove(\"open\");\n document.getElementById(\"drawer\").setAttribute(\"aria-hidden\", \"true\");\n document.getElementById(\"backdrop\").classList.remove(\"open\");\n markSelectedRow();\n writeHash();\n }\n\n // --- Deep-links: reflect the open trace + span in the URL hash -------\n // #trace=<id>&span=<id>. Written on open/close/select; read on load and\n // on manual hash edits (back/forward). A guard flag stops writeHash from\n // re-triggering our own hashchange handler in a loop.\n var suppressHash = false;\n function writeHash() {\n var h = \"\";\n if (state.selectedId) {\n h = \"#trace=\" + encodeURIComponent(state.selectedId);\n if (state.selectedSpanId && state.selectedSpanId !== state.selectedId) {\n h += \"&span=\" + encodeURIComponent(state.selectedSpanId);\n }\n }\n suppressHash = true;\n try {\n if (history && history.replaceState) history.replaceState(null, \"\", h || (location.pathname + location.search));\n else location.hash = h;\n } catch (e) { location.hash = h; }\n suppressHash = false;\n }\n function readHash() {\n var raw = (location.hash || \"\").replace(/^#/, \"\");\n var out = { trace: null, span: null };\n raw.split(\"&\").forEach(function (kv) {\n var i = kv.indexOf(\"=\");\n if (i === -1) return;\n var k = kv.slice(0, i), v = decodeURIComponent(kv.slice(i + 1));\n if (k === \"trace\") out.trace = v;\n else if (k === \"span\") out.span = v;\n });\n return out;\n }\n // Open whatever the hash points at, if that trace is loaded. Returns\n // true when it acted so the caller can mark the one-time load as done.\n function applyHash() {\n var h = readHash();\n if (!h.trace) {\n if (state.selectedId) closeDrawer();\n return true;\n }\n if (!findTrace(h.trace)) return false; // not polled yet — retry next poll\n openTrace(h.trace, h.span || undefined);\n return true;\n }\n function markSelectedRow() {\n var rows = document.querySelectorAll(\".trace-row\");\n for (var i = 0; i < rows.length; i++) {\n if (rows[i].getAttribute(\"data-id\") === state.selectedId) rows[i].classList.add(\"selected\");\n else rows[i].classList.remove(\"selected\");\n }\n }\n\n document.getElementById(\"traces\").addEventListener(\"click\", function (e) {\n var head = e.target.closest ? e.target.closest(\".sgroup-head\") : null;\n if (head) {\n var g = head.getAttribute(\"data-group\");\n state.collapsedGroups[g] = !state.collapsedGroups[g];\n renderList();\n return;\n }\n var row = e.target.closest ? e.target.closest(\".trace-row\") : null;\n if (row) openTrace(row.getAttribute(\"data-id\"));\n });\n document.getElementById(\"drawer-head\").addEventListener(\"click\", function (e) {\n if (e.target.closest && e.target.closest(\".drawer-close\")) closeDrawer();\n });\n document.getElementById(\"drawer-body\").addEventListener(\"click\", function (e) {\n var vb = e.target.closest ? e.target.closest(\"[data-view]\") : null;\n if (vb) { setView(vb.getAttribute(\"data-view\")); return; }\n var tog = e.target.closest ? e.target.closest(\"[data-toggle]\") : null;\n if (tog) { toggleSpan(tog.getAttribute(\"data-toggle\")); return; }\n var evalToggle = e.target.closest ? e.target.closest(\"[data-evaluate-toggle]\") : null;\n if (evalToggle) { toggleEvalPanel(evalToggle.getAttribute(\"data-evaluate-toggle\")); return; }\n var evalRun = e.target.closest ? e.target.closest(\"[data-evaluate-run]\") : null;\n if (evalRun && state.selectedId) { runEvaluate(state.selectedId, evalRun.getAttribute(\"data-evaluate-run\")); return; }\n var node = e.target.closest ? e.target.closest(\".tnode[data-span], .grow[data-span]\") : null;\n if (node) selectSpan(node.getAttribute(\"data-span\"));\n });\n // Track the instructions textarea live (no re-render on keystroke, so\n // typing never loses focus/cursor position).\n document.getElementById(\"drawer-body\").addEventListener(\"input\", function (e) {\n if (e.target && e.target.id === \"eval-instructions\" && state.selectedSpanId) {\n evalState(state.selectedSpanId).instructions = e.target.value;\n }\n });\n document.getElementById(\"backdrop\").addEventListener(\"click\", closeDrawer);\n document.addEventListener(\"keydown\", function (e) { if (e.key === \"Escape\" || e.keyCode === 27) closeDrawer(); });\n\n // --- Filter / toolbar wiring -----------------------------------------\n function toggleMapKey(map, key) { if (map[key]) delete map[key]; else map[key] = 1; }\n document.getElementById(\"status-chips\").addEventListener(\"click\", function (e) {\n if (!e.target.closest) return;\n // The \"Errors only\" shortcut chip lives in the status group now.\n if (e.target.closest(\"[data-errors]\")) {\n state.filter.errorsOnly = !state.filter.errorsOnly;\n renderChips(); renderList();\n return;\n }\n var b = e.target.closest(\"[data-status]\");\n if (!b) return;\n toggleMapKey(state.filter.statuses, b.getAttribute(\"data-status\"));\n renderChips(); renderList();\n });\n document.getElementById(\"type-chips\").addEventListener(\"click\", function (e) {\n var b = e.target.closest ? e.target.closest(\"[data-type]\") : null;\n if (!b) return;\n toggleMapKey(state.filter.types, b.getAttribute(\"data-type\"));\n renderChips(); renderList();\n });\n document.getElementById(\"session-chips\").addEventListener(\"click\", function (e) {\n var b = e.target.closest ? e.target.closest(\"[data-session]\") : null;\n if (!b) return;\n var s = b.getAttribute(\"data-session\");\n state.filter.sessionId = (state.filter.sessionId === s) ? null : s;\n renderChips(); renderList();\n });\n document.getElementById(\"prompt-chips\").addEventListener(\"click\", function (e) {\n var b = e.target.closest ? e.target.closest(\"[data-prompt]\") : null;\n if (!b) return;\n var p = b.getAttribute(\"data-prompt\");\n state.filter.promptKey = (state.filter.promptKey === p) ? null : p;\n renderChips(); renderList();\n });\n document.getElementById(\"search\").addEventListener(\"input\", function (e) {\n state.filter.text = e.target.value || \"\";\n renderList();\n });\n // One mutually-exclusive grouping dimension, chosen from the Group dropdown.\n // \"\" = no grouping; renderList's precedence chain only ever matches one.\n // Switching dimensions drops stale collapsed-header keys.\n function setGrouping(dim) {\n state.groupBySession = dim === \"session\";\n state.groupByPrompt = dim === \"prompt\";\n state.groupByType = dim === \"type\";\n state.collapsedGroups = {};\n renderList();\n }\n document.getElementById(\"group-by\").addEventListener(\"change\", function (e) {\n setGrouping(e.target.value);\n });\n document.getElementById(\"show-stats\").addEventListener(\"change\", function (e) {\n state.showStats = !!e.target.checked;\n renderStatsPanel();\n });\n document.getElementById(\"clear-filters\").addEventListener(\"click\", function () {\n state.filter = { text: \"\", statuses: {}, types: {}, sessionId: null, promptKey: null, errorsOnly: false };\n document.getElementById(\"search\").value = \"\";\n renderChips(); renderList();\n });\n\n // Back/forward or a manual hash edit re-syncs the open trace/span.\n window.addEventListener(\"hashchange\", function () {\n if (suppressHash) return;\n applyHash();\n });\n\n var THEME_KEY = \"panoptic-theme\";\n var mql = window.matchMedia ? window.matchMedia(\"(prefers-color-scheme: light)\") : null;\n function applyTheme(mode) {\n try { localStorage.setItem(THEME_KEY, mode); } catch (e) {}\n var light = mode === \"light\" || (mode === \"system\" && mql && mql.matches);\n document.documentElement.setAttribute(\"data-theme\", light ? \"light\" : \"dark\");\n var btns = document.querySelectorAll(\"[data-theme-set]\");\n for (var i = 0; i < btns.length; i++) btns[i].classList.toggle(\"active\", btns[i].getAttribute(\"data-theme-set\") === mode);\n }\n document.getElementById(\"theme\").addEventListener(\"click\", function (e) {\n var b = e.target.closest ? e.target.closest(\"[data-theme-set]\") : null;\n if (b) applyTheme(b.getAttribute(\"data-theme-set\"));\n });\n if (mql && mql.addEventListener) mql.addEventListener(\"change\", function () {\n var cur = \"system\";\n try { cur = localStorage.getItem(THEME_KEY) || \"system\"; } catch (e) {}\n if (cur === \"system\") applyTheme(\"system\");\n });\n var savedTheme = \"system\";\n try { savedTheme = localStorage.getItem(THEME_KEY) || \"system\"; } catch (e) {}\n applyTheme(savedTheme);\n\n function poll() {\n Promise.all([\n fetchAuthed(API + \"/aggregate\").then(function (r) { return r.json(); }),\n fetchAuthed(API + \"/traces\").then(function (r) { return r.json(); })\n ]).then(function (res) {\n renderStats(res[0]);\n state.traces = res[1] || [];\n document.getElementById(\"meta\").textContent = state.traces.length + \" trace(s) · live\";\n renderChips();\n renderList();\n // Open whatever the URL hash deep-links to, once the target trace\n // has actually arrived in a poll (it may not be in the first batch).\n if (!state.hashApplied) {\n if (applyHash()) state.hashApplied = true;\n } else if (state.selectedId) {\n var t = findTrace(state.selectedId);\n if (t) { var sig = traceSig(t); if (sig !== state.sig) { renderDrawer(t); state.sig = sig; } }\n }\n }).catch(function (e) {\n document.getElementById(\"meta\").textContent = \"disconnected\";\n });\n }\n\n // FOLLOW-UP: a live socket tail (SSE / WebSocket push) is out of scope\n // for this pass; the dashboard stays on the 2s JSON poll below. When\n // added, it should reuse renderList/renderDrawer and keep the poll as a\n // reconnect fallback.\n poll();\n setInterval(poll, 2000);\n})();\n</script>\n</body>\n</html>`;\n}\n\n/** Escape a string for safe interpolation into static HTML text. */\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Encode free-form text (e.g. `evaluate.instructions`, which an operator\n * could type anything into) as a JS EXPRESSION that reconstructs it at\n * runtime, WITHOUT ever emitting a literal backtick into the served page —\n * the client script is itself built from a TS template literal, so a raw\n * backtick in the output would be a real syntax hazard (see the `BT =\n * String.fromCharCode(96)` construction already in the client script for\n * the same reason). `JSON.stringify` alone doesn't escape backticks (they\n * aren't JSON-significant), so a value containing one is split around it\n * and rejoined with the client's own `BT` constant. No backticks in the\n * input ⇒ a single plain `JSON.stringify(value)` — no unnecessary\n * concatenation in the common case.\n */\nfunction encodeForInlineScript(value: string): string {\n return value\n .split(\"`\")\n .map(part => JSON.stringify(part))\n .join(\" + BT + \");\n}\n","import { timingSafeEqual } from \"node:crypto\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { EvaluateConfig } from \"../evaluate/evaluate.type\";\nimport { evaluateSystemPrompt } from \"../evaluate/evaluate-system-prompt\";\nimport { extractLastSystemPrompt } from \"../evaluate/extract-last-system-prompt\";\nimport { findSpanById } from \"../evaluate/find-span-by-id\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport { parseQuery } from \"./parse-query\";\nimport { dashboardHtml } from \"./ui.html\";\n\n/** Fully-resolved routing config the request handler closes over. */\nexport type ServeConfig = {\n /** Normalized mount path, always ending in `/` (e.g. `\"/\"`). */\n basePath: string;\n /** Header title baked into the served page. */\n title: string;\n /** Bearer token required on every request when set (S4). */\n authToken?: string;\n /** `Host` header allowlist — defends against DNS-rebinding (S4). */\n allowedHosts: string[];\n /**\n * Enables the one write route: `POST\n * {basePath}api/traces/:traceId/spans/:spanId/evaluate`. Absent ⇒ the\n * route 405s (POST isn't accepted by any route) and the served page never\n * renders the Evaluate button.\n */\n evaluate?: EvaluateConfig;\n};\n\n/** Request body accepted by the evaluate route — everything optional. */\ntype EvaluateRequestBody = {\n /** Per-run instructions override; falls back to `config.evaluate.instructions`. */\n instructions?: string;\n};\n\n/** Hard cap on the evaluate route's request body — well beyond a real instructions string. */\nconst MAX_EVALUATE_BODY_BYTES = 64 * 1024;\n\n/**\n * Security response headers added to every dashboard response (S4):\n * block MIME-sniffing, framing, referrer leakage, and lock the page's\n * content sources down to itself (the UI is fully self-contained — no CDN).\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"no-referrer\",\n \"content-security-policy\":\n \"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\",\n};\n\n/** Extract the host (no port) from a `Host` header value. */\nfunction hostHeaderName(hostHeader: string | undefined): string | undefined {\n if (!hostHeader) return undefined;\n // IPv6 literal `[::1]:4319` → `[::1]`; otherwise strip `:port`.\n if (hostHeader.startsWith(\"[\")) {\n return hostHeader.slice(0, hostHeader.indexOf(\"]\") + 1);\n }\n const colon = hostHeader.indexOf(\":\");\n return colon === -1 ? hostHeader : hostHeader.slice(0, colon);\n}\n\n/**\n * Constant-time string equality (S4) — guards against a timing\n * side-channel that could otherwise let a network-adjacent attacker\n * recover the token byte-by-byte via repeated timed guesses. Plain `===`\n * short-circuits on the first differing byte, so response latency leaks\n * how many leading bytes matched; `timingSafeEqual` does not. Lengths are\n * compared first (a length mismatch is not secret and `timingSafeEqual`\n * requires equal-length buffers anyway).\n */\nfunction constantTimeEqual(a: string, b: string): boolean {\n const bufA = Buffer.from(a);\n const bufB = Buffer.from(b);\n\n if (bufA.length !== bufB.length) return false;\n\n return timingSafeEqual(bufA, bufB);\n}\n\n/**\n * Bearer-token check (S4): `Authorization: Bearer <token>` header, or\n * (only when `allowQueryToken`) a `?token=` query param.\n *\n * The query-string form only exists for the one request that structurally\n * cannot carry a custom header — the initial browser navigation that\n * loads the HTML shell (a typed/clicked/bookmarked URL). Every other\n * request the served page makes is same-origin `fetch()`, which can and\n * does set `Authorization` (see `ui.html.ts`'s `fetchAuthed`), so the API\n * routes never need to accept a query-string token. Restricting the\n * fallback to just the page route minimizes the token's exposure in\n * server access logs, proxy logs, and browser history to a single route\n * instead of every poll.\n */\nfunction isAuthorized(\n req: IncomingMessage,\n url: URL,\n token: string,\n allowQueryToken: boolean,\n): boolean {\n const header = req.headers.authorization;\n if (header && constantTimeEqual(header, `Bearer ${token}`)) return true;\n if (!allowQueryToken) return false;\n const queryToken = url.searchParams.get(\"token\");\n return queryToken !== null && constantTimeEqual(queryToken, token);\n}\n\n/**\n * Build the `node:http` request handler for the dashboard over a given\n * trace store. Kept separate from the server lifecycle so it can be unit\n * tested by feeding it a fake `req`/`res` without binding a port.\n *\n * Routes (all under `config.basePath`):\n *\n * - `GET api/traces` → `store.query(parseQuery(searchParams))`\n * - `GET api/traces/:id` → `store.get(id)` or `404`\n * - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`\n * - `GET {basePath}` → the self-contained HTML page\n * - `POST api/traces/:traceId/spans/:spanId/evaluate` → ONLY when\n * `config.evaluate` is set; grades the span's last captured system\n * prompt and returns a {@link EvaluateVerdict}. The dashboard's one\n * write route — absent config, POST 405s like it would against any\n * other route (the path is never even pattern-matched).\n *\n * Anything else → `404`. A method the matched route doesn't accept → `405`.\n * Host allowlist + bearer-token auth (S4) are checked for EVERY request,\n * regardless of method, before any routing. The store shapes\n * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so GET responses\n * are a plain `JSON.stringify` with no serializer.\n *\n * @example\n * const handler = createRequestHandler(store, { basePath: \"/\", title: \"Panoptic\" });\n * http.createServer(handler).listen(4319, \"127.0.0.1\");\n */\nexport function createRequestHandler(\n store: TraceStoreContract,\n config: ServeConfig,\n): (req: IncomingMessage, res: ServerResponse) => void {\n const base = config.basePath;\n const apiPrefix = `${base}api`;\n\n return function handle(req: IncomingMessage, res: ServerResponse): void {\n // `req.url` is path + query only; a dummy origin lets URL parse it.\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n const pathname = url.pathname;\n\n // Host-header allowlist — blocks DNS-rebinding attacks that point a\n // hostile domain at this loopback port (S4). Checked for every method.\n const host = hostHeaderName(req.headers.host);\n if (!host || !config.allowedHosts.includes(host.toLowerCase())) {\n sendJson(res, 403, { error: \"host_not_allowed\" });\n\n return;\n }\n\n // Bearer-token auth when configured (always required off loopback, S4).\n // `?token=` is only honored on the HTML page route — see `isAuthorized`.\n const isPageRoute = pathname === base || pathname === base.replace(/\\/$/, \"\");\n if (config.authToken && !isAuthorized(req, url, config.authToken, isPageRoute)) {\n sendJson(res, 401, { error: \"unauthorized\" });\n\n return;\n }\n\n // The one write route — opt-in via `config.evaluate`, so it must be\n // checked before the blanket GET-only gate below.\n if (req.method === \"POST\" && config.evaluate) {\n const match = matchEvaluatePath(pathname, apiPrefix);\n\n if (match) {\n void handleEvaluate(store, config.evaluate, match.traceId, match.spanId, req, res);\n\n return;\n }\n }\n\n if (req.method !== \"GET\") {\n sendJson(res, 405, { error: \"method_not_allowed\" });\n\n return;\n }\n\n if (pathname === `${apiPrefix}/traces`) {\n sendJson(res, 200, store.query(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname.startsWith(`${apiPrefix}/traces/`)) {\n const traceId = decodeURIComponent(pathname.slice(`${apiPrefix}/traces/`.length));\n const trace = traceId.length > 0 ? store.get(traceId) : undefined;\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n sendJson(res, 200, trace);\n\n return;\n }\n\n if (pathname === `${apiPrefix}/aggregate`) {\n sendJson(res, 200, store.aggregate(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname === base || pathname === base.replace(/\\/$/, \"\")) {\n res.writeHead(200, {\n \"content-type\": \"text/html; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(dashboardHtml(base, config.title, Boolean(config.evaluate), config.evaluate?.instructions ?? \"\"));\n\n return;\n }\n\n sendJson(res, 404, { error: \"not_found\" });\n };\n}\n\n/** Write a JSON response with the given status code and security headers. */\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n \"content-type\": \"application/json; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(JSON.stringify(body));\n}\n\n/**\n * Match `{apiPrefix}/traces/:traceId/spans/:spanId/evaluate` — the one\n * write route. Returns `undefined` for anything else, including the plain\n * `{apiPrefix}/traces/:id` read route (no `/spans/.../evaluate` suffix),\n * so the two never collide.\n */\nfunction matchEvaluatePath(\n pathname: string,\n apiPrefix: string,\n): { traceId: string; spanId: string } | undefined {\n const prefix = `${apiPrefix}/traces/`;\n\n if (!pathname.startsWith(prefix)) {\n return undefined;\n }\n\n const match = /^([^/]+)\\/spans\\/([^/]+)\\/evaluate$/.exec(pathname.slice(prefix.length));\n\n if (!match) {\n return undefined;\n }\n\n return {\n traceId: decodeURIComponent(match[1]),\n spanId: decodeURIComponent(match[2]),\n };\n}\n\n/**\n * Collect and JSON-parse a request body, capped at\n * {@link MAX_EVALUATE_BODY_BYTES} so an oversized body can't hold the\n * connection open indefinitely. An empty body resolves to `undefined` —\n * the evaluate route treats that as \"no per-run override\".\n */\nfunction readJsonBody<T>(req: IncomingMessage): Promise<T | undefined> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n\n if (size > MAX_EVALUATE_BODY_BYTES) {\n req.destroy();\n reject(new Error(\"payload_too_large\"));\n\n return;\n }\n\n chunks.push(chunk);\n });\n\n req.on(\"end\", () => {\n if (chunks.length === 0) {\n resolve(undefined);\n\n return;\n }\n\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString(\"utf-8\")) as T);\n } catch {\n reject(new Error(\"invalid_json\"));\n }\n });\n\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Handle `POST {apiPrefix}/traces/:traceId/spans/:spanId/evaluate`. Looks\n * up the trace + span, extracts its last captured system prompt, and\n * grades it via {@link evaluateSystemPrompt}. `judgePromptBody` itself\n * never throws (a broken judge degrades to an issues-only outcome) — the\n * try/catch here only guards `config.evaluate.model` resolution, the one\n * step that CAN throw (e.g. a factory constructing an SDK client).\n */\nasync function handleEvaluate(\n store: TraceStoreContract,\n evaluate: EvaluateConfig,\n traceId: string,\n spanId: string,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const trace = store.get(traceId);\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n const span = findSpanById(trace.root, spanId);\n\n if (span === undefined) {\n sendJson(res, 404, { error: \"span_not_found\", spanId });\n\n return;\n }\n\n const systemPrompt = extractLastSystemPrompt(span);\n\n if (systemPrompt === undefined) {\n sendJson(res, 422, { error: \"no_system_prompt\" });\n\n return;\n }\n\n let body: EvaluateRequestBody | undefined;\n\n try {\n body = await readJsonBody<EvaluateRequestBody>(req);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"invalid_json\";\n sendJson(res, message === \"payload_too_large\" ? 413 : 400, { error: message });\n\n return;\n }\n\n try {\n const verdict = await evaluateSystemPrompt(systemPrompt, evaluate, body?.instructions);\n sendJson(res, 200, verdict);\n } catch (error) {\n sendJson(res, 502, {\n error: \"evaluate_failed\",\n message: error instanceof Error ? error.message : String(error),\n });\n }\n}\n","import { createServer } from \"node:http\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport type { DashboardHandle, DashboardOptions } from \"./dashboard.type\";\nimport { createRequestHandler } from \"./serve\";\n\nconst DEFAULT_PORT = 4319;\nconst DEFAULT_HOST = \"127.0.0.1\";\nconst DEFAULT_TITLE = \"Panoptic\";\n\n/**\n * Start the local Panoptic dashboard over a {@link TraceStoreContract} —\n * a zero-dependency `node:http` server that serves a read-only JSON API\n * and one self-contained HTML page polling it. The store is the live\n * object the collector fills, so each poll reflects the latest completed\n * traces with no extra wiring.\n *\n * This is a low-level building block. The documented path is\n * `ai.config({ panoptic: { dashboard } })`, which constructs (or reuses)\n * the store and calls this for you. Use it directly only when you manage\n * the store yourself.\n *\n * Binds loopback-only by default (`127.0.0.1`) so prompt content is never\n * exposed to the LAN. Pass `port: 0` for an ephemeral port — the resolved\n * port comes back on the handle. A port already in use rejects with a\n * clear, actionable `Error` rather than the raw `EADDRINUSE`.\n *\n * @example\n * const store = createInMemoryTraceStore();\n * const handle = await dashboard(store, { port: 4319, open: true });\n * console.log(handle.url); // http://127.0.0.1:4319/\n * // ...later:\n * await handle.close();\n */\nexport function dashboard(\n store: TraceStoreContract,\n options: DashboardOptions = {},\n): Promise<DashboardHandle> {\n const port = options.port ?? DEFAULT_PORT;\n const host = options.host ?? DEFAULT_HOST;\n const title = options.title ?? DEFAULT_TITLE;\n const basePath = normalizeBasePath(options.basePath);\n\n // Secure-by-default off loopback (S4): a non-loopback bind without an\n // auth token would expose raw prompt content to the network, so refuse\n // to start rather than silently exposing it.\n if (!isLoopbackHost(host) && !options.authToken) {\n return Promise.reject(\n new Error(\n `Panoptic dashboard: binding to a non-loopback host (\"${host}\") requires an \\`authToken\\` ` +\n \"(the dashboard exposes raw prompt content). Pass `authToken`, or bind to 127.0.0.1.\",\n ),\n );\n }\n\n const allowedHosts = (options.allowedHosts ?? defaultAllowedHosts(host)).map(h =>\n h.toLowerCase(),\n );\n\n const handler = createRequestHandler(store, {\n basePath,\n title,\n authToken: options.authToken,\n allowedHosts,\n evaluate: options.evaluate,\n });\n const server = createServer(handler);\n\n return new Promise<DashboardHandle>((resolve, reject) => {\n const onError = (error: NodeJS.ErrnoException): void => {\n server.off(\"error\", onError);\n\n if (error.code === \"EADDRINUSE\") {\n reject(\n new Error(\n `Panoptic dashboard: port ${port} in use; pass { port: 0 } for an ephemeral port`,\n ),\n );\n\n return;\n }\n\n reject(error);\n };\n\n server.on(\"error\", onError);\n\n server.listen(port, host, () => {\n server.off(\"error\", onError);\n\n const address = server.address();\n const resolvedPort = typeof address === \"object\" && address !== null ? address.port : port;\n const url = `http://${host}:${resolvedPort}${basePath}`;\n\n if (options.open) {\n openBrowser(url);\n }\n\n resolve({\n url,\n port: resolvedPort,\n close(): Promise<void> {\n return new Promise<void>((closeResolve, closeReject) => {\n server.close((closeError) => {\n if (closeError) {\n closeReject(closeError);\n\n return;\n }\n\n closeResolve();\n });\n });\n },\n });\n });\n });\n}\n\n/** Loopback hosts the dashboard may bind without an auth token. */\nfunction isLoopbackHost(host: string): boolean {\n const h = host.toLowerCase();\n return h === \"127.0.0.1\" || h === \"::1\" || h === \"[::1]\" || h === \"localhost\";\n}\n\n/**\n * Default `Host` allowlist for a given bind host. A loopback bind accepts\n * the loopback names a browser would send; a non-loopback bind defaults to\n * just the bound host (callers can widen via `allowedHosts`).\n */\nfunction defaultAllowedHosts(host: string): string[] {\n if (isLoopbackHost(host)) {\n return [\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"];\n }\n return [host];\n}\n\n/**\n * Normalize a caller `basePath` to a leading-and-trailing-slash form the\n * router can prefix routes with. `undefined` / `\"\"` → `\"/\"`.\n */\nfunction normalizeBasePath(basePath?: string): string {\n if (basePath === undefined || basePath.length === 0 || basePath === \"/\") {\n return \"/\";\n }\n\n const withLeading = basePath.startsWith(\"/\") ? basePath : `/${basePath}`;\n\n return withLeading.endsWith(\"/\") ? withLeading : `${withLeading}/`;\n}\n\n/**\n * Best-effort open of the default browser at `url`. Fire-and-forget and\n * fully swallowed — failing to open a browser must never reject the\n * dashboard start. Uses the platform's native opener via a lazy\n * `node:child_process` import so the dependency is paid only when\n * `open: true`.\n */\nfunction openBrowser(url: string): void {\n void import(\"node:child_process\")\n .then(({ spawn }) => {\n const command =\n process.platform === \"win32\" ? \"cmd\" : process.platform === \"darwin\" ? \"open\" : \"xdg-open\";\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n\n const child = spawn(command, args, { stdio: \"ignore\", detached: true });\n child.on(\"error\", () => {\n // Swallow — opening a browser is best-effort.\n });\n child.unref();\n })\n .catch(() => {\n // Swallow — opening a browser is best-effort.\n });\n}\n","import type { ReportStatus, ReportType } from \"@warlock.js/ai\";\nimport type { Trace, TraceSpan } from \"../contracts/trace.type\";\n\n/**\n * Client-side trace-list filter criteria. The dashboard polls the full\n * trace list every 2s and narrows it in the browser with these — search\n * box, filter chips, and the errors-only header toggle all feed one\n * {@link TraceFilter}. Kept as a pure, framework-free module so the exact\n * matching rules are unit-tested here and the inlined dashboard JS in\n * {@link import(\"./ui.html\").dashboardHtml} mirrors them 1:1.\n *\n * Every field is optional — an absent field is \"don't care\". An empty\n * filter therefore matches every trace.\n */\nexport type TraceFilter = {\n /**\n * Free-text needle matched case-insensitively against a trace's root\n * span name and session id. Whitespace-trimmed; an empty/blank string\n * is treated as absent.\n */\n text?: string;\n /**\n * Restrict to these terminal statuses (root span status). Empty array\n * is treated as \"don't care\" (matches every status).\n */\n statuses?: ReportStatus[];\n /**\n * Restrict to these executable types (root span type). Empty array is\n * treated as \"don't care\".\n */\n types?: ReportType[];\n /**\n * Exact session-id chip. When set, only traces whose `sessionId`\n * equals this value match. Distinct from {@link TraceFilter.text},\n * which is a fuzzy substring across name + session.\n */\n sessionId?: string;\n /**\n * Exact prompt chip — a `name@version` key (see {@link tracePromptKey}).\n * When set, only traces whose root span resolved that exact named prompt\n * version match. Mirrors {@link TraceFilter.sessionId} but over the\n * prompt-version-linkage attributes the collector stamps on agent spans.\n */\n promptKey?: string;\n /**\n * Errors-only header toggle. When `true`, only `failed` / `cancelled`\n * traces match — independent of (and intersected with) `statuses`.\n */\n errorsOnly?: boolean;\n};\n\n/** Statuses the errors-only toggle keeps. */\nconst ERROR_STATUSES: readonly ReportStatus[] = [\"failed\", \"cancelled\"] as const;\n\n/**\n * Span-attribute keys the collector stamps with prompt-version linkage when\n * an agent ran against a *named* `ai.prompts` builder. Read here (not\n * imported) so this pure module stays decoupled from the collector.\n */\nconst PROMPT_NAME_ATTR = \"agent.promptName\";\nconst PROMPT_VERSION_ATTR = \"agent.promptVersion\";\n\n/**\n * The `name@version` prompt key a trace falls under, read from its root\n * span's prompt-version-linkage attributes (`agent.promptName` /\n * `agent.promptVersion`). Returns `undefined` when the run carried no named\n * prompt (raw-string / anonymous / no prompt), so callers can bucket those\n * under a synthetic \"no prompt\" key or skip them.\n *\n * The version is included so two runs of the same prompt name at different\n * versions are distinct keys — that is the whole point of the linkage: to\n * group / filter by the *exact* prompt revision that produced a run.\n */\nexport function tracePromptKey(trace: Trace): string | undefined {\n const attributes = trace.root.attributes;\n\n if (attributes === undefined) {\n return undefined;\n }\n\n const name = attributes[PROMPT_NAME_ATTR];\n\n if (typeof name !== \"string\" || name.length === 0) {\n return undefined;\n }\n\n const version = attributes[PROMPT_VERSION_ATTR];\n const versionLabel = typeof version === \"string\" && version.length > 0 ? version : \"1\";\n\n return `${name}@${versionLabel}`;\n}\n\n/**\n * Does a single trace pass the given filter? Pure predicate — all\n * conditions are ANDed; an unset field never excludes. Used by\n * {@link filterTraces} and mirrored by the dashboard's inlined JS.\n */\nexport function matchesFilter(trace: Trace, filter: TraceFilter): boolean {\n const root = trace.root;\n\n if (filter.errorsOnly === true && !ERROR_STATUSES.includes(root.status)) {\n return false;\n }\n\n if (filter.statuses !== undefined && filter.statuses.length > 0) {\n if (!filter.statuses.includes(root.status)) {\n return false;\n }\n }\n\n if (filter.types !== undefined && filter.types.length > 0) {\n if (!filter.types.includes(root.type)) {\n return false;\n }\n }\n\n if (filter.sessionId !== undefined && filter.sessionId.length > 0) {\n if (trace.sessionId !== filter.sessionId) {\n return false;\n }\n }\n\n if (filter.promptKey !== undefined && filter.promptKey.length > 0) {\n if (tracePromptKey(trace) !== filter.promptKey) {\n return false;\n }\n }\n\n const text = filter.text?.trim().toLowerCase();\n if (text !== undefined && text.length > 0) {\n const haystack = `${root.name} ${trace.sessionId ?? \"\"}`.toLowerCase();\n if (!haystack.includes(text)) {\n return false;\n }\n }\n\n return true;\n}\n\n/** Narrow a trace list to those matching the filter, order preserved. */\nexport function filterTraces(traces: Trace[], filter: TraceFilter): Trace[] {\n return traces.filter((trace) => matchesFilter(trace, filter));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by\n * session. Traces without a `sessionId` collapse into one synthetic\n * \"(no session)\" bucket so they remain visible.\n */\nexport const NO_SESSION_KEY = \"(no session)\";\n\n/** One session bucket — its key plus the traces under it, order preserved. */\nexport type SessionGroup = {\n /** The `sessionId`, or {@link NO_SESSION_KEY} for sessionless traces. */\n sessionId: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by session id,\n * preserving first-seen order of both the groups and the traces within\n * each. Sessionless traces bucket under {@link NO_SESSION_KEY}.\n */\nexport function groupBySession(traces: Trace[]): SessionGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.sessionId ?? NO_SESSION_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((sessionId) => ({ sessionId, traces: byKey.get(sessionId) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by prompt\n * version. Runs that resolved no named prompt collapse into one synthetic\n * \"(no prompt)\" bucket so they remain visible.\n */\nexport const NO_PROMPT_KEY = \"(no prompt)\";\n\n/** One prompt-version bucket — its `name@version` key plus its traces. */\nexport type PromptGroup = {\n /** The `name@version` key, or {@link NO_PROMPT_KEY} for unlinked traces. */\n promptKey: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by prompt version —\n * the `name@version` key {@link tracePromptKey} derives from each run's\n * prompt-version-linkage attributes. Preserves first-seen order of both the\n * groups and the traces within each. Runs with no named prompt bucket under\n * {@link NO_PROMPT_KEY}.\n *\n * The dashboard offers this as a second group-by dimension beside session,\n * so a reviewer can see every run of \"support@2\" together and compare its\n * cost / failure rate against \"support@3\".\n */\nexport function groupByPrompt(traces: Trace[]): PromptGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = tracePromptKey(trace) ?? NO_PROMPT_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((promptKey) => ({ promptKey, traces: byKey.get(promptKey) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by root\n * type. `root.type` is always present, so this is only a defensive\n * fallback for a malformed trace with no typed root.\n */\nexport const NO_TYPE_KEY = \"(no type)\";\n\n/** One root-type bucket — its type discriminator plus its traces. */\nexport type TypeGroup = {\n /** The root span's `type` (e.g. `agent` / `workflow` / `planner`), or {@link NO_TYPE_KEY}. */\n type: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by root type —\n * the `agent` / `workflow` / `supervisor` / `orchestrator` / `planner` /\n * `tool` / `batch` / `callback` discriminator on each trace's root span.\n * Preserves first-seen order of both the groups and the traces within each.\n *\n * The dashboard offers this as the coarsest group-by dimension beside\n * session and prompt: when a benchmark floods the flat list with hundreds\n * of one primitive (e.g. standalone `agent` runs), this collapses them\n * behind a per-type header so the structural primitives (the lone planner,\n * the team, the supervisors) are one click away instead of a scroll past.\n */\nexport function groupByType(traces: Trace[]): TypeGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => ({ type, traces: byKey.get(type) ?? [] }));\n}\n\n/** One row of the per-type aggregate-stats panel. */\nexport type TypeStat = {\n /** Root type discriminator, or {@link NO_TYPE_KEY}. */\n type: string;\n /** Number of traces of this type. */\n count: number;\n /** Traces whose status is `failed` / `cancelled`. */\n failed: number;\n /** `failed / count` in `[0, 1]`. */\n failRate: number;\n /** Median (p50) root duration in ms. */\n p50: number;\n /** p95 root duration in ms. */\n p95: number;\n /** Summed `usage.total` tokens across the bucket. */\n tokens: number;\n /** Summed per-trace USD cost across the bucket. */\n cost: number;\n};\n\n/**\n * Nearest-rank percentile of `values` at `p` (0–100). Returns 0 for an\n * empty input and sorts a copy so the caller's array is left untouched.\n */\nexport function percentile(values: number[], p: number): number {\n if (values.length === 0) {\n return 0;\n }\n\n const sorted = values.slice().sort((a, b) => a - b);\n const rank = Math.ceil((p / 100) * sorted.length) - 1;\n const index = Math.min(Math.max(rank, 0), sorted.length - 1);\n\n return sorted[index];\n}\n\n/**\n * One trace's total USD cost: its explicit root-usage cost when priced,\n * else the rollup of its subtree. Mirrors the dashboard's `traceCost` so the\n * stats panel and the trace rows agree to the cent.\n */\nexport function traceCost(trace: Trace): number {\n const own = costSumObj(trace.usage.cost);\n return own > 0 ? own : rollupCost(trace.root);\n}\n\n/**\n * Aggregate an (already filtered) trace list into one {@link TypeStat} row\n * per root type — count, failure rate, p50/p95 latency, total tokens, total\n * cost — preserving first-seen type order. Powers the dashboard's per-type\n * stats panel; computed over the same filtered list the trace view shows, so\n * the panel always reflects the active filters.\n */\nexport function aggregateByType(traces: Trace[]): TypeStat[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => {\n const bucket = byKey.get(type) ?? [];\n const durations = bucket.map((trace) => trace.duration);\n let failed = 0;\n let tokens = 0;\n let cost = 0;\n\n for (const trace of bucket) {\n if (ERROR_STATUSES.includes(trace.root.status)) {\n failed += 1;\n }\n\n tokens += trace.usage.total ?? 0;\n cost += traceCost(trace);\n }\n\n return {\n type,\n count: bucket.length,\n failed,\n failRate: bucket.length > 0 ? failed / bucket.length : 0,\n p50: percentile(durations, 50),\n p95: percentile(durations, 95),\n tokens,\n cost,\n };\n });\n}\n\n/**\n * Sum every priced lane of a usage cost object into one USD number.\n * Mirrors the dashboard's `costSumObj` so heatmap intensities computed\n * here match what the page renders.\n */\nfunction costSumObj(cost: TraceSpan[\"usage\"][\"cost\"]): number {\n if (cost === undefined) {\n return 0;\n }\n\n return (\n (cost.input ?? 0) +\n (cost.output ?? 0) +\n (cost.cachedInput ?? 0) +\n (cost.cachedOutput ?? 0) +\n (cost.reasoning ?? 0)\n );\n}\n\n/**\n * Rollup-aware subtree cost: a node's own cost when it carries one\n * (it already rolls up its trips), otherwise the sum of its children.\n * Avoids double-counting wrapper nodes (workflow/supervisor roots carry\n * tokens but no cost). Mirrors the dashboard's `rollupCost`.\n */\nexport function rollupCost(span: TraceSpan): number {\n const own = costSumObj(span.usage.cost);\n if (own > 0) {\n return own;\n }\n\n let sum = 0;\n for (const child of span.children) {\n sum += rollupCost(child);\n }\n\n return sum;\n}\n\n/**\n * Heatmap intensity in `[0, 1]` for a node's `rollupCost` relative to the\n * trace's most-expensive node. The dashboard tints each tree node's left\n * accent by this. `max <= 0` (a free trace) yields `0` for every node so\n * the heatmap simply stays cold rather than dividing by zero.\n */\nexport function heatIntensity(nodeCost: number, maxCost: number): number {\n if (maxCost <= 0 || nodeCost <= 0) {\n return 0;\n }\n\n const ratio = nodeCost / maxCost;\n\n return ratio > 1 ? 1 : ratio;\n}\n\n/**\n * The largest single-node {@link rollupCost} anywhere in a span tree —\n * the denominator for {@link heatIntensity}. Walks the whole subtree.\n */\nexport function maxNodeCost(root: TraceSpan): number {\n let max = rollupCost(root);\n\n for (const child of root.children) {\n const childMax = maxNodeCost(child);\n if (childMax > max) {\n max = childMax;\n }\n }\n\n return max;\n}\n","import type { ExecutionReport, Observer } from \"@warlock.js/ai\";\nimport { registerObserver, setObserveAll } from \"@warlock.js/ai\";\nimport { log } from \"@warlock.js/logger\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport { dashboard } from \"../dashboard/dashboard\";\nimport type { DashboardHandle, DashboardOptions } from \"../dashboard/dashboard.type\";\nimport { panoptic } from \"../panoptic/panoptic\";\nimport type { Panoptic } from \"../panoptic/panoptic.type\";\nimport { createCacheTraceStore } from \"../store/cache-trace-store\";\nimport type { CacheTraceStoreHandle } from \"../store/cache-trace-store\";\nimport { createInMemoryTraceStore } from \"../store/in-memory-trace-store\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport type { PanopticConfig } from \"./panoptic-config.type\";\n\n/**\n * Module-level applied state. Applying panoptic config is idempotent:\n * the collector is registered as a core `Observer` exactly once, the\n * dashboard is started at most once, and repeat calls only refresh the\n * `observeAll` flag (the latest config wins) without double-registering.\n */\ntype AppliedState = {\n /** The single collector registered as a core `Observer`. */\n panopticInstance: Panoptic;\n /** The dashboard handle, once started. Started at most once. */\n dashboardHandle?: DashboardHandle;\n};\n\nlet applied: AppliedState | undefined;\n\n/**\n * Read a {@link PanopticConfig} and wire panoptic onto the core observe\n * seam — the bridge `ai.config({ panoptic })` resolves through. Builds a\n * collector via the EXISTING {@link panoptic} factory (reusing its\n * collection pipeline, not reinventing it), registers it once via core's\n * `registerObserver` (the subscriber's `collect(report)` structurally IS\n * an {@link Observer}, threaded through a thin wrapper), sets\n * `observeAll`, and — when `config.dashboard` is set — starts the\n * dashboard over the config's store (a store-shaped exporter, or a fresh\n * in-memory store panoptic registers when none was supplied).\n *\n * **Idempotent.** Safe to call on every `onConfigApplied` notification:\n * the observer is registered once, the dashboard started once. Repeat\n * calls update `observeAll` to the latest value but never double-register\n * or double-start. `undefined` config is a no-op (nothing wired).\n *\n * @returns the resolved store-shaped exporter when the dashboard needs\n * one and a fresh store was created — primarily for tests; callers can\n * ignore it.\n */\nexport function applyPanopticConfig(config?: PanopticConfig): void {\n if (config === undefined) {\n return;\n }\n\n // observeAll always tracks the latest config, even on a repeat call.\n setObserveAll(Boolean(config.observeAll));\n\n if (applied === undefined) {\n const exporters = [...(config.exporters ?? [])];\n let store = findStore(exporters);\n\n // Track a cache store separately so we can await its hydration before\n // the dashboard reads it — the in-memory store needs no warm-up.\n let cacheStore: CacheTraceStoreHandle | undefined;\n\n // The dashboard needs a queryable store. If none was supplied via\n // exporters but a dashboard is requested, create one and register it\n // as an exporter so the collector fills it. A configured `cache` picks\n // the durable cache-backed store; otherwise fall back to in-memory so\n // the dashboard works with no cache configured.\n if (store === undefined && config.dashboard) {\n if (config.cache !== undefined) {\n const onError =\n config.onError ??\n ((error: unknown) => log.error(\"ai-panoptic\", \"cacheStore\", error));\n\n cacheStore = createCacheTraceStore(config.cache, { onError });\n store = cacheStore;\n } else {\n store = createInMemoryTraceStore();\n }\n\n exporters.push(store as unknown as ExporterContract);\n }\n\n const panopticInstance = panoptic({\n exporters,\n captureContent: config.captureContent,\n fullHistory: config.fullHistory,\n });\n registerObserver(toObserver(panopticInstance));\n\n applied = { panopticInstance };\n\n if (config.dashboard && store !== undefined) {\n const resolvedStore = store;\n\n // Hydrate the cache mirror (if any) before serving, so a restart\n // surfaces previously-persisted traces immediately. A bare in-memory\n // store resolves the readyGate instantly.\n const readyGate =\n cacheStore !== undefined ? cacheStore.ready() : Promise.resolve();\n\n void readyGate\n .catch(() => {\n // Hydration is best-effort — never block the dashboard on it.\n })\n .then(() => startDashboard(resolvedStore, config.dashboard ?? {}))\n .then((handle) => {\n if (applied !== undefined) {\n applied.dashboardHandle = handle;\n }\n });\n }\n }\n}\n\n/**\n * Reset the applied state — internal, test-only. Closes any running\n * dashboard and forgets the registered collector so a fresh\n * `applyPanopticConfig` starts clean. NOT part of the public surface and\n * does NOT unregister from the core observer registry (use core's\n * `clearObservers` for that in tests).\n */\nexport async function resetAppliedPanopticConfig(): Promise<void> {\n const handle = applied?.dashboardHandle;\n applied = undefined;\n\n if (handle !== undefined) {\n await handle.close();\n }\n}\n\n/**\n * The currently-running dashboard handle, or `undefined` when no\n * dashboard is active. Internal, test-only — lets a spec await the\n * asynchronously-started dashboard. NOT part of the public surface.\n */\nexport function getActiveDashboardHandle(): DashboardHandle | undefined {\n return applied?.dashboardHandle;\n}\n\n/**\n * Wrap a {@link Panoptic} subscriber as a core {@link Observer}. The\n * subscriber already exposes `collect(report)` with a matching signature,\n * but the thin wrapper makes the structural adaptation explicit and keeps\n * the registered object a minimal `Observer` rather than the whole\n * subscriber surface.\n */\nfunction toObserver(instance: Panoptic): Observer {\n return {\n collect(report: ExecutionReport): Promise<void> {\n return instance.collect(report);\n },\n };\n}\n\n/**\n * Find the first exporter that satisfies the {@link TraceStoreContract}\n * read surface (`query` / `get` / `aggregate`) — the in-memory store\n * doubles as an exporter, so a store passed via `exporters` is reused for\n * the dashboard rather than creating a second one.\n */\nfunction findStore(exporters: ExporterContract[]): TraceStoreContract | undefined {\n for (const exporter of exporters) {\n const candidate = exporter as unknown as Partial<TraceStoreContract>;\n\n if (\n typeof candidate.query === \"function\" &&\n typeof candidate.get === \"function\" &&\n typeof candidate.aggregate === \"function\"\n ) {\n return candidate as TraceStoreContract;\n }\n }\n\n return undefined;\n}\n\n/** Start the dashboard, normalizing the `true | DashboardOptions` switch. */\nfunction startDashboard(\n store: TraceStoreContract,\n dashboardConfig: boolean | DashboardOptions,\n): Promise<DashboardHandle> {\n const options: DashboardOptions =\n typeof dashboardConfig === \"object\" ? dashboardConfig : {};\n\n return dashboard(store, options);\n}\n","import { getAIConfig, onConfigApplied } from \"@warlock.js/ai\";\nimport { applyPanopticConfig } from \"./config/apply-panoptic-config\";\n\n// Side-effect wiring. Importing `@warlock.js/ai-panoptic` (even bare,\n// `import \"@warlock.js/ai-panoptic\"`) subscribes panoptic to the core\n// config seam so a later `ai.config({ panoptic })` wires the collector +\n// dashboard onto the observe registry — without app code calling\n// `applyPanopticConfig` by hand.\n//\n// 1. React to every future `ai.config(...)` merge.\nonConfigApplied((config) => {\n applyPanopticConfig(config.panoptic);\n});\n\n// 2. Catch config that was applied BEFORE this import ran (e.g. the app\n// called `ai.config({ panoptic })` and only then imported panoptic).\napplyPanopticConfig(getAIConfig().panoptic);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["EXPORTER_NAME","EXPORTER_NAME","EXPORTER_NAME","isModuleExists","loadingPromise","log"],"sources":["../../../../../../ai-panoptic/src/exporters/utils/walk-spans.ts","../../../../../../ai-panoptic/src/collector/normalize-error.ts","../../../../../../ai-panoptic/src/collector/extract-span-attributes.ts","../../../../../../ai-panoptic/src/collector/report-to-span.ts","../../../../../../ai-panoptic/src/collector/report-to-trace.ts","../../../../../../ai-panoptic/src/collector/collector.ts","../../../../../../ai-panoptic/src/store/match-trace.ts","../../../../../../ai-panoptic/src/store/sum-usage.ts","../../../../../../ai-panoptic/src/store/cache-trace-store.ts","../../../../../../ai-panoptic/src/store/in-memory-trace-store.ts","../../../../../../ai-panoptic/src/exporters/utils/total-cost.ts","../../../../../../ai-panoptic/src/exporters/utils/gen-ai-attributes.ts","../../../../../../ai-panoptic/src/exporters/console/format-span-io.ts","../../../../../../ai-panoptic/src/exporters/console/format-span-line.ts","../../../../../../ai-panoptic/src/exporters/console/console-exporter.ts","../../../../../../ai-panoptic/src/exporters/file/file-exporter.ts","../../../../../../ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts","../../../../../../ai-panoptic/src/exporters/otel/otel-exporter.ts","../../../../../../ai-panoptic/src/panoptic/panoptic-middleware.ts","../../../../../../ai-panoptic/src/panoptic/panoptic.ts","../../../../../../ai-panoptic/src/evaluate/evaluate-system-prompt.ts","../../../../../../ai-panoptic/src/evaluate/extract-last-system-prompt.ts","../../../../../../ai-panoptic/src/evaluate/find-span-by-id.ts","../../../../../../ai-panoptic/src/dashboard/parse-query.ts","../../../../../../ai-panoptic/src/dashboard/warlock-logo.ts","../../../../../../ai-panoptic/src/dashboard/ui.html.ts","../../../../../../ai-panoptic/src/dashboard/serve.ts","../../../../../../ai-panoptic/src/dashboard/dashboard.ts","../../../../../../ai-panoptic/src/dashboard/trace-filter.ts","../../../../../../ai-panoptic/src/config/apply-panoptic-config.ts","../../../../../../ai-panoptic/src/register.ts"],"sourcesContent":["import type { TraceSpan } from \"../../contracts\";\n\n/**\n * Depth-first pre-order traversal of a {@link TraceSpan} tree, yielding\n * the root first and then each descendant in `children` (invocation)\n * order. Exporters that emit a flat span stream — OpenTelemetry, the\n * console table — walk the tree once with this instead of re-writing the\n * recursion in every exporter.\n *\n * @example\n * for (const span of walkSpans(trace.root)) {\n * emit(span);\n * }\n */\nexport function* walkSpans(root: TraceSpan): Generator<TraceSpan> {\n yield root;\n\n for (const child of root.children) {\n yield* walkSpans(child);\n }\n}\n","import { redact, scrubSecrets } from \"@warlock.js/ai\";\nimport type { TraceSpanError } from \"../contracts/trace.type\";\n\n/**\n * Project a captured execution error onto the structural\n * {@link TraceSpanError} shape used on a failed / cancelled span.\n *\n * Source errors are typically `AIError` instances (every error surfaced\n * by `@warlock.js/ai` is one), but the collector never depends on the\n * concrete class — it reads only the structural surface (`name` / `code`\n * / `message` / `stack`) so a plain `Error`, an `AIError`, or any\n * thrown value all normalize identically. The result is a JSON-safe\n * plain object so it survives serialization to a backend collector\n * unchanged.\n *\n * The error `type` prefers the stable `code` (e.g. `\"RATE_LIMIT\"`) over\n * the class `name`, falling back to `name` and finally to the generic\n * `\"Error\"` so the field is always populated.\n *\n * @example\n * const spanError = normalizeError(report.error);\n * // { type: \"RATE_LIMIT\", message: \"429 Too Many Requests\", stack: \"...\" }\n */\nexport function normalizeError(error: unknown): TraceSpanError | undefined {\n if (error === undefined || error === null) {\n return undefined;\n }\n\n if (typeof error !== \"object\") {\n return {\n type: \"Error\",\n message: scrubSecrets(String(error)),\n };\n }\n\n const candidate = error as {\n code?: unknown;\n name?: unknown;\n message?: unknown;\n stack?: unknown;\n cause?: unknown;\n };\n\n const type = pickString(candidate.code) ?? pickString(candidate.name) ?? \"Error\";\n const message = pickString(candidate.message) ?? \"\";\n const stack = pickString(candidate.stack);\n\n // Scrub free-text secrets (Bearer tokens, api keys) from the message and\n // stack, and deep-redact the cause (a raw provider SDK error can carry\n // auth/cookie headers) before either is stored or exported (S4).\n const normalized: TraceSpanError = {\n type,\n message: scrubSecrets(message),\n };\n\n if (stack !== undefined) {\n normalized.stack = scrubSecrets(stack);\n }\n\n if (candidate.cause !== undefined && candidate.cause !== null) {\n normalized.cause =\n typeof candidate.cause === \"object\"\n ? redact(candidate.cause)\n : scrubSecrets(String(candidate.cause));\n }\n\n return normalized;\n}\n\n/**\n * Return the value when it is a non-empty string, otherwise `undefined`.\n * Keeps `normalizeError` from promoting empty / non-string fields.\n */\nfunction pickString(value: unknown): string | undefined {\n if (typeof value === \"string\" && value.length > 0) {\n return value;\n }\n\n return undefined;\n}\n","import type { BaseReport } from \"@warlock.js/ai\";\n\n/**\n * Per-primitive `BaseReport` extension fields the collector surfaces as\n * span attributes. Declared as a structural superset (every field\n * optional) so a single reader can pull whatever the concrete report\n * carried without narrowing on `type` first — a `BaseReport` widened to\n * this shape exposes `undefined` for the fields its primitive doesn't\n * set, and the extractor simply skips those.\n *\n * Mirrors the public per-primitive report types in `@warlock.js/ai`\n * (`AgentReport`, `WorkflowReport`, `SupervisorReport`,\n * `OrchestratorReport`, `ToolCall`) — kept structural rather than a\n * union import so adding a new optional report field upstream is a\n * non-breaking read here.\n */\ntype ReportExtensions = {\n model?: { name?: string; provider?: string };\n trips?: unknown[];\n promptName?: string;\n promptVersion?: string;\n workflowName?: string;\n supervisorName?: string;\n signature?: string;\n terminatedBy?: string;\n iterations?: number;\n steps?: Record<string, unknown>;\n turns?: unknown[];\n turnIndex?: number;\n tripIndex?: number;\n recoveredFrom?: string;\n};\n\n/**\n * Build the free-form `TraceSpan.attributes` bag for one report node.\n *\n * The collector keeps the first-class span fields (identity, timing,\n * status, usage, error) on the span itself and routes everything\n * primitive-specific here — trip/step/iteration counts, the model\n * identity an agent ran against, the tool's originating trip index, a\n * supervisor's termination reason. Exporters forward this verbatim as\n * backend span attributes (OTel attributes, Langfuse metadata).\n *\n * Only populated keys are emitted; the function returns `undefined`\n * when the node carried no extra detail, so the optional\n * `TraceSpan.attributes` field stays absent rather than holding an\n * empty object (matches the contract's \"absent when empty\" note).\n *\n * Retry count is surfaced from the shared `BaseReport.attempts` for\n * every primitive so cost dashboards see the real call count.\n *\n * @example\n * const attributes = extractSpanAttributes(agentReport);\n * // { \"agent.trips\": 3, \"agent.model.name\": \"gpt-4o\", \"agent.model.provider\": \"openai\" }\n */\nexport function extractSpanAttributes(report: BaseReport): Record<string, unknown> | undefined {\n const extensions = report as BaseReport & ReportExtensions;\n const attributes: Record<string, unknown> = {};\n\n if (report.attempts !== undefined && report.attempts.length > 0) {\n attributes[\"retries\"] = report.attempts.length;\n }\n\n switch (report.type) {\n case \"agent\": {\n addAgentAttributes(attributes, extensions);\n break;\n }\n\n case \"workflow\": {\n addWorkflowAttributes(attributes, extensions);\n break;\n }\n\n case \"supervisor\":\n case \"team\": {\n // ai.team reuses the supervisor engine, so a team report carries the\n // same supervisorName / terminatedBy / iterations fields.\n addSupervisorAttributes(attributes, extensions);\n break;\n }\n\n case \"orchestrator\": {\n addOrchestratorAttributes(attributes, extensions);\n break;\n }\n\n case \"tool\": {\n addToolAttributes(attributes, extensions);\n break;\n }\n\n default: {\n break;\n }\n }\n\n if (Object.keys(attributes).length === 0) {\n return undefined;\n }\n\n return attributes;\n}\n\nfunction addAgentAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (Array.isArray(extensions.trips)) {\n attributes[\"agent.trips\"] = extensions.trips.length;\n }\n\n if (extensions.model?.name !== undefined) {\n attributes[\"agent.model.name\"] = extensions.model.name;\n }\n\n if (extensions.model?.provider !== undefined) {\n attributes[\"agent.model.provider\"] = extensions.model.provider;\n }\n\n // Prompt-version linkage (core `AgentReport.promptName` / `promptVersion`).\n // Present only when the agent ran against a *named* `ai.prompts` builder;\n // surfaced so the dashboard can group / filter runs by `name@version` and\n // attribute behavior shifts to a specific prompt revision.\n if (extensions.promptName !== undefined) {\n attributes[\"agent.promptName\"] = extensions.promptName;\n }\n\n if (extensions.promptVersion !== undefined) {\n attributes[\"agent.promptVersion\"] = extensions.promptVersion;\n }\n}\n\nfunction addWorkflowAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (extensions.workflowName !== undefined) {\n attributes[\"workflow.name\"] = extensions.workflowName;\n }\n\n if (extensions.signature !== undefined) {\n attributes[\"workflow.signature\"] = extensions.signature;\n }\n\n if (extensions.steps !== undefined) {\n attributes[\"workflow.steps\"] = Object.keys(extensions.steps).length;\n }\n}\n\nfunction addSupervisorAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (extensions.supervisorName !== undefined) {\n attributes[\"supervisor.name\"] = extensions.supervisorName;\n }\n\n if (extensions.terminatedBy !== undefined) {\n attributes[\"supervisor.terminatedBy\"] = extensions.terminatedBy;\n }\n\n if (extensions.iterations !== undefined) {\n attributes[\"supervisor.iterations\"] = extensions.iterations;\n }\n}\n\nfunction addOrchestratorAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (extensions.turnIndex !== undefined) {\n attributes[\"orchestrator.turnIndex\"] = extensions.turnIndex;\n }\n\n if (extensions.signature !== undefined) {\n attributes[\"orchestrator.signature\"] = extensions.signature;\n }\n\n if (Array.isArray(extensions.turns)) {\n attributes[\"orchestrator.turns\"] = extensions.turns.length;\n }\n}\n\nfunction addToolAttributes(attributes: Record<string, unknown>, extensions: ReportExtensions): void {\n if (extensions.tripIndex !== undefined) {\n attributes[\"tool.tripIndex\"] = extensions.tripIndex;\n }\n\n if (extensions.recoveredFrom !== undefined) {\n attributes[\"tool.recoveredFrom\"] = extensions.recoveredFrom;\n }\n}\n","import type { BaseReport } from \"@warlock.js/ai\";\nimport type { TraceSpan } from \"../contracts/trace.type\";\nimport type { ContentCaptureOptions, ContentRedactor } from \"./content-capture.type\";\nimport { extractSpanAttributes } from \"./extract-span-attributes\";\nimport { normalizeError } from \"./normalize-error\";\n\n/**\n * Project one {@link BaseReport} node — and its entire subtree — into a\n * {@link TraceSpan}. Pure and recursive: identity, timing, status, and\n * the rolled-up `usage` map across 1:1 from the report; lineage maps\n * `runId → spanId`, `parentRunId → parentSpanId`, `rootRunId → traceId`;\n * children recurse in invocation order so the span tree mirrors the\n * report tree exactly.\n *\n * The error is normalized to the JSON-safe {@link\n * import(\"../contracts/trace.type\").TraceSpanError} shape only when the\n * node carried one (failed / cancelled). Primitive-specific detail that\n * has no first-class span field (trip / step / iteration counts, model\n * identity, tool trip index) is routed into the optional `attributes`\n * bag via {@link extractSpanAttributes}.\n *\n * When {@link ContentCaptureOptions.captureContent} is set, the raw\n * prompt/response (agents) and args/result (tools) are additionally\n * copied onto `span.input` / `span.output` — off by default because\n * payloads are large and often sensitive. A {@link ContentRedactor} can\n * mask each value first.\n *\n * No external lookup is needed — a `BaseReport` already carries\n * everything a span requires, so a collector can flatten a tree without\n * consulting any other source.\n *\n * @example\n * const root = reportToSpan(result.report);\n * console.log(root.spanId, root.traceId, root.children.length);\n */\nexport function reportToSpan(report: BaseReport, options?: ContentCaptureOptions): TraceSpan {\n const span: TraceSpan = {\n spanId: report.runId,\n traceId: report.rootRunId,\n name: report.name,\n type: report.type,\n status: report.status,\n startedAt: report.startedAt,\n endedAt: report.endedAt,\n duration: report.duration,\n usage: report.usage,\n children: report.children.map((child) => reportToSpan(child, options)),\n };\n\n if (report.parentRunId !== undefined) {\n span.parentSpanId = report.parentRunId;\n }\n\n if (report.sessionId !== undefined) {\n span.sessionId = report.sessionId;\n }\n\n if (report.version !== undefined) {\n span.version = report.version;\n }\n\n const error = normalizeError((report as { error?: unknown }).error);\n if (error !== undefined) {\n span.error = error;\n }\n\n const attributes = extractSpanAttributes(report);\n if (attributes !== undefined) {\n span.attributes = attributes;\n }\n\n if (options?.captureContent) {\n captureContent(span, report, options);\n }\n\n return span;\n}\n\n/** Per-primitive content fields read off a widened report node. */\ntype ContentReport = {\n input?: unknown;\n output?: unknown;\n systemPrompt?: string;\n trips?: Array<{ input?: unknown; output?: unknown }>;\n /**\n * The full assembled conversation an agent run captured when its\n * `captureMessages` option was set (core `AgentReport.messages`).\n * Present only on opted-in agent runs; absent otherwise.\n */\n messages?: unknown;\n};\n\n/**\n * Copy the node's raw content onto `span.input` / `span.output`.\n *\n * - Tools carry the call arguments + return value directly on the report\n * (`ToolCall.input` / `ToolCall.output`).\n * - Agents carry a `trips[]` history plus the resolved `systemPrompt`. The\n * input is emitted as a `[system, user]` chat array — so backends like\n * Langfuse render the full prompt as sent — or the bare user string when\n * there's no system prompt. The output is the last NON-EMPTY trip\n * `output` (the final response text); a failed / max-trips run can end on\n * a trip whose `output` is `\"\"` or tool-call-only, so we scan back for the\n * last one that carried text. Intermediate trips store a `\"[tool results]\"`\n * placeholder upstream.\n * - When {@link ContentCaptureOptions.fullHistory} is on AND the agent run\n * opted into `captureMessages` (so the report carries a `messages`\n * array), the *whole* `CapturedMessage[]` is emitted as `span.input`\n * instead of the `[system, user]` first-trip array — every role, every\n * trip. The output stays the last non-empty trip output. When `messages`\n * is absent the branch falls back to today's first-trip logic, so a run\n * that didn't opt in degrades gracefully.\n *\n * Each value is passed through the optional {@link ContentRedactor};\n * a redactor returning `undefined` drops the field. Under `fullHistory`\n * the redactor receives the full array as a single value.\n */\nfunction captureContent(\n span: TraceSpan,\n report: BaseReport,\n options: ContentCaptureOptions,\n): void {\n const node = report as BaseReport & ContentReport;\n const redact = options.redactContent;\n\n let input: unknown;\n let output: unknown;\n\n if (report.type === \"tool\") {\n input = node.input;\n output = node.output;\n } else if (\n options.fullHistory &&\n Array.isArray(node.messages) &&\n node.messages.length > 0\n ) {\n // Full-history capture: the entire assembled conversation as a single\n // input value. Output remains the agent's final response text.\n input = node.messages;\n output = Array.isArray(node.trips) ? lastNonEmptyOutput(node.trips) : undefined;\n } else if (Array.isArray(node.trips) && node.trips.length > 0) {\n const userInput = node.trips[0]?.input;\n // Emit a [system, user] chat array when the agent carried a system\n // prompt, so backends (Langfuse) render the full prompt as sent;\n // otherwise keep the bare user string.\n input =\n typeof node.systemPrompt === \"string\" && node.systemPrompt.length > 0\n ? [\n { role: \"system\", content: node.systemPrompt },\n { role: \"user\", content: userInput },\n ]\n : userInput;\n output = lastNonEmptyOutput(node.trips);\n }\n\n if (input !== undefined) {\n const value = redact ? redact(input, { name: span.name, type: span.type, field: \"input\" }) : input;\n if (value !== undefined) {\n span.input = value;\n }\n }\n\n if (output !== undefined) {\n const value = redact ? redact(output, { name: span.name, type: span.type, field: \"output\" }) : output;\n if (value !== undefined) {\n span.output = value;\n }\n }\n}\n\n/**\n * The last trip output that actually carries text — the agent's final\n * response on the happy path. A failed or max-trips run can end on a trip\n * whose `output` is `\"\"` or tool-call-only, so we scan backwards for the\n * last trip that produced text rather than blindly taking the final trip\n * (which would surface an empty string). Returns `undefined` when no trip\n * produced any output.\n */\nfunction lastNonEmptyOutput(trips: Array<{ output?: unknown }>): unknown {\n for (let i = trips.length - 1; i >= 0; i -= 1) {\n const out = trips[i]?.output;\n const hasText = typeof out === \"string\" ? out.length > 0 : out !== undefined;\n\n if (hasText) {\n return out;\n }\n }\n\n return undefined;\n}\n","import type { BaseReport } from \"@warlock.js/ai\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport type { ContentCaptureOptions } from \"./content-capture.type\";\nimport { normalizeError } from \"./normalize-error\";\nimport { reportToSpan } from \"./report-to-span\";\n\n/**\n * Project an outermost {@link BaseReport} (one whole `.execute()` /\n * `.invoke()` run) into a {@link Trace} — the root {@link\n * import(\"../contracts/trace.type\").TraceSpan} plus the trace-wide\n * rollups exporters need without re-walking the tree.\n *\n * The trace-level identity and rollups all read off the root span the\n * projection already built (`traceId`, `usage`, timing), so the trace\n * envelope never disagrees with its own root. `reportSchemaVersion` is\n * mirrored from the root report when present (it is stamped only on\n * root nodes upstream) so exporters can branch on the source shape.\n *\n * Pure — the same input always yields the same trace. The collector\n * exposes this as `toTrace` so callers can inspect the normalized shape\n * without dispatching to exporters.\n *\n * The optional `rootError` threads the failing run's envelope error\n * (`BaseResult.error`) onto the root span — a fallback for callers that\n * hold the result envelope (the `attach`/middleware path). Root primitives\n * now also stamp their terminal error onto the report itself\n * (`BaseReport.error`), so the observe path — which delivers only the\n * report, never the envelope — still surfaces a failed root's error.\n * `rootError` is applied only when the projected root span carries none of\n * its own; the subtree projection stays pure (each child surfaces its own\n * report-level error, if any).\n *\n * @example\n * const trace = reportToTrace(result.report, result.error);\n * console.log(trace.traceId, trace.usage.total, trace.duration);\n */\nexport function reportToTrace(\n report: BaseReport,\n rootError?: unknown,\n options?: ContentCaptureOptions,\n): Trace {\n const root = reportToSpan(report, options);\n\n if (root.error === undefined) {\n const error = normalizeError(rootError);\n\n if (error !== undefined) {\n root.error = error;\n }\n }\n\n const trace: Trace = {\n traceId: root.traceId,\n root,\n startedAt: root.startedAt,\n endedAt: root.endedAt,\n duration: root.duration,\n usage: root.usage,\n };\n\n if (root.sessionId !== undefined) {\n trace.sessionId = root.sessionId;\n }\n\n if (report.reportSchemaVersion !== undefined) {\n trace.reportSchemaVersion = report.reportSchemaVersion;\n }\n\n return trace;\n}\n","import type { BaseReport } from \"@warlock.js/ai\";\nimport type { CollectorContract } from \"../contracts/collector.contract\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { walkSpans } from \"../exporters/utils/walk-spans\";\nimport type { ContentCaptureOptions } from \"./content-capture.type\";\nimport { reportToTrace } from \"./report-to-trace\";\n\n/**\n * Notified when a registered exporter throws during `collect` / `flush` /\n * `shutdown`. The failure stays isolated (the originating run never sees\n * it) — this is purely a chance to surface it (route to your logger,\n * bump a metric). Receives the exporter's `name` and the thrown error.\n */\nexport type ExporterErrorHandler = (exporterName: string, error: unknown) => void;\n\n/** Options for {@link createCollector}. */\nexport type CollectorOptions = ContentCaptureOptions & {\n /**\n * Called when an exporter throws (and is isolated). Overrides the default\n * one-time `console.warn` per exporter — route the failure to your own\n * logger / metrics instead. Errors thrown by the handler itself are\n * swallowed so observability never crashes the run.\n */\n onError?: ExporterErrorHandler;\n};\n\n/**\n * Drive the source end of the Panoptic pipeline: ingest core\n * `@warlock.js/ai` {@link BaseReport} trees, project them into {@link\n * Trace}s, and fan each trace out to every registered exporter.\n *\n * Owns exporter registration (deduped by `ExporterContract.name`), the\n * report→trace projection, and graceful shutdown so exporters drain\n * before exit. Instantiated fresh per collector via {@link\n * createCollector}; callers never see `new`.\n *\n * **Failure isolation.** An exporter that throws never propagates back\n * into the originating run — `collect`, `flush`, and `shutdown` settle\n * every exporter independently (mirrors how the core event hooks\n * swallow consumer errors). One broken exporter can neither crash the\n * agent loop nor stop sibling exporters from receiving the trace. The\n * failure is still **surfaced** — via the `onError` option, or a one-time\n * `console.warn` per exporter — so a misconfiguration (e.g. a missing\n * optional peer like `langfuse`) doesn't fail silently.\n */\nclass Collector implements CollectorContract {\n /**\n * Registered exporters in insertion order. A `Map` keyed by\n * `ExporterContract.name` gives O(1) dedupe on `use` while preserving\n * registration order for deterministic fan-out.\n */\n private readonly exporters = new Map<string, ExporterContract>();\n\n /**\n * Exporters already warned about on the default error path — so a\n * persistent misconfiguration surfaces once, not on every trace.\n */\n private readonly warnedExporters = new Set<string>();\n\n /**\n * Collector options: content capture threaded into every `toTrace`\n * projection (`captureContent` populates span `input` / `output`), plus\n * an optional `onError` for isolated exporter failures.\n */\n public constructor(private readonly options: CollectorOptions = {}) {}\n\n public use(exporter: ExporterContract): this {\n if (!this.exporters.has(exporter.name)) {\n this.exporters.set(exporter.name, exporter);\n }\n\n return this;\n }\n\n public toTrace(report: BaseReport, rootError?: unknown): Trace {\n return reportToTrace(report, rootError, this.options);\n }\n\n public async collect(report: BaseReport, rootError?: unknown): Promise<void> {\n const trace = this.toTrace(report, rootError);\n\n await this.dispatch(trace);\n }\n\n public async flush(): Promise<void> {\n await this.settleAll((exporter) => exporter.flush?.());\n }\n\n public async shutdown(): Promise<void> {\n await this.flush();\n\n await this.settleAll((exporter) => exporter.shutdown?.());\n\n this.exporters.clear();\n }\n\n /**\n * Fan one trace out to every exporter and, when an exporter advertises\n * the per-span hook, deliver every span in the finalized tree to it as\n * well. `exportSpan` is a post-completion per-span hook (not a live /\n * streaming feed — the trace is already finalized): we walk the tree in\n * pre-order with {@link walkSpans} so the exporter sees the root and\n * every descendant exactly once. Every invocation is isolated so a\n * throwing exporter can't abort the dispatch to its siblings or escape\n * into the originating run.\n */\n private async dispatch(trace: Trace): Promise<void> {\n await this.settleAll(async (exporter) => {\n await exporter.export(trace);\n\n if (exporter.exportSpan !== undefined) {\n for (const span of walkSpans(trace.root)) {\n await exporter.exportSpan(span);\n }\n }\n });\n }\n\n /**\n * Run `task` against every registered exporter and wait for all of\n * them to settle, swallowing individual rejections. `Promise.allSettled`\n * guarantees one failure neither rejects the batch nor blocks the\n * others — the contract's failure-isolation requirement.\n */\n private async settleAll(\n task: (exporter: ExporterContract) => void | Promise<void>,\n ): Promise<void> {\n const runs = [...this.exporters.entries()].map(([name, exporter]) =>\n Promise.resolve()\n .then(() => task(exporter))\n .catch((error: unknown) => this.reportExporterError(name, error)),\n );\n\n await Promise.allSettled(runs);\n }\n\n /**\n * Surface an isolated exporter failure. The originating run never sees it\n * (the isolation guarantee holds), but a silent failure is the wrong\n * default for a config error — e.g. a missing optional peer like\n * `langfuse` would otherwise drop every trace with no signal. The\n * supplied `onError` is called, or — by default — a `console.warn` is\n * emitted ONCE per exporter so the cause is visible without spamming.\n */\n private reportExporterError(name: string, error: unknown): void {\n if (this.options.onError) {\n try {\n this.options.onError(name, error);\n } catch {\n // Never let the error handler itself escape into the run.\n }\n return;\n }\n\n if (this.warnedExporters.has(name)) {\n return;\n }\n\n this.warnedExporters.add(name);\n const message = error instanceof Error ? error.message : String(error);\n console.warn(`[panoptic] exporter \"${name}\" failed and was isolated: ${message}`);\n }\n}\n\n/**\n * Create a Panoptic collector — the single integration point an app\n * wires into its agents/workflows (typically via the `onComplete`\n * report hook). Register exporters with `use`, then feed finalized root\n * reports to `collect`.\n *\n * @example\n * const collector = createCollector().use(otelExporter).use(langfuseExporter);\n * agent.on(\"onComplete\", ({ result }) => collector.collect(result.report));\n * // on shutdown:\n * await collector.shutdown();\n */\nexport function createCollector(options: CollectorOptions = {}): CollectorContract {\n return new Collector(options);\n}\n","import type { ReportStatus } from \"@warlock.js/ai\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport type { TraceQuery } from \"./trace-query.type\";\n\n/**\n * Test whether one trace satisfies a {@link TraceQuery}. Every declared\n * filter field must match (logical AND); absent fields are ignored, so\n * an empty / undefined filter matches every trace.\n *\n * Time bounds compare against the trace's root `startedAt`, parsed to\n * an epoch once per call, inclusive on both ends. Status accepts a\n * single value or an array (membership test). Identity fields are exact\n * string equality.\n *\n * Pure — used by the store's `query` and `aggregate` so both share one\n * matching definition.\n */\nexport function matchTrace(trace: Trace, filter?: TraceQuery): boolean {\n if (!filter) {\n return true;\n }\n\n if (filter.traceId !== undefined && trace.traceId !== filter.traceId) {\n return false;\n }\n\n if (filter.sessionId !== undefined && trace.sessionId !== filter.sessionId) {\n return false;\n }\n\n if (filter.status !== undefined && !statusMatches(trace.root.status, filter.status)) {\n return false;\n }\n\n const startedAt = Date.parse(trace.startedAt);\n\n if (filter.startedAfter !== undefined && startedAt < toEpoch(filter.startedAfter)) {\n return false;\n }\n\n if (filter.startedBefore !== undefined && startedAt > toEpoch(filter.startedBefore)) {\n return false;\n }\n\n return true;\n}\n\n/**\n * Membership test for the status filter — true when `status` equals the\n * single wanted value, or is one of the wanted array.\n */\nfunction statusMatches(status: ReportStatus, wanted: ReportStatus | ReportStatus[]): boolean {\n if (Array.isArray(wanted)) {\n return wanted.includes(status);\n }\n\n return status === wanted;\n}\n\n/**\n * Normalize a time bound (ISO string or `Date`) to epoch milliseconds\n * for comparison against a parsed `startedAt`.\n */\nfunction toEpoch(bound: string | Date): number {\n if (bound instanceof Date) {\n return bound.getTime();\n }\n\n return Date.parse(bound);\n}\n","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, next: number | undefined): number | undefined {\n if (accumulator === undefined && next === undefined) {\n return undefined;\n }\n\n return (accumulator ?? 0) + (next ?? 0);\n}\n","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","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.\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 * 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(options?: InMemoryTraceStoreOptions): TraceStoreContract & ExporterContract {\n return new InMemoryTraceStore(options);\n}\n","import 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 field. Mirrors the formula documented on\n * `Usage.cost` (input + output + cachedInput + cachedOutput), and also\n * folds in `reasoning` for forward-safety when a provider prices\n * reasoning tokens as a separate channel. 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 const cost = usage.cost;\n\n if (!cost) {\n return undefined;\n }\n\n return (\n (cost.input ?? 0) +\n (cost.output ?? 0) +\n (cost.cachedInput ?? 0) +\n (cost.cachedOutput ?? 0) +\n (cost.reasoning ?? 0)\n );\n}\n","import type { TraceSpan } from \"../../contracts\";\nimport { totalCostUsd } from \"./total-cost\";\n\n/**\n * Subset of the OpenTelemetry GenAI semantic-convention attribute keys\n * Panoptic emits. Kept as a named constant map (not inline string\n * literals scattered through the mapper) so the convention names live in\n * one place and a convention bump is a single edit.\n *\n * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/\n */\nexport const GEN_AI_ATTRIBUTES = {\n operationName: \"gen_ai.operation.name\",\n system: \"gen_ai.system\",\n requestModel: \"gen_ai.request.model\",\n responseModel: \"gen_ai.response.model\",\n usageInputTokens: \"gen_ai.usage.input_tokens\",\n usageOutputTokens: \"gen_ai.usage.output_tokens\",\n conversationId: \"gen_ai.conversation.id\",\n /** Captured prompt/input (set only under content capture). */\n prompt: \"gen_ai.prompt\",\n /** Captured completion/output (set only under content capture). */\n completion: \"gen_ai.completion\",\n} as const;\n\n/**\n * Panoptic-specific attribute keys that have no GenAI-convention\n * equivalent. Namespaced under `warlock.*` so they never collide with a\n * future `gen_ai.*` key the spec might add.\n */\nexport const WARLOCK_ATTRIBUTES = {\n reportType: \"warlock.report.type\",\n version: \"warlock.version\",\n durationMs: \"warlock.duration_ms\",\n totalTokens: \"gen_ai.usage.total_tokens\",\n cachedTokens: \"gen_ai.usage.cached_tokens\",\n reasoningTokens: \"gen_ai.usage.reasoning_tokens\",\n costUsd: \"warlock.cost.usd\",\n} as const;\n\n/**\n * Span attribute values an OpenTelemetry / Langfuse backend accepts.\n * GenAI attributes are scalars; the framework's free-form\n * `TraceSpan.attributes` may also carry these.\n */\nexport type AttributeValue = string | number | boolean;\n\n/**\n * Project a {@link TraceSpan} onto the OpenTelemetry GenAI\n * semantic-convention attribute set.\n *\n * The vendor-neutral {@link TraceSpan} carries identity, timing, status,\n * and rolled-up `usage` as first-class fields; model identity and other\n * provider detail live in the free-form `attributes` bag the collector\n * populated. This mapper folds both into a flat `gen_ai.*` /\n * `warlock.*` attribute map ready to set on an OTel span or hand to a\n * Langfuse generation.\n *\n * - `gen_ai.operation.name` / `gen_ai.system` / `gen_ai.request.model`\n * are forwarded from the span's `attributes` when the collector set\n * them; never invented here.\n * - Token counts come from the span's typed `usage` rollup.\n * - The free-form `attributes` are merged last so an explicit collector\n * value wins over a derived one.\n *\n * @example\n * const attributes = toGenAiAttributes(span);\n * // { \"gen_ai.usage.input_tokens\": 150, \"gen_ai.usage.output_tokens\": 320, ... }\n */\nexport function toGenAiAttributes(span: TraceSpan): Record<string, AttributeValue> {\n const attributes: Record<string, AttributeValue> = {\n [WARLOCK_ATTRIBUTES.reportType]: span.type,\n [WARLOCK_ATTRIBUTES.durationMs]: span.duration,\n [WARLOCK_ATTRIBUTES.totalTokens]: span.usage.total,\n [GEN_AI_ATTRIBUTES.usageInputTokens]: span.usage.input,\n [GEN_AI_ATTRIBUTES.usageOutputTokens]: span.usage.output,\n };\n\n if (span.version !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.version] = span.version;\n }\n\n if (span.sessionId !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.conversationId] = span.sessionId;\n }\n\n if (span.usage.cachedTokens !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.cachedTokens] = span.usage.cachedTokens;\n }\n\n if (span.usage.reasoningTokens !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.reasoningTokens] = span.usage.reasoningTokens;\n }\n\n const cost = totalCostUsd(span.usage);\n\n if (cost !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.costUsd] = cost;\n }\n\n mergeScalarAttributes(attributes, span.attributes);\n\n return attributes;\n}\n\n/**\n * Copy the scalar entries of a free-form attribute bag onto the target\n * map. Non-scalar values (objects, arrays, functions) are skipped — OTel\n * and Langfuse attribute values must be primitives, and the collector's\n * bag may legitimately hold nested digests that don't belong on a span\n * attribute. Explicit collector values overwrite derived ones.\n */\nfunction mergeScalarAttributes(\n target: Record<string, AttributeValue>,\n source: Record<string, unknown> | undefined,\n): void {\n if (!source) {\n return;\n }\n\n for (const [key, value] of Object.entries(source)) {\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n target[key] = value;\n }\n }\n}\n","import type { TraceSpan } from \"../../contracts\";\n\n/** Default cap on how many characters of captured content a console line shows. */\nconst DEFAULT_IO_MAX_CHARS = 500;\n\n/**\n * Render a span's captured content ({@link TraceSpan.input} /\n * {@link TraceSpan.output}) as extra indented console lines, one level\n * below the span's own line:\n *\n * ```text\n * ok agent \"market-research\" — 1794ms, 224 tok, $0.0008\n * in: Research the market for Acme Coffee Roasters …\n * out: Demand is steady; specialty buyers skew premium …\n * ```\n *\n * Returns `[]` when the span carries no content — capture disabled, or a\n * composite node with no own I/O. Each value is stringified (JSON for\n * non-strings), whitespace-collapsed to stay scannable, and truncated to\n * `maxChars` (default {@link DEFAULT_IO_MAX_CHARS}) with an ellipsis. Use\n * the file exporter for the full, untruncated payload.\n *\n * @example\n * formatSpanIO(toolSpan, 1);\n * // [' in: {\"query\":\"specialty coffee demand\"}', ' out: {\"results\":[…]}']\n */\nexport function formatSpanIO(span: TraceSpan, depth = 0, maxChars = DEFAULT_IO_MAX_CHARS): string[] {\n const lines: string[] = [];\n const indent = \" \".repeat(depth + 1);\n\n if (span.input !== undefined) {\n lines.push(`${indent}in: ${preview(span.input, maxChars)}`);\n }\n\n if (span.output !== undefined) {\n lines.push(`${indent}out: ${preview(span.output, maxChars)}`);\n }\n\n return lines;\n}\n\n/**\n * One-line, length-capped preview of a captured value. Strings pass\n * through; everything else is JSON-stringified (falling back to\n * `String()` on a circular / unstringifiable value). Internal whitespace\n * is collapsed so the preview never breaks the tree layout.\n */\nfunction preview(value: unknown, maxChars: number): string {\n const text = typeof value === \"string\" ? value : stringify(value);\n const collapsed = text.replace(/\\s+/g, \" \").trim();\n\n return collapsed.length > maxChars ? `${collapsed.slice(0, maxChars)}…` : collapsed;\n}\n\nfunction stringify(value: unknown): string {\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n","import type { TraceSpan } from \"../../contracts\";\nimport { totalCostUsd } from \"../utils\";\n\n/**\n * Render a single {@link TraceSpan} as one scannable console line:\n * `<status> <type> \"<name>\" — <duration>ms, <tokens> tok[, $<cost>]`.\n * `depth` controls leading indentation when printing a tree. Pure — no\n * side effects — so it is trivially testable and reused by both the\n * per-trace summary and the per-span streaming line.\n *\n * @example\n * formatSpanLine(span, 1);\n * // ' ok agent \"router\" — 1240ms, 470 tok, $0.0021'\n */\nexport function formatSpanLine(span: TraceSpan, depth = 0): string {\n const indent = \" \".repeat(depth);\n const marker = statusMarker(span.status);\n const cost = totalCostUsd(span.usage);\n const costSuffix = cost === undefined ? \"\" : `, $${cost.toFixed(4)}`;\n\n let line = `${indent}${marker} ${span.type} \"${span.name}\" — ${span.duration}ms, ${span.usage.total} tok${costSuffix}`;\n\n if (span.error) {\n line += ` [${span.error.type}: ${span.error.message}]`;\n }\n\n return line;\n}\n\n/**\n * Short ASCII marker for a span's terminal status. Plain ASCII (no\n * emoji/color codes) so output stays clean in log aggregators and CI.\n */\nfunction statusMarker(status: TraceSpan[\"status\"]): string {\n switch (status) {\n case \"completed\":\n return \"ok\";\n case \"failed\":\n return \"ERR\";\n case \"cancelled\":\n return \"cancel\";\n case \"max-iterations\":\n return \"max-iter\";\n case \"awaiting-input\":\n return \"await\";\n default:\n return status;\n }\n}\n","import type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport { walkSpans } from \"../utils\";\nimport type { ConsoleExporterOptions, ConsoleLike } from \"./console-exporter.type\";\nimport { formatSpanIO } from \"./format-span-io\";\nimport { formatSpanLine } from \"./format-span-line\";\n\nconst EXPORTER_NAME = \"console\";\n\n/**\n * Zero-dependency {@link ExporterContract} that prints traces to a\n * console-like sink. The simplest exporter — useful in development and\n * as the reference implementation of the contract.\n *\n * By default it prints one summary line per completed trace. Set\n * `tree: true` to print the full indented span tree, `io: true` to also\n * print each span's captured `input` / `output` (needs the collector's\n * `captureContent`), and `streaming: true` to print each span the moment\n * it finalizes (via the optional `exportSpan` hook).\n *\n * @example\n * collector.use(consoleExporter());\n * // ok workflow \"checkout\" — 2103ms, 1820 tok, $0.0094\n *\n * @example\n * collector.use(consoleExporter({ tree: true }));\n *\n * @example\n * // Full content trace — prompts, responses, and tool I/O:\n * const observe = panoptic({\n * captureContent: true,\n * exporters: [consoleExporter({ tree: true, io: true })],\n * });\n */\nexport function consoleExporter(options: ConsoleExporterOptions = {}): ExporterContract {\n const sink: ConsoleLike = options.console ?? console;\n const tree = options.tree ?? false;\n const io = options.io ?? false;\n const ioMaxChars = options.ioMaxChars;\n\n const exporter: ExporterContract = {\n name: EXPORTER_NAME,\n export(trace: Trace): void {\n writeTrace(sink, trace, tree, io, ioMaxChars);\n },\n };\n\n if (options.streaming) {\n exporter.exportSpan = (span: TraceSpan): void => {\n sink.log(formatSpanLine(span));\n\n if (io) {\n for (const line of formatSpanIO(span, 0, ioMaxChars)) {\n sink.log(line);\n }\n }\n };\n }\n\n return exporter;\n}\n\n/**\n * Write a completed trace — either a single root summary line or the\n * full indented tree, each span optionally followed by its captured\n * `input` / `output`. Failed / cancelled spans route to `console.error`\n * so they surface at the right severity in log aggregators.\n */\nfunction writeTrace(\n sink: ConsoleLike,\n trace: Trace,\n tree: boolean,\n io: boolean,\n ioMaxChars: number | undefined,\n): void {\n if (!tree) {\n writeSpan(sink, trace.root, 0, io, ioMaxChars);\n return;\n }\n\n for (const span of walkSpans(trace.root)) {\n const depth = spanDepth(trace.root, span.spanId);\n writeSpan(sink, span, depth, io, ioMaxChars);\n }\n}\n\n/**\n * Write one span's line and — when `io` is on — its captured content,\n * all routed at the span's own severity so a failed span keeps its\n * content beside it in the error stream.\n */\nfunction writeSpan(\n sink: ConsoleLike,\n span: TraceSpan,\n depth: number,\n io: boolean,\n ioMaxChars: number | undefined,\n): void {\n writeAtSeverity(sink, span.status, formatSpanLine(span, depth));\n\n if (io) {\n for (const line of formatSpanIO(span, depth, ioMaxChars)) {\n writeAtSeverity(sink, span.status, line);\n }\n }\n}\n\n/**\n * Route a line to `error` when the span failed/cancelled, otherwise to\n * `log`. Keeps healthy traces out of the error stream.\n */\nfunction writeAtSeverity(sink: ConsoleLike, status: TraceSpan[\"status\"], line: string): void {\n if (status === \"failed\" || status === \"cancelled\") {\n sink.error(line);\n return;\n }\n\n sink.log(line);\n}\n\n/**\n * Depth of `targetSpanId` below `root` for indentation. Walks the tree\n * once; returns 0 when the span is the root or not found.\n */\nfunction spanDepth(root: TraceSpan, targetSpanId: string, depth = 0): number {\n if (root.spanId === targetSpanId) {\n return depth;\n }\n\n for (const child of root.children) {\n const found = spanDepth(child, targetSpanId, depth + 1);\n\n if (found > 0) {\n return found;\n }\n }\n\n return 0;\n}\n","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\n ? JSON.stringify(record, undefined, 2)\n : JSON.stringify(record);\n\n return `${json}\\n`;\n }\n}\n","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","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","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(\n (result as { report?: unknown }).report,\n (result as { error?: unknown }).error,\n );\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","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 =\n 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","import { judgePromptBody } from \"@warlock.js/ai\";\nimport type { EvaluateConfig, EvaluateVerdict } from \"./evaluate.type\";\n\n/**\n * Grade `systemPrompt` with the configured judge model, reusing\n * `ai.prompts().validate()`'s own `judgePromptBody` — never a second\n * judging implementation. `instructionsOverride` (the dashboard's\n * per-run textarea) wins over `config.instructions`; with neither, the\n * judge falls back to `judgePromptBody`'s built-in prompt-quality rubric.\n *\n * The judge itself never throws (`judgePromptBody` degrades to an\n * issues-only outcome on failure) — the only thing that CAN throw here is\n * resolving `config.model` (a factory constructing an SDK client), which\n * the caller (the dashboard route) is expected to catch.\n */\nexport async function evaluateSystemPrompt(\n systemPrompt: string,\n config: EvaluateConfig,\n instructionsOverride?: string,\n): Promise<EvaluateVerdict> {\n const model = typeof config.model === \"function\" ? await config.model() : config.model;\n const instructions = instructionsOverride?.trim() || config.instructions;\n\n return judgePromptBody(systemPrompt, model, instructions);\n}\n","import type { TraceSpan } from \"../contracts/trace.type\";\n\n/**\n * Pull the LAST `{role: \"system\"}` message off a span's captured `input`.\n *\n * Only present under `captureContent` (off by default), and only shaped\n * this way for non-tool spans — either the `[system, user]` first-trip pair\n * or, under `fullHistory`, the full `CapturedMessage[]` conversation (see\n * `collector/report-to-span.ts`). \"Last\" (not \"only\") matters for\n * `fullHistory`: a long-running agent can carry more than one system-role\n * turn, and the most recent one is the one actually in effect. Returns\n * `undefined` for a tool span, an agent with no system prompt, or when\n * content capture is off — the caller treats that as \"nothing to evaluate.\"\n */\nexport function extractLastSystemPrompt(span: TraceSpan): string | undefined {\n if (!Array.isArray(span.input)) {\n return undefined;\n }\n\n for (let index = span.input.length - 1; index >= 0; index -= 1) {\n const entry = span.input[index] as { role?: unknown; content?: unknown };\n\n if (entry && typeof entry === \"object\" && entry.role === \"system\") {\n return typeof entry.content === \"string\" ? entry.content : undefined;\n }\n }\n\n return undefined;\n}\n","import type { TraceSpan } from \"../contracts/trace.type\";\n\n/** Depth-first search for the span with `spanId` inside a trace's span tree. */\nexport function findSpanById(root: TraceSpan, spanId: string): TraceSpan | undefined {\n if (root.spanId === spanId) {\n return root;\n }\n\n for (const child of root.children) {\n const found = findSpanById(child, spanId);\n\n if (found !== undefined) {\n return found;\n }\n }\n\n return undefined;\n}\n","import type { ReportStatus } from \"@warlock.js/ai\";\nimport type { TraceQuery } from \"../store/trace-query.type\";\n\n/**\n * The set of terminal statuses a stored trace can carry. Used to keep\n * `parseQuery` from forwarding arbitrary `?status=` junk into the store.\n */\nconst KNOWN_STATUSES: readonly ReportStatus[] = [\n \"completed\",\n \"failed\",\n \"cancelled\",\n \"max-iterations\",\n \"awaiting-input\",\n \"awaiting-approval\",\n] as const;\n\n/**\n * Map a request's query string onto a {@link TraceQuery} the trace store\n * understands. Every field is optional — an absent param is \"don't care\",\n * so an empty query string yields an empty filter that matches every\n * stored trace.\n *\n * - `traceId` / `sessionId` — passed through verbatim (first value wins).\n * - `status` — **repeatable**: `?status=failed&status=cancelled` becomes\n * `[\"failed\", \"cancelled\"]`; a single value stays a scalar. Unknown\n * status tokens are dropped so a typo never silently matches nothing in\n * a confusing way (it simply isn't filtered on).\n * - `startedAfter` / `startedBefore` — forwarded as ISO strings; the\n * store accepts either a string or `Date`, and `matchTrace` does the\n * inclusive bound comparison.\n *\n * Unknown params are ignored. Pure — takes a `URLSearchParams`, returns a\n * plain object — so it's trivially testable without a live server.\n *\n * @example\n * parseQuery(new URLSearchParams(\"status=failed&status=cancelled&sessionId=s1\"));\n * // → { status: [\"failed\", \"cancelled\"], sessionId: \"s1\" }\n */\nexport function parseQuery(params: URLSearchParams): TraceQuery {\n const query: TraceQuery = {};\n\n const traceId = params.get(\"traceId\");\n if (traceId !== null && traceId.length > 0) {\n query.traceId = traceId;\n }\n\n const sessionId = params.get(\"sessionId\");\n if (sessionId !== null && sessionId.length > 0) {\n query.sessionId = sessionId;\n }\n\n const statuses = params\n .getAll(\"status\")\n .filter((value): value is ReportStatus => (KNOWN_STATUSES as readonly string[]).includes(value));\n if (statuses.length === 1) {\n query.status = statuses[0];\n } else if (statuses.length > 1) {\n query.status = statuses;\n }\n\n const startedAfter = params.get(\"startedAfter\");\n if (startedAfter !== null && startedAfter.length > 0) {\n query.startedAfter = startedAfter;\n }\n\n const startedBefore = params.get(\"startedBefore\");\n if (startedBefore !== null && startedBefore.length > 0) {\n query.startedBefore = startedBefore;\n }\n\n return query;\n}\n","/**\n * The Warlock logo as an inlined base64 PNG data URI (64x64, ~9KB).\n * Inlined so the dashboard stays a single self-contained, offline-capable\n * page with no external asset request. Regenerate from\n * @warlock.js/docs/public/logo.png via:\n * magick logo.png -resize 64x64 -strip out.png && base64 -w0 out.png\n */\nexport const WARLOCK_LOGO_DATA_URI =\n \"data:image/png;base64,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\";\n","import { WARLOCK_LOGO_DATA_URI } from \"./warlock-logo\";\n\n/**\n * Build the single self-contained dashboard HTML page. No external\n * assets, no bundler, no framework — one inlined string that polls the\n * read-only JSON API (`{basePath}api/aggregate`, `{basePath}api/traces`,\n * `{basePath}api/traces/:id`) and renders:\n *\n * - the {@link TraceAggregate} headline counts (traces / completed /\n * failed / cancelled / tokens in·out·total / cost);\n * - a newest-first trace list as clickable master rows, each led by a\n * colour-coded, title-cased type label (Supervisor / Agent / Tool / …);\n * - a two-pane drawer: a collapsible call tree on the left (the nested\n * span hierarchy) and the selected node's detail on the right — rich\n * input/output, token breakdown, and a metadata panel (session id,\n * ids, version, attributes).\n *\n * Span input/output is rendered structurally (chat bubbles / key-value /\n * Markdown), durations in seconds, tokens as ↓input · ↑output · total.\n * Theme is light / dark / system (persisted to localStorage). All UI\n * state — selected trace, selected span, collapsed nodes — lives in JS,\n * NOT the DOM, so the 2s poll never disturbs an open drawer.\n *\n * The list is filterable entirely client-side over the polled traces: a\n * free-text search (name + session), status / type / session filter\n * chips, an \"errors only\" header toggle, and an optional group-by-session\n * view with collapsible headers. Each tree node and trace row carries a\n * cost heatmap accent scaled to the trace's most expensive node, with a\n * small legend. The drawer's left pane toggles between the nested call\n * TREE and a Gantt TIMELINE (span offset from root start + duration,\n * critical path highlighted). The selected trace and span are reflected\n * in the URL hash (`#trace=&span=`) and re-opened from it on load — so a\n * drawer view is shareable/bookmarkable. A live socket tail is a noted\n * follow-up; this pass stays on the 2s poll.\n *\n * `basePath` and `title` are baked in at serve time. The page is\n * intentionally dependency-free vanilla JS so it works offline.\n *\n * @param basePath Normalized mount path ending in `/` (e.g. `\"/\"`).\n * @param title Header title shown in the page.\n */\nexport function dashboardHtml(\n basePath: string,\n title: string,\n evaluateEnabled: boolean = false,\n evaluateDefaultInstructions: string = \"\",\n): string {\n const apiBase = `${basePath}api`;\n const safeTitle = escapeHtml(title);\n\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n<title>${safeTitle}</title>\n<script>\n(function () {\n try {\n var m = localStorage.getItem(\"panoptic-theme\") || \"system\";\n var light = m === \"light\" || (m === \"system\" && window.matchMedia && window.matchMedia(\"(prefers-color-scheme: light)\").matches);\n document.documentElement.setAttribute(\"data-theme\", light ? \"light\" : \"dark\");\n } catch (e) {}\n})();\n</script>\n<style>\n :root {\n color-scheme: dark;\n --bg: #0b0d10; --surface: #101317; --surface2: #14171c; --panel: #0d1014;\n --border: #23272e; --border2: #2d333b;\n --text: #e6e8eb; --text2: #adbac7; --dim: #8b949e;\n --sel-bg: #0f1722; --sel-border: #316dca;\n --ty-agent: #539bf5; --ty-tool: #c297ff; --ty-model: #4cc2b0; --ty-prim: #e3b341; --ty-other: #adbac7;\n --tok-in: #58a6ff; --tok-out: #3fb950; --tok-total: #b899ff; --cost: #e3b341;\n --ok: #56d364; --ok-bg: #0f2e1d; --fail: #f85149; --fail-bg: #3a1416; --cancel: #d29922; --cancel-bg: #332701; --other: #8b949e; --other-bg: #1c2128;\n --code-bg: #0b0d10; --inline-bg: #1c2128; --link: #539bf5;\n }\n :root[data-theme=\"light\"] {\n color-scheme: light;\n --bg: #ffffff; --surface: #f6f8fa; --surface2: #eef1f4; --panel: #ffffff;\n --border: #d0d7de; --border2: #afb8c1;\n --text: #1f2328; --text2: #3b4350; --dim: #636c76;\n --sel-bg: #ddf4ff; --sel-border: #0969da;\n --ty-agent: #0969da; --ty-tool: #8250df; --ty-model: #0f7d6b; --ty-prim: #9a6700; --ty-other: #57606a;\n --tok-in: #0969da; --tok-out: #1a7f37; --tok-total: #8250df; --cost: #9a6700;\n --ok: #1a7f37; --ok-bg: #dafbe1; --fail: #cf222e; --fail-bg: #ffebe9; --cancel: #9a6700; --cancel-bg: #fff8c5; --other: #57606a; --other-bg: #eaeef2;\n --code-bg: #f6f8fa; --inline-bg: #eaeef2; --link: #0969da;\n }\n * { box-sizing: border-box; }\n body { margin: 0; font: 14px/1.5 ui-sans-serif, system-ui, sans-serif; background: var(--bg); color: var(--text); }\n header { padding: 14px 20px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 12px; }\n header img.logo { height: 26px; width: 26px; display: block; flex: none; }\n header h1 { font-size: 16px; margin: 0; font-weight: 600; }\n header .meta { color: var(--dim); font-size: 12px; }\n .theme { margin-left: auto; display: flex; gap: 2px; border: 1px solid var(--border); border-radius: 8px; padding: 2px; }\n .theme button { background: transparent; border: none; color: var(--dim); cursor: pointer; font-size: 14px; line-height: 1; padding: 4px 8px; border-radius: 6px; }\n .theme button:hover { color: var(--text); }\n .theme button.active { background: var(--surface2); color: var(--text); }\n .stats { display: flex; flex-wrap: wrap; gap: 10px; padding: 14px 20px; border-bottom: 1px solid var(--border); }\n .stat { background: var(--surface2); border: 1px solid var(--border); border-radius: 8px; padding: 8px 12px; min-width: 84px; }\n .stat .label { color: var(--dim); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }\n .stat .value { font-size: 18px; font-weight: 600; }\n main { padding: 12px 20px 40px; }\n\n .trace-row { border: 1px solid var(--border); border-radius: 8px; margin-bottom: 8px; background: var(--surface); padding: 9px 12px; display: flex; align-items: center; gap: 9px; cursor: pointer; transition: background .12s ease, border-color .12s ease; }\n .trace-row:hover { background: var(--surface2); border-color: var(--border2); }\n .trace-row.selected { border-color: var(--sel-border); background: var(--sel-bg); }\n .rname { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .rprompt { font-size: 11px; color: var(--ty-agent); border: 1px solid var(--border2); border-radius: 6px; padding: 1px 6px; white-space: nowrap; }\n .rright { margin-left: auto; display: flex; align-items: center; gap: 6px; white-space: nowrap; }\n .chev { color: var(--dim); font-size: 16px; font-style: normal; }\n\n .tylabel { font-weight: 600; flex: none; }\n .ty-agent { color: var(--ty-agent); } .ty-tool { color: var(--ty-tool); } .ty-model { color: var(--ty-model); } .ty-prim { color: var(--ty-prim); } .ty-other { color: var(--ty-other); }\n .badge { font-size: 11px; padding: 2px 8px; border-radius: 999px; font-weight: 600; flex: none; }\n .badge.completed { background: var(--ok-bg); color: var(--ok); }\n .badge.failed { background: var(--fail-bg); color: var(--fail); }\n .badge.cancelled { background: var(--cancel-bg); color: var(--cancel); }\n .badge.other { background: var(--other-bg); color: var(--other); }\n .sdot { width: 7px; height: 7px; border-radius: 999px; display: inline-block; flex: none; }\n .sdot-completed { background: var(--ok); } .sdot-failed { background: var(--fail); } .sdot-cancelled { background: var(--cancel); } .sdot-other { background: var(--other); }\n .dim { color: var(--dim); font-size: 12px; }\n .tok { white-space: nowrap; font-size: 12px; }\n .tok-in { color: var(--tok-in); } .tok-out { color: var(--tok-out); } .tok-total { color: var(--tok-total); }\n .cost { color: var(--cost); font-weight: 600; font-size: 12px; white-space: nowrap; }\n\n .io-body { margin-bottom: 6px; }\n .piol { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; margin: 11px 0 3px; }\n .messages { display: flex; flex-direction: column; gap: 8px; }\n .msg-collapse { border: 1px solid var(--border); border-radius: 8px; background: var(--surface); }\n .msg-collapse > summary { cursor: pointer; padding: 7px 10px; list-style: none; font-size: 12px; display: flex; align-items: center; gap: 6px; }\n .msg-collapse > summary::-webkit-details-marker { display: none; }\n .msg-collapse > summary::before { content: \"\\\\25B8\"; color: var(--dim); }\n .msg-collapse[open] > summary::before { content: \"\\\\25BE\"; }\n .msg-collapse[open] > summary { border-bottom: 1px solid var(--border); }\n .msg-collapse .messages { padding: 8px; }\n .msg-collapse .msg-count { font-weight: 600; color: var(--text2); }\n .msg { border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--surface); }\n .msg-role { font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .05em; padding: 4px 10px; color: var(--text2); background: var(--surface2); border-bottom: 1px solid var(--border); }\n .msg-role.role-system { color: var(--cancel); } .msg-role.role-user { color: var(--ty-agent); } .msg-role.role-assistant { color: var(--ok); } .msg-role.role-tool { color: var(--ty-tool); }\n .msg-content { padding: 8px 10px; }\n .kv { display: grid; grid-template-columns: max-content 1fr; gap: 2px 12px; align-items: start; }\n .kv-row { display: contents; }\n .kv-k { color: var(--dim); font-family: ui-monospace, monospace; font-size: 12px; padding: 2px 0; white-space: nowrap; }\n .kv-v { font-size: 13px; min-width: 0; overflow: auto; padding: 1px 0; word-break: break-word; }\n .part { border-left: 2px solid var(--border2); padding-left: 8px; margin: 4px 0; }\n .part-label { font-size: 11px; text-transform: uppercase; color: var(--dim); margin-bottom: 2px; }\n .md-h { font-weight: 700; margin: 8px 0 4px; }\n .md-h1 { font-size: 16px; } .md-h2 { font-size: 14px; } .md-h3 { font-size: 13px; color: var(--text2); } .md-h4 { font-size: 12px; color: var(--dim); }\n .md-p { margin: 4px 0; white-space: pre-wrap; word-break: break-word; }\n .io-body ul, .msg-content ul { margin: 4px 0; padding-left: 18px; }\n pre.code { background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: 8px; overflow: auto; max-height: 320px; font-size: 12px; white-space: pre; margin: 6px 0; }\n pre.mini { background: var(--code-bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; overflow: auto; max-height: 200px; font-size: 12px; margin: 0; }\n code { background: var(--inline-bg); border-radius: 4px; padding: 1px 4px; font-family: ui-monospace, monospace; font-size: 12px; }\n a { color: var(--link); }\n .empty { color: var(--dim); padding: 30px; text-align: center; }\n\n .backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.5); opacity: 0; pointer-events: none; transition: opacity .18s ease; z-index: 40; }\n .backdrop.open { opacity: 1; pointer-events: auto; }\n .drawer { position: fixed; top: 0; right: 0; bottom: 0; width: min(760px, 96vw); background: var(--panel); border-left: 1px solid var(--border); transform: translateX(100%); transition: transform .18s ease; z-index: 50; display: flex; flex-direction: column; box-shadow: -16px 0 40px rgba(0,0,0,.4); }\n .drawer.open { transform: translateX(0); }\n .drawer-head { padding: 12px 14px; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: 9px; flex-wrap: wrap; }\n .drawer-close { margin-left: auto; background: transparent; border: 1px solid var(--border); color: var(--dim); border-radius: 6px; cursor: pointer; font-size: 13px; line-height: 1; padding: 5px 9px; }\n .drawer-close:hover { color: var(--text); border-color: var(--border2); }\n .drawer-body { flex: 1; overflow: hidden; }\n .dsplit { display: flex; height: 100%; }\n .dtree { flex: 0 0 44%; overflow: auto; padding: 8px 6px; border-right: 1px solid var(--border); }\n .ddetail { flex: 1; overflow: auto; padding: 10px 14px; min-width: 0; }\n .tnode { display: flex; align-items: center; gap: 7px; padding: 5px 7px; border-radius: 6px; cursor: pointer; font-size: 13px; border: 1px solid transparent; }\n .tnode:hover { background: var(--surface); }\n .tnode.selected { background: var(--sel-bg); border-color: var(--sel-border); }\n .tname { color: var(--text2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .tmeta { margin-left: auto; color: var(--dim); font-size: 12px; white-space: nowrap; }\n .twisty { color: var(--dim); font-size: 11px; width: 12px; text-align: center; flex: none; }\n .tkids { margin-left: 10px; padding-left: 9px; border-left: 1px solid var(--border); }\n .crumb { font-size: 12px; color: var(--dim); margin-bottom: 6px; word-break: break-all; }\n .crumb .sep { color: var(--border2); }\n .dhead-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 4px; }\n .meta-sec { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 8px; }\n .meta-title { font-size: 11px; text-transform: uppercase; letter-spacing: .05em; color: var(--dim); margin-bottom: 6px; }\n\n /* Toolbar: search box + filter chips over the trace list. */\n .toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 10px 20px; border-bottom: 1px solid var(--border); }\n .search { flex: 1 1 220px; min-width: 160px; background: var(--surface2); border: 1px solid var(--border); color: var(--text); border-radius: 8px; padding: 7px 10px; font: inherit; }\n .search:focus { outline: none; border-color: var(--sel-border); }\n .chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }\n .chip { background: var(--surface2); border: 1px solid var(--border); color: var(--text2); border-radius: 999px; padding: 4px 11px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease; }\n .chip:hover { color: var(--text); border-color: var(--border2); }\n .chip.active { background: var(--sel-bg); border-color: var(--sel-border); color: var(--text); }\n .chip-clear { color: var(--dim); border-style: dashed; }\n .chip-group { display: inline-flex; gap: 6px; align-items: center; }\n .chip-group .gl { font-size: 11px; text-transform: uppercase; letter-spacing: .04em; color: var(--dim); }\n .toggle { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; color: var(--text2); user-select: none; }\n .toggle input { accent-color: var(--sel-border); }\n .toggle select { background: var(--surface2); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 2px 6px; font-size: 12px; cursor: pointer; }\n .toggle select:hover { border-color: var(--border2); }\n\n /* Session grouping headers. */\n .sgroup { margin-bottom: 10px; }\n .sgroup-head { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: 8px; background: var(--surface2); border: 1px solid var(--border); cursor: pointer; margin-bottom: 6px; }\n .sgroup-head:hover { border-color: var(--border2); }\n .sgroup-tw { color: var(--dim); font-size: 11px; width: 12px; text-align: center; flex: none; }\n .sgroup-id { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .sgroup-count { margin-left: auto; color: var(--dim); font-size: 12px; }\n .sgroup-body { padding-left: 6px; }\n\n /* Per-type aggregate stats panel (a CSS-grid table above the trace list). */\n .stats-table { margin: 0 0 14px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; font-size: 12px; }\n .strow { display: grid; grid-template-columns: 1.5fr 0.7fr 1fr 0.8fr 0.8fr 1fr 1fr; gap: 10px; align-items: center; padding: 6px 12px; border-bottom: 1px solid var(--border); }\n .strow:last-child { border-bottom: none; }\n .strow.sthead { background: var(--surface2); color: var(--dim); font-weight: 600; }\n .strow > span { text-align: right; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }\n .strow > .stc-type { text-align: left; }\n .stc-fail.fail { color: var(--fail); }\n\n /* Cost heatmap: a left accent bar tinted by relative rollup cost. */\n .trace-row { position: relative; }\n .tnode { position: relative; }\n .heat { position: absolute; left: 0; top: 3px; bottom: 3px; width: 3px; border-radius: 2px; background: var(--cost); }\n\n /* Timeline / waterfall view in the drawer. */\n .dview { display: flex; gap: 4px; padding: 6px 8px; border-bottom: 1px solid var(--border); }\n .dview button { background: transparent; border: 1px solid var(--border); color: var(--dim); cursor: pointer; font-size: 12px; padding: 4px 10px; border-radius: 6px; }\n .dview button:hover { color: var(--text); }\n .dview button.active { background: var(--surface2); color: var(--text); border-color: var(--border2); }\n .legend { margin-left: auto; display: flex; align-items: center; gap: 6px; font-size: 11px; color: var(--dim); }\n .legend .grad { width: 64px; height: 8px; border-radius: 4px; background: linear-gradient(90deg, var(--surface2), var(--cost)); border: 1px solid var(--border); }\n .gantt { padding: 8px 10px; }\n\n /* Evaluate — the drawer's one write action (config-gated). */\n .eval-btn { background: transparent; border: 1px solid var(--border); color: var(--text2); cursor: pointer; font-size: 12px; padding: 4px 10px; border-radius: 6px; margin: 10px 0 0; }\n .eval-btn:hover:not(:disabled) { color: var(--text); border-color: var(--border2); }\n .eval-btn:disabled { opacity: .6; cursor: default; }\n .eval-panel { margin-top: 8px; padding: 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface2); }\n .eval-panel textarea { width: 100%; min-height: 64px; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 8px; font: 12px/1.5 ui-sans-serif, system-ui, sans-serif; resize: vertical; }\n .eval-actions { display: flex; align-items: center; gap: 8px; margin-top: 8px; }\n .eval-error { color: var(--fail); font-size: 12px; }\n .eval-result { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border); font-size: 12px; }\n .eval-score { font-weight: 600; }\n .eval-issues { margin: 6px 0 0; padding-left: 18px; }\n .grow { display: flex; align-items: center; gap: 8px; padding: 2px 0; font-size: 12px; cursor: pointer; border-radius: 4px; }\n .grow:hover { background: var(--surface); }\n .grow.selected { background: var(--sel-bg); }\n .glabel { flex: 0 0 38%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text2); }\n .gtrack { position: relative; flex: 1; height: 14px; background: var(--surface2); border-radius: 4px; overflow: hidden; }\n .gbar { position: absolute; top: 2px; bottom: 2px; border-radius: 3px; background: var(--ty-agent); min-width: 2px; }\n .gbar.crit { background: var(--fail); }\n .gbar.bar-completed { background: var(--ty-agent); } .gbar.bar-failed { background: var(--fail); } .gbar.bar-cancelled { background: var(--cancel); } .gbar.bar-other { background: var(--other); }\n .gdur { flex: 0 0 auto; color: var(--dim); white-space: nowrap; min-width: 44px; text-align: right; }\n .gantt-legend { display: flex; gap: 14px; margin-top: 8px; font-size: 11px; color: var(--dim); }\n .gantt-legend .ck { display: inline-flex; align-items: center; gap: 5px; }\n .gantt-legend .sw { width: 12px; height: 8px; border-radius: 2px; display: inline-block; }\n\n @media (max-width: 560px) {\n .dsplit { flex-direction: column; }\n .dtree { flex: none; max-height: 42vh; border-right: none; border-bottom: 1px solid var(--border); }\n .glabel { flex-basis: 30%; }\n }\n</style>\n</head>\n<body>\n<header>\n <img class=\"logo\" src=\"${WARLOCK_LOGO_DATA_URI}\" alt=\"Warlock\" />\n <h1>${safeTitle}</h1>\n <span class=\"meta\" id=\"meta\">connecting…</span>\n <div class=\"theme\" id=\"theme\" role=\"group\" aria-label=\"Theme\" style=\"margin-left:auto\">\n <button type=\"button\" data-theme-set=\"light\" title=\"Light\" aria-label=\"Light theme\">☀</button>\n <button type=\"button\" data-theme-set=\"dark\" title=\"Dark\" aria-label=\"Dark theme\">☾</button>\n <button type=\"button\" data-theme-set=\"system\" title=\"System\" aria-label=\"System theme\">◐</button>\n </div>\n</header>\n<div class=\"stats\" id=\"stats\"></div>\n<div class=\"toolbar\" id=\"toolbar\">\n <input class=\"search\" id=\"search\" type=\"search\" placeholder=\"Search name or session…\" aria-label=\"Search traces\" autocomplete=\"off\" />\n <div class=\"chips\" id=\"status-chips\" role=\"group\" aria-label=\"Filter by status\"></div>\n <div class=\"chips\" id=\"type-chips\" role=\"group\" aria-label=\"Filter by type\"></div>\n <div class=\"chips\" id=\"session-chips\" role=\"group\" aria-label=\"Filter by session\"></div>\n <div class=\"chips\" id=\"prompt-chips\" role=\"group\" aria-label=\"Filter by prompt version\"></div>\n <label class=\"toggle\" id=\"group-wrap\" title=\"Group the trace list (mutually exclusive)\">\n Group\n <select id=\"group-by\" aria-label=\"Group the trace list\">\n <option value=\"\">None</option>\n <option value=\"session\">Session</option>\n <option value=\"prompt\">Prompt</option>\n <option value=\"type\">Type</option>\n </select>\n </label>\n <label class=\"toggle\" id=\"stats-wrap\" title=\"Show a per-type aggregate stats panel (count, failure rate, p50/p95 latency, tokens, cost)\">\n <input type=\"checkbox\" id=\"show-stats\" /> Stats\n </label>\n <button class=\"chip chip-clear\" id=\"clear-filters\" type=\"button\" title=\"Clear all filters\">Clear</button>\n</div>\n<div id=\"stats-panel\"></div>\n<main id=\"traces\"><div class=\"empty\">Loading…</div></main>\n\n<div class=\"backdrop\" id=\"backdrop\"></div>\n<aside class=\"drawer\" id=\"drawer\" aria-hidden=\"true\" aria-label=\"Trace detail\">\n <div class=\"drawer-head\" id=\"drawer-head\"></div>\n <div class=\"drawer-body\" id=\"drawer-body\"></div>\n</aside>\n\n<script>\n(function () {\n var API = ${JSON.stringify(apiBase)};\n var EVALUATE_ENABLED = ${JSON.stringify(evaluateEnabled)};\n var BT = String.fromCharCode(96);\n var EVALUATE_DEFAULT_INSTRUCTIONS = ${encodeForInlineScript(evaluateDefaultInstructions)};\n\n // Carry the ?token= the page itself was loaded with onto every\n // subsequent poll as an Authorization header — otherwise the API calls\n // below inherit no auth and 401 forever once authToken is configured\n // (the initial page load is the only request the URL's query string\n // naturally reaches; the server only honors ?token= on that one route,\n // see serve.ts). The token itself is kept in its own nested closure,\n // not a plain var sitting alongside the rest of this file's top-level\n // state, so it isn't trivially reachable from other code sharing this\n // script's outer scope; only the fetchAuthed function it returns is.\n var fetchAuthed = (function () {\n var TOKEN = new URLSearchParams(window.location.search).get(\"token\");\n return function fetchAuthed(url, options) {\n var opts = options || {};\n var headers = opts.headers || {};\n if (TOKEN) headers = Object.assign({}, headers, { Authorization: \"Bearer \" + TOKEN });\n return fetch(url, Object.assign({}, opts, { headers: headers }));\n };\n })();\n\n var state = {\n traces: [], selectedId: null, selectedSpanId: null, collapsed: {}, sig: null,\n // Client-side filter state (search box + chips + errors-only header toggle).\n filter: { text: \"\", statuses: {}, types: {}, sessionId: null, promptKey: null, errorsOnly: false },\n groupBySession: false, // session-grouping list toggle\n groupByPrompt: false, // prompt-version-grouping list toggle\n groupByType: false, // root-type-grouping list toggle\n showStats: false, // per-type aggregate-stats panel toggle (independent of grouping)\n collapsedGroups: {}, // collapsed group headers (session, prompt, or type)\n view: \"tree\", // drawer left pane: \"tree\" | \"timeline\"\n hashApplied: false, // guards one-time deep-link open on load\n evaluate: {} // per-span evaluate UI state, keyed by spanId\n };\n\n var STATUS_FILTERS = [\"completed\", \"failed\", \"cancelled\"];\n var TYPE_FILTERS = [\"agent\", \"tool\", \"model\", \"supervisor\", \"team\", \"workflow\", \"orchestrator\", \"planner\", \"batch\", \"callback\"];\n var ERROR_STATUSES = { failed: 1, cancelled: 1 };\n var NO_SESSION_KEY = \"(no session)\";\n var NO_PROMPT_KEY = \"(no prompt)\";\n var NO_TYPE_KEY = \"(no type)\";\n\n var statusClass = function (s) {\n if (s === \"completed\" || s === \"failed\" || s === \"cancelled\") return s;\n return \"other\";\n };\n var esc = function (v) {\n return String(v).replace(/[&<>\"']/g, function (c) {\n return { \"&\": \"&\", \"<\": \"<\", \">\": \">\", '\"': \""\", \"'\": \"'\" }[c];\n });\n };\n var num = function (n) { return (n == null ? 0 : n).toLocaleString(); };\n var fmt = function (v) {\n if (v == null) return \"\";\n if (typeof v === \"string\") return v;\n try { return JSON.stringify(v, null, 2); } catch (e) { return String(v); }\n };\n\n function dur(ms) {\n if (ms == null) return \"—\";\n if (ms >= 1000) return (ms / 1000).toFixed(1) + \"s\";\n if (ms >= 1) return Math.round(ms) + \"ms\";\n return ms.toFixed(1) + \"ms\";\n }\n\n function tokens(u) {\n if (!u) return \"\";\n var i = u.input || 0, o = u.output || 0, t = (u.total != null) ? u.total : (i + o);\n if (i === 0 && o === 0 && t === 0) return \"\"; // nothing to show — hide the row\n return '<span class=\"tok\">'\n + '<span class=\"tok-in\" title=\"input tokens\">↓ ' + num(i) + \"</span> \"\n + '<span class=\"tok-out\" title=\"output tokens\">↑ ' + num(o) + \"</span> \"\n + '<span class=\"tok-total\" title=\"total tokens\">' + num(t) + \" total</span></span>\";\n }\n\n var TYPE_LABELS = {\n agent: \"Agent\", tool: \"Tool\", model: \"Model\", supervisor: \"Supervisor\",\n workflow: \"Workflow\", orchestrator: \"Orchestrator\", team: \"Team\",\n planner: \"Planner\", prompt: \"Prompt\", guardrail: \"Guardrail\"\n };\n var PRIMITIVES = { supervisor: 1, workflow: 1, orchestrator: 1, team: 1, planner: 1 };\n function typeClass(type) {\n if (PRIMITIVES[type]) return \"ty-prim\";\n if (type === \"agent\") return \"ty-agent\";\n if (type === \"tool\") return \"ty-tool\";\n if (type === \"model\") return \"ty-model\";\n return \"ty-other\";\n }\n function typeLabel(type) {\n var t = type || \"node\";\n var label = TYPE_LABELS[t] || (t.charAt(0).toUpperCase() + t.slice(1));\n return '<span class=\"tylabel ' + typeClass(t) + '\">' + esc(label) + \"</span>\";\n }\n // Plain-text (no span wrapper) capitalized labels for bare-text sites —\n // group headers, filter-chip labels — where the colored typeLabel span is\n // not wanted. Underlying keys/classes/filter values stay raw lowercase.\n function typeText(type) {\n var t = type || \"node\";\n return TYPE_LABELS[t] || (t.charAt(0).toUpperCase() + t.slice(1));\n }\n function statusText(s) {\n return s ? String(s).charAt(0).toUpperCase() + String(s).slice(1) : s;\n }\n function statusDot(s) { return '<span class=\"sdot sdot-' + statusClass(s) + '\" title=\"' + esc(s) + '\"></span>'; }\n\n function findTrace(id) {\n for (var i = 0; i < state.traces.length; i++) if (state.traces[i].traceId === id) return state.traces[i];\n return null;\n }\n function findSpan(span, id) {\n if (span.spanId === id) return span;\n var k = span.children || [];\n for (var i = 0; i < k.length; i++) { var r = findSpan(k[i], id); if (r) return r; }\n return null;\n }\n function findPath(span, id, acc) {\n var p = (acc || []).concat([span]);\n if (span.spanId === id) return p;\n var k = span.children || [];\n for (var i = 0; i < k.length; i++) { var r = findPath(k[i], id, p); if (r) return r; }\n return null;\n }\n function countSpans(span) {\n var n = 1, k = span.children || [];\n for (var i = 0; i < k.length; i++) n += countSpans(k[i]);\n return n;\n }\n function traceSig(t) { return t.root.status + \"|\" + countSpans(t.root) + \"|\" + t.duration + \"|\" + (t.usage && t.usage.total); }\n // Sum every priced lane of a cost object into one USD number.\n function costSumObj(c) {\n if (!c) return 0;\n return (c.input || 0) + (c.output || 0) + (c.cachedInput || 0) + (c.cachedOutput || 0) + (c.reasoning || 0);\n }\n // The cost a single span directly carries (on its rolled-up usage).\n function usageCost(usage) { return usage ? costSumObj(usage.cost) : 0; }\n // Rollup-aware subtree cost: take a node's own cost when it has one\n // (it already rolls up its trips); otherwise sum the children. This\n // avoids double-counting on wrapper nodes (workflow/supervisor roots\n // carry tokens but no cost, so we descend to the priced agent/model).\n function rollupCost(span) {\n var own = usageCost(span.usage);\n if (own > 0) return own;\n var sum = 0;\n (span.children || []).forEach(function (c) { sum += rollupCost(c); });\n return sum;\n }\n function traceCost(t) {\n var explicit = costSumObj(t.cost);\n return explicit > 0 ? explicit : rollupCost(t.root);\n }\n // Format a USD amount; tiny per-node costs need more decimals to read.\n function money(n) {\n if (!n) return \"$0\";\n return \"$\" + (n < 0.01 ? n.toFixed(6) : n.toFixed(4));\n }\n\n // --- Client-side filtering / grouping / heatmap ----------------------\n // These mirror the pure, unit-tested helpers in trace-filter.ts. Keep\n // the two in sync: trace-filter.ts is the spec, this is its inlined twin.\n function anySelected(map) {\n for (var k in map) { if (map[k]) return true; }\n return false;\n }\n function matchesFilter(t) {\n var f = state.filter, root = t.root;\n if (f.errorsOnly && !ERROR_STATUSES[root.status]) return false;\n if (anySelected(f.statuses) && !f.statuses[root.status]) return false;\n if (anySelected(f.types) && !f.types[root.type]) return false;\n if (f.sessionId && t.sessionId !== f.sessionId) return false;\n if (f.promptKey && tracePromptKey(t) !== f.promptKey) return false;\n var text = (f.text || \"\").trim().toLowerCase();\n if (text) {\n var hay = (String(root.name) + \" \" + (t.sessionId || \"\")).toLowerCase();\n if (hay.indexOf(text) === -1) return false;\n }\n return true;\n }\n function filteredTraces() {\n var out = [];\n for (var i = 0; i < state.traces.length; i++) if (matchesFilter(state.traces[i])) out.push(state.traces[i]);\n return out;\n }\n function groupBySession(list) {\n var order = [], byKey = {};\n for (var i = 0; i < list.length; i++) {\n var key = list[i].sessionId || NO_SESSION_KEY;\n if (!byKey[key]) { byKey[key] = []; order.push(key); }\n byKey[key].push(list[i]);\n }\n return order.map(function (k) { return { sessionId: k, traces: byKey[k] }; });\n }\n // Group by prompt version (name@version) — the second group-by dimension\n // beside session. Mirrors groupByPrompt in trace-filter.ts. Unlinked runs\n // bucket under NO_PROMPT_KEY so they stay visible.\n function groupByPrompt(list) {\n var order = [], byKey = {};\n for (var i = 0; i < list.length; i++) {\n var key = tracePromptKey(list[i]) || NO_PROMPT_KEY;\n if (!byKey[key]) { byKey[key] = []; order.push(key); }\n byKey[key].push(list[i]);\n }\n return order.map(function (k) { return { groupKey: k, traces: byKey[k] }; });\n }\n // Group by root type (agent/workflow/supervisor/planner/…) — the coarsest\n // group-by dimension. Mirrors groupByType in trace-filter.ts. root.type is\n // always present, so the NO_TYPE_KEY bucket is only a defensive fallback.\n function typeGroups(list) {\n var order = [], byKey = {};\n for (var i = 0; i < list.length; i++) {\n var key = (list[i].root && list[i].root.type) || NO_TYPE_KEY;\n if (!byKey[key]) { byKey[key] = []; order.push(key); }\n byKey[key].push(list[i]);\n }\n return order.map(function (k) { return { groupKey: k, traces: byKey[k] }; });\n }\n // Inlined twins of percentile + aggregateByType in trace-filter.ts (the\n // spec). Power the per-type stats panel from the same filtered list the\n // trace view renders, so the panel honors active filters with no API call.\n function percentile(values, p) {\n if (!values.length) return 0;\n var sorted = values.slice().sort(function (a, b) { return a - b; });\n var rank = Math.ceil((p / 100) * sorted.length) - 1;\n var index = Math.min(Math.max(rank, 0), sorted.length - 1);\n return sorted[index];\n }\n function aggregateByType(list) {\n var order = [], byKey = {};\n for (var i = 0; i < list.length; i++) {\n var key = (list[i].root && list[i].root.type) || NO_TYPE_KEY;\n if (!byKey[key]) { byKey[key] = []; order.push(key); }\n byKey[key].push(list[i]);\n }\n return order.map(function (k) {\n var bucket = byKey[k];\n var durations = [], failed = 0, tokens = 0, cost = 0;\n for (var j = 0; j < bucket.length; j++) {\n var t = bucket[j];\n durations.push(t.duration);\n if (ERROR_STATUSES[t.root.status]) failed += 1;\n tokens += (t.usage && t.usage.total) || 0;\n cost += traceCost(t);\n }\n return {\n type: k, count: bucket.length, failed: failed,\n failRate: bucket.length ? failed / bucket.length : 0,\n p50: percentile(durations, 50), p95: percentile(durations, 95),\n tokens: tokens, cost: cost\n };\n });\n }\n // Per-type aggregate panel above the trace list. Honors the active filters\n // (computed over filteredTraces) and re-renders on every poll/filter tick.\n function renderStatsPanel() {\n var host = document.getElementById(\"stats-panel\");\n if (!host) return;\n var stats = state.showStats ? aggregateByType(filteredTraces()) : [];\n if (!stats.length) { host.innerHTML = \"\"; return; }\n var head = '<div class=\"strow sthead\">'\n + '<span class=\"stc-type\">Type</span><span>Count</span><span>Failed</span>'\n + '<span>p50</span><span>p95</span><span>Tokens</span><span>Cost</span></div>';\n var rows = stats.map(function (s) {\n var failTxt = s.failed\n ? s.failed + \" (\" + Math.round(s.failRate * 100) + \"%)\"\n : \"0\";\n return '<div class=\"strow\">'\n + '<span class=\"stc-type\">' + typeLabel(s.type) + \"</span>\"\n + '<span>' + num(s.count) + \"</span>\"\n + '<span class=\"stc-fail' + (s.failed ? \" fail\" : \"\") + '\">' + failTxt + \"</span>\"\n + '<span class=\"dim\">' + dur(s.p50) + \"</span>\"\n + '<span class=\"dim\">' + dur(s.p95) + \"</span>\"\n + '<span>' + num(s.tokens) + \"</span>\"\n + '<span class=\"cost\">' + money(s.cost) + \"</span>\"\n + \"</div>\";\n }).join(\"\");\n host.innerHTML = '<div class=\"stats-table\">' + head + rows + \"</div>\";\n }\n // Largest single-node rollup cost in a subtree — heatmap denominator.\n function maxNodeCost(span) {\n var max = rollupCost(span);\n (span.children || []).forEach(function (c) { var m = maxNodeCost(c); if (m > max) max = m; });\n return max;\n }\n // Intensity in [0,1] of a node's cost vs the trace max. Free trace → 0.\n function heatIntensity(nodeCost, maxCost) {\n if (maxCost <= 0 || nodeCost <= 0) return 0;\n var r = nodeCost / maxCost;\n return r > 1 ? 1 : r;\n }\n // The distinct sessionIds present across the polled traces, first-seen\n // order, capped so the chip row never overflows the toolbar.\n function presentSessions() {\n var seen = {}, out = [];\n for (var i = 0; i < state.traces.length && out.length < 12; i++) {\n var s = state.traces[i].sessionId;\n if (s && !seen[s]) { seen[s] = 1; out.push(s); }\n }\n return out;\n }\n // The root types actually present in the polled traces — so the type filter\n // chips show only what exists (no dead \"Tool\"/\"Model\"/\"Batch\"/… chips), in\n // canonical TYPE_FILTERS order. An active-but-aged-out selection stays so\n // the filter is never stranded with no chip to clear it.\n function presentTypes() {\n var seen = {};\n for (var i = 0; i < state.traces.length; i++) {\n var t = state.traces[i].root && state.traces[i].root.type;\n if (t) seen[t] = 1;\n }\n for (var k in state.filter.types) { if (state.filter.types[k]) seen[k] = 1; }\n var out = [];\n for (var j = 0; j < TYPE_FILTERS.length; j++) {\n if (seen[TYPE_FILTERS[j]]) { out.push(TYPE_FILTERS[j]); delete seen[TYPE_FILTERS[j]]; }\n }\n for (var x in seen) { out.push(x); }\n return out;\n }\n // The distinct prompt name@version keys present across the polled traces,\n // first-seen order, capped so the chip row never overflows the toolbar.\n function presentPrompts() {\n var seen = {}, out = [];\n for (var i = 0; i < state.traces.length && out.length < 12; i++) {\n var p = tracePromptKey(state.traces[i]);\n if (p && !seen[p]) { seen[p] = 1; out.push(p); }\n }\n return out;\n }\n\n // (S5) Markdown link URLs come from captured prompt/tool text — attacker-\n // influenced content — so only http:, https:, mailto: and relative/anchor\n // URLs may become an href; javascript:, data:, vbscript:, etc. must not.\n // The check runs on the URL as the browser will act on it: undo the\n // entities esc() introduced (the HTML parser decodes them exactly once in\n // the attribute), strip the control chars / whitespace browsers ignore\n // when parsing a scheme (java\\\\tscript:), and lowercase. Returns the\n // original (still-escaped) URL when safe, or null to drop the link.\n function sanitizeHref(url) {\n var probe = url.replace(/&(amp|lt|gt|quot|#39);/g, function (m, name) {\n return { amp: \"&\", lt: \"<\", gt: \">\", quot: '\"', \"#39\": \"'\" }[name];\n });\n probe = probe.replace(/[\\\\u0000-\\\\u0020\\\\u007f]+/g, \"\").toLowerCase();\n if (/^[a-z][a-z0-9+.-]*:/.test(probe)) {\n return /^(https?|mailto):/.test(probe) ? url : null;\n }\n if (/^[\\\\/\\\\\\\\]{2}/.test(probe)) return null; // scheme-relative smuggles a foreign host\n return url;\n }\n function mdInline(s) {\n s = s.replace(/\\\\*\\\\*([^*]+)\\\\*\\\\*/g, \"<strong>$1</strong>\");\n var codeRe = new RegExp(BT + \"([^\" + BT + \"]+)\" + BT, \"g\");\n s = s.replace(codeRe, \"<code>$1</code>\");\n s = s.replace(/\\\\[([^\\\\]]+)\\\\]\\\\(([^)]+)\\\\)/g, function (m, label, url) {\n var href = sanitizeHref(url);\n if (href === null) return label; // unsafe scheme: label as plain text, no <a>\n return '<a href=\"' + href + '\" target=\"_blank\" rel=\"noopener\">' + label + \"</a>\";\n });\n return s;\n }\n function mdToHtml(raw) {\n var src = esc(String(raw));\n var fence = BT + BT + BT;\n var html = \"\", idx = 0;\n while (true) {\n var start = src.indexOf(fence, idx);\n if (start === -1) { html += mdBlocks(src.slice(idx)); break; }\n html += mdBlocks(src.slice(idx, start));\n var nl = src.indexOf(\"\\\\n\", start + 3);\n var bodyStart = (nl === -1) ? start + 3 : nl + 1;\n var end = src.indexOf(fence, bodyStart);\n if (end === -1) { html += mdBlocks(src.slice(start)); break; }\n html += '<pre class=\"code\">' + src.slice(bodyStart, end).replace(/\\\\n$/, \"\") + \"</pre>\";\n idx = end + 3;\n }\n return html;\n }\n function mdBlocks(src) {\n var lines = src.split(\"\\\\n\"), html = \"\", inList = false;\n function closeList() { if (inList) { html += \"</ul>\"; inList = false; } }\n for (var i = 0; i < lines.length; i++) {\n var ln = lines[i];\n var h = ln.match(/^(#{1,4})\\\\s+(.*)$/);\n if (h) { closeList(); html += '<div class=\"md-h md-h' + h[1].length + '\">' + mdInline(h[2]) + \"</div>\"; continue; }\n var li = ln.match(/^\\\\s*[-*]\\\\s+(.*)$/);\n if (li) { if (!inList) { html += \"<ul>\"; inList = true; } html += \"<li>\" + mdInline(li[1]) + \"</li>\"; continue; }\n if (ln.trim() === \"\") { closeList(); continue; }\n closeList();\n html += '<div class=\"md-p\">' + mdInline(ln) + \"</div>\";\n }\n closeList();\n return html;\n }\n function renderKv(obj) {\n var keys = Object.keys(obj);\n if (!keys.length) return '<span class=\"dim\">{}</span>';\n return '<div class=\"kv\">' + keys.map(function (k) {\n var v = obj[k], vs;\n if (v === null || v === undefined) vs = '<span class=\"dim\">null</span>';\n else if (typeof v === \"object\") vs = '<pre class=\"mini\">' + esc(fmt(v)) + \"</pre>\";\n else vs = esc(String(v));\n return '<div class=\"kv-row\"><span class=\"kv-k\">' + esc(k) + '</span><span class=\"kv-v\">' + vs + \"</span></div>\";\n }).join(\"\") + \"</div>\";\n }\n function isMessageArray(v) {\n return Array.isArray(v) && v.length > 0 && v.every(function (m) { return m && typeof m === \"object\" && typeof m.role === \"string\"; });\n }\n function renderContent(c) {\n if (c == null) return '<span class=\"dim\">—</span>';\n if (typeof c === \"string\") return mdToHtml(c);\n if (Array.isArray(c)) return c.map(renderPart).join(\"\");\n if (typeof c === \"object\") return renderKv(c);\n return esc(String(c));\n }\n function renderPart(p) {\n if (p == null) return \"\";\n if (typeof p === \"string\") return mdToHtml(p);\n if (p.type === \"text\" && typeof p.text === \"string\") return mdToHtml(p.text);\n return '<div class=\"part\"><div class=\"part-label\">' + esc(p.type || \"part\") + \"</div>\" + renderKv(p) + \"</div>\";\n }\n function previewText(c) {\n if (c == null) return \"\";\n if (typeof c === \"string\") return c.replace(/\\\\s+/g, \" \").slice(0, 70);\n if (Array.isArray(c)) {\n for (var i = 0; i < c.length; i++) {\n var p = c[i];\n if (typeof p === \"string\") return p.slice(0, 70);\n if (p && p.type === \"text\" && p.text) return String(p.text).slice(0, 70);\n }\n return \"\";\n }\n try { return JSON.stringify(c).slice(0, 70); } catch (e) { return \"\"; }\n }\n function renderMsg(m) {\n var role = m.role || \"msg\";\n var body = (m.content !== undefined) ? renderContent(m.content) : renderKv(m);\n return '<div class=\"msg\"><div class=\"msg-role role-' + esc(role) + '\">' + esc(role) + '</div><div class=\"msg-content\">' + body + \"</div></div>\";\n }\n function renderMessages(arr) {\n var inner = '<div class=\"messages\">' + arr.map(renderMsg).join(\"\") + \"</div>\";\n // Short threads render inline; a long history (10-15+ messages) collapses\n // behind a <details> so it doesn't blow up the detail pane — the summary\n // shows the count + a preview of the latest message; click to expand.\n if (arr.length <= 6) return inner;\n var last = arr[arr.length - 1] || {};\n var preview = (last.role ? last.role + \": \" : \"\") + previewText(last.content);\n return '<details class=\"msg-collapse\"><summary><span class=\"msg-count\">' + arr.length\n + ' messages</span> <span class=\"dim\">' + esc(preview) + \"</span></summary>\" + inner + \"</details>\";\n }\n function smartValue(v) {\n if (v === null || v === undefined) return '<span class=\"dim\">—</span>';\n if (typeof v === \"string\") {\n var t = v.trim();\n if (t.charAt(0) === \"{\" || t.charAt(0) === \"[\") { try { return smartValue(JSON.parse(t)); } catch (e) {} }\n return mdToHtml(v);\n }\n if (isMessageArray(v)) return renderMessages(v);\n if (Array.isArray(v)) {\n return '<div class=\"kv\">' + v.map(function (item, i) {\n var vs = (item && typeof item === \"object\") ? renderKv(item) : esc(String(item));\n return '<div class=\"kv-row\"><span class=\"kv-k\">' + i + '</span><span class=\"kv-v\">' + vs + \"</span></div>\";\n }).join(\"\") + \"</div>\";\n }\n if (typeof v === \"object\") return renderKv(v);\n return esc(String(v));\n }\n\n function renderStats(a) {\n var cost = a.cost ? (((a.cost.input || 0) + (a.cost.output || 0) + (a.cost.cachedInput || 0) + (a.cost.cachedOutput || 0)).toFixed(4)) : \"—\";\n var u = a.usage || {};\n var cells = [\n [\"traces\", num(a.traces), null],\n [\"completed\", num(a.completed), null],\n [\"failed\", num(a.failed), null],\n [\"cancelled\", num(a.cancelled), null],\n [\"↓ tokens in\", num(u.input), \"var(--tok-in)\"],\n [\"↑ tokens out\", num(u.output), \"var(--tok-out)\"],\n [\"tokens total\", num(u.total), \"var(--tok-total)\"],\n [\"cost (usd)\", cost, null]\n ];\n document.getElementById(\"stats\").innerHTML = cells.map(function (c) {\n var st = c[2] ? ' style=\"color:' + c[2] + '\"' : \"\";\n return '<div class=\"stat\"><div class=\"label\"' + st + \">\" + c[0] + '</div><div class=\"value\"' + st + \">\" + esc(c[1]) + \"</div></div>\";\n }).join(\"\");\n }\n\n // Per-row heat accent: tint the left edge by the trace's own cost\n // relative to the most expensive trace currently in the (filtered) list.\n function heatStyle(intensity) {\n if (intensity <= 0) return \"\";\n var op = (0.12 + intensity * 0.88).toFixed(3);\n return ' style=\"--heat-op:' + op + '\"';\n }\n function heatBar(intensity) {\n if (intensity <= 0) return \"\";\n var op = (0.12 + intensity * 0.88).toFixed(3);\n return '<span class=\"heat\" style=\"opacity:' + op + '\" title=\"relative cost\"></span>';\n }\n function renderRow(t, maxTraceCost) {\n var sel = t.traceId === state.selectedId ? \" selected\" : \"\";\n var intensity = heatIntensity(traceCost(t), maxTraceCost);\n var pk = tracePromptKey(t);\n return '<div class=\"trace-row' + sel + '\" data-id=\"' + esc(t.traceId) + '\">'\n + heatBar(intensity)\n + typeLabel(t.root.type)\n + '<span class=\"badge ' + statusClass(t.root.status) + '\">' + esc(statusText(t.root.status)) + \"</span>\"\n + '<span class=\"rname\">' + esc(t.root.name) + \"</span>\"\n + (pk ? '<span class=\"rprompt\" title=\"prompt version\">' + esc(pk) + \"</span>\" : \"\")\n + (t.sessionId ? '<span class=\"dim\">' + esc(t.sessionId) + \"</span>\" : \"\")\n + '<span class=\"rright\">' + tokens(t.usage) + '<span class=\"dim\">· ' + dur(t.duration) + \"</span></span>\"\n + '<i class=\"chev\">›</i>'\n + \"</div>\";\n }\n function maxTraceCostOf(list) {\n var max = 0;\n for (var i = 0; i < list.length; i++) { var c = traceCost(list[i]); if (c > max) max = c; }\n return max;\n }\n // Render grouped buckets. \"groups\" is the normalized list each group-by\n // dimension produces — a groupKey + its traces. Shared by session, prompt,\n // and type. Optional \"labelFn\" maps the raw groupKey to a display label and\n // switches the header to the compact \"Label (N)\" form (used by type, whose\n // keys are friendly enums); session/prompt omit it and keep their raw id\n // plus the right-aligned \"N trace(s)\" count. data-group stays the RAW key\n // so collapse state keys consistently regardless of the display label.\n function renderGroups(groups, maxTraceCost, labelFn) {\n return groups.map(function (g) {\n var collapsed = !!state.collapsedGroups[g.groupKey];\n var idAndCount = labelFn\n ? '<span class=\"sgroup-id\">' + esc(labelFn(g.groupKey)) + \" (\" + g.traces.length + \")</span>\"\n : '<span class=\"sgroup-id\">' + esc(g.groupKey) + \"</span>\"\n + '<span class=\"sgroup-count\">' + g.traces.length + \" trace(s)</span>\";\n var head = '<div class=\"sgroup-head\" data-group=\"' + esc(g.groupKey) + '\">'\n + '<span class=\"sgroup-tw\">' + (collapsed ? \"▸\" : \"▾\") + \"</span>\"\n + idAndCount + \"</div>\";\n var body = collapsed ? \"\" : '<div class=\"sgroup-body\">'\n + g.traces.map(function (t) { return renderRow(t, maxTraceCost); }).join(\"\") + \"</div>\";\n return '<div class=\"sgroup\">' + head + body + \"</div>\";\n }).join(\"\");\n }\n // Normalize a session group ({sessionId,…}) to the shared {groupKey,…} shape.\n function sessionGroups(list) {\n return groupBySession(list).map(function (g) { return { groupKey: g.sessionId, traces: g.traces }; });\n }\n function renderList() {\n var list = filteredTraces();\n // Keep the per-type stats panel in sync — runs on every poll + filter\n // change, before the empty-state early return below.\n renderStatsPanel();\n var host = document.getElementById(\"traces\");\n if (!list.length) {\n host.innerHTML = state.traces.length\n ? '<div class=\"empty\">No traces match the current filters.</div>'\n : '<div class=\"empty\">No traces yet. Run an observed flow and they will appear here.</div>';\n return;\n }\n var maxTraceCost = maxTraceCostOf(list);\n // One grouping dimension renders at a time. The toggles are kept mutually\n // exclusive in their change handlers, so this precedence chain (most\n // specific → coarsest: prompt → session → type) only ever matches one.\n var html;\n if (state.groupByPrompt) html = renderGroups(groupByPrompt(list), maxTraceCost);\n else if (state.groupBySession) html = renderGroups(sessionGroups(list), maxTraceCost);\n else if (state.groupByType) html = renderGroups(typeGroups(list), maxTraceCost, typeText);\n else html = list.map(function (t) { return renderRow(t, maxTraceCost); }).join(\"\");\n host.innerHTML = html;\n document.getElementById(\"meta\").textContent = list.length + \" of \" + state.traces.length + \" trace(s) · live\";\n }\n\n // --- Filter UI rendering ---------------------------------------------\n function renderChips() {\n var f = state.filter;\n document.getElementById(\"status-chips\").innerHTML =\n '<span class=\"chip-group\"><span class=\"gl\">status</span>'\n + STATUS_FILTERS.map(function (s) {\n return '<button type=\"button\" class=\"chip' + (f.statuses[s] ? \" active\" : \"\") + '\" data-status=\"' + esc(s) + '\">' + esc(statusText(s)) + \"</button>\";\n }).join(\"\")\n + '<button type=\"button\" class=\"chip' + (f.errorsOnly ? \" active\" : \"\") + '\" data-errors=\"1\" title=\"Show only failed / cancelled traces\">Errors only</button>'\n + \"</span>\";\n var types = presentTypes();\n document.getElementById(\"type-chips\").innerHTML = types.length\n ? '<span class=\"chip-group\"><span class=\"gl\">type</span>'\n + types.map(function (ty) {\n return '<button type=\"button\" class=\"chip' + (f.types[ty] ? \" active\" : \"\") + '\" data-type=\"' + esc(ty) + '\">' + esc(typeText(ty)) + \"</button>\";\n }).join(\"\") + \"</span>\"\n : \"\";\n var sessions = presentSessions();\n document.getElementById(\"session-chips\").innerHTML = sessions.length\n ? '<span class=\"chip-group\"><span class=\"gl\">session</span>'\n + sessions.map(function (s) {\n return '<button type=\"button\" class=\"chip' + (f.sessionId === s ? \" active\" : \"\") + '\" data-session=\"' + esc(s) + '\">' + esc(s) + \"</button>\";\n }).join(\"\") + \"</span>\"\n : \"\";\n // Prompt-version filter chips — one per distinct name@version seen.\n // Hidden entirely until a named-prompt run shows up, so the toolbar stays\n // clean for projects that don't use the ai.prompts registry.\n var prompts = presentPrompts();\n document.getElementById(\"prompt-chips\").innerHTML = prompts.length\n ? '<span class=\"chip-group\"><span class=\"gl\">prompt</span>'\n + prompts.map(function (p) {\n return '<button type=\"button\" class=\"chip' + (f.promptKey === p ? \" active\" : \"\") + '\" data-prompt=\"' + esc(p) + '\">' + esc(p) + \"</button>\";\n }).join(\"\") + \"</span>\"\n : \"\";\n }\n\n // Heatmap denominator for the currently rendered trace tree. Set by\n // renderDrawer before each renderTree pass so node accents are scaled\n // against the most expensive node in this trace.\n var currentTreeMax = 0;\n function renderTree(span) {\n var hasKids = span.children && span.children.length;\n var collapsed = !!state.collapsed[span.spanId];\n var sel = span.spanId === state.selectedSpanId ? \" selected\" : \"\";\n var tw = hasKids ? (collapsed ? \"▸\" : \"▾\") : \"·\";\n var node = '<div class=\"tnode' + sel + '\" data-span=\"' + esc(span.spanId) + '\">'\n + heatBar(heatIntensity(rollupCost(span), currentTreeMax))\n + '<span class=\"twisty\"' + (hasKids ? ' data-toggle=\"' + esc(span.spanId) + '\"' : \"\") + \">\" + tw + \"</span>\"\n + typeLabel(span.type)\n + '<span class=\"tname\">' + esc(span.name) + \"</span>\"\n + statusDot(span.status)\n + '<span class=\"tmeta\">' + dur(span.duration) + \"</span>\"\n + \"</div>\";\n var kids = (hasKids && !collapsed) ? '<div class=\"tkids\">' + span.children.map(renderTree).join(\"\") + \"</div>\" : \"\";\n return node + kids;\n }\n\n // --- Timeline / waterfall (Gantt) ------------------------------------\n // Flatten the span tree to a depth-first ordered list, each entry\n // carrying its offset (ms from root start) + duration, so we can lay\n // out concurrency without re-walking. parseTs tolerates a missing/bad\n // startedAt by falling back to 0 so a bad clock never NaNs the bars.\n function parseTs(s) { var n = Date.parse(s); return isNaN(n) ? 0 : n; }\n // Render an ISO timestamp for the detail drawer as a readable, locale-\n // unambiguous local time — \"28 Jun 2026 03:16 PM\" (named month so there is\n // no M/D vs D/M confusion; minute precision). Falls back to the raw string\n // on a bad clock.\n var MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"];\n function pad2(n) { return (n < 10 ? \"0\" : \"\") + n; }\n function fmtTs(iso) {\n if (!iso) return \"—\";\n var d = new Date(iso);\n if (isNaN(d.getTime())) return String(iso);\n var h = d.getHours();\n var ampm = h >= 12 ? \"PM\" : \"AM\";\n var h12 = h % 12;\n if (h12 === 0) h12 = 12;\n return d.getDate() + \" \" + MONTHS[d.getMonth()] + \" \" + d.getFullYear()\n + \" \" + pad2(h12) + \":\" + pad2(d.getMinutes()) + \" \" + ampm;\n }\n function flattenSpans(root) {\n var base = parseTs(root.startedAt);\n var rows = [];\n (function walk(span, depth) {\n var start = parseTs(span.startedAt) - base;\n if (start < 0) start = 0;\n var d = (span.duration != null) ? span.duration : 0;\n rows.push({ span: span, depth: depth, offset: start, duration: d, end: start + d });\n (span.children || []).forEach(function (c) { walk(c, depth + 1); });\n })(root, 0);\n return rows;\n }\n // Critical path: from the root, repeatedly step into the child whose\n // end time is latest (the one that pushed the parent's finish). Marks\n // the spans that determine total wall-clock time.\n function criticalPath(root) {\n var crit = {};\n (function walk(span) {\n crit[span.spanId] = 1;\n var kids = span.children || [];\n if (!kids.length) return;\n var pick = null, pe = -1;\n for (var i = 0; i < kids.length; i++) {\n var e = parseTs(kids[i].startedAt) + ((kids[i].duration != null) ? kids[i].duration : 0);\n if (e > pe) { pe = e; pick = kids[i]; }\n }\n if (pick) walk(pick);\n })(root);\n return crit;\n }\n function renderGantt(root) {\n var rows = flattenSpans(root);\n var span0 = rows.length ? rows[0] : null;\n var total = 0;\n rows.forEach(function (r) { if (r.end > total) total = r.end; });\n if (total <= 0) total = (span0 && span0.duration) || 1;\n var crit = criticalPath(root);\n var body = rows.map(function (r) {\n var leftPct = (r.offset / total) * 100;\n var widthPct = Math.max((r.duration / total) * 100, 0.6);\n var sel = r.span.spanId === state.selectedSpanId ? \" selected\" : \"\";\n var isCrit = crit[r.span.spanId] ? \" crit\" : \"\";\n var barClass = \"gbar bar-\" + statusClass(r.span.status) + (isCrit ? \" crit\" : \"\");\n var pad = \"padding-left:\" + (r.depth * 10) + \"px\";\n return '<div class=\"grow' + sel + '\" data-span=\"' + esc(r.span.spanId) + '\">'\n + '<span class=\"glabel\" style=\"' + pad + '\" title=\"' + esc(r.span.name) + '\">' + esc(r.span.name) + \"</span>\"\n + '<span class=\"gtrack\"><span class=\"' + barClass + '\" style=\"left:' + leftPct.toFixed(2) + \"%;width:\" + widthPct.toFixed(2) + '%\"></span></span>'\n + '<span class=\"gdur\">' + dur(r.duration) + \"</span>\"\n + \"</div>\";\n }).join(\"\");\n var legend = '<div class=\"gantt-legend\">'\n + '<span class=\"ck\"><span class=\"sw\" style=\"background:var(--fail)\"></span> critical path</span>'\n + '<span class=\"ck\"><span class=\"sw\" style=\"background:var(--ty-agent)\"></span> span (offset + duration)</span>'\n + \"</div>\";\n return '<div class=\"gantt\">' + (body || '<div class=\"dim\">No spans.</div>') + legend + \"</div>\";\n }\n\n // The name@version of the named prompt this span's run resolved, read\n // from the collector's prompt-version-linkage attributes. Returns null when\n // the run carried no named prompt. Mirrors tracePromptKey in trace-filter.ts.\n function spanPromptKey(span) {\n var a = span && span.attributes;\n if (!a || typeof a !== \"object\") return null;\n var name = a[\"agent.promptName\"];\n if (typeof name !== \"string\" || !name.length) return null;\n var ver = a[\"agent.promptVersion\"];\n var vl = (typeof ver === \"string\" && ver.length) ? ver : \"1\";\n return name + \"@\" + vl;\n }\n function tracePromptKey(t) { return spanPromptKey(t.root); }\n\n // Humanize a metadata key for display: split dot.notation + camelCase,\n // Title-case each word, upcase \"id\". e.g. \"supervisor.terminatedBy\" →\n // \"Supervisor Terminated By\", \"span id\" → \"Span ID\", \"agent.trips\" →\n // \"Agent Trips\". Underlying attribute keys are untouched.\n // NB: this whole script is a template literal — regex backslash classes\n // MUST be double-escaped (\\\\s, not \\s) or \"\\\\s\" collapses to a literal \"s\".\n function humanizeKey(key) {\n return String(key).split(/[.\\\\s]+/).map(function (seg) {\n return seg.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\").split(/\\\\s+/).map(function (w) {\n if (!w) return w;\n if (w.toLowerCase() === \"id\") return \"ID\";\n return w.charAt(0).toUpperCase() + w.slice(1);\n }).join(\" \");\n }).join(\" \");\n }\n\n function renderMeta(span, trace) {\n var rows = [];\n // Absolute wall-clock span — the head shows only the elapsed duration, so\n // surface when this node actually started/ended for log correlation.\n rows.push([\"started\", fmtTs(span.startedAt)]);\n rows.push([\"ended\", fmtTs(span.endedAt)]);\n var sid = span.sessionId || trace.sessionId;\n if (sid) rows.push([\"session\", sid]);\n // Prompt-version linkage: surface the resolved named prompt as one clean\n // name@version row right under session, so the panel reads it as a\n // first-class dimension rather than two raw attribute keys.\n var pk = spanPromptKey(span);\n if (pk) rows.push([\"prompt\", pk]);\n if (span.version) rows.push([\"version\", span.version]);\n rows.push([\"span id\", span.spanId]);\n if (span.parentSpanId) rows.push([\"parent\", span.parentSpanId]);\n rows.push([\"trace id\", span.traceId || trace.traceId]);\n var attrs = span.attributes;\n // Skip the two raw prompt keys — already shown as the clean prompt row.\n if (attrs && typeof attrs === \"object\") Object.keys(attrs).forEach(function (k) {\n if (k === \"agent.promptName\" || k === \"agent.promptVersion\") return;\n rows.push([k, attrs[k]]);\n });\n var kv = rows.map(function (r) {\n var v = r[1];\n var vs = (v && typeof v === \"object\") ? '<pre class=\"mini\">' + esc(fmt(v)) + \"</pre>\" : esc(String(v));\n return '<div class=\"kv-row\"><span class=\"kv-k\">' + esc(humanizeKey(r[0])) + '</span><span class=\"kv-v\">' + vs + \"</span></div>\";\n }).join(\"\");\n return '<div class=\"meta-sec\"><div class=\"meta-title\">metadata</div><div class=\"kv\">' + kv + \"</div></div>\";\n }\n\n // --- Evaluate: grade a span's last captured system prompt -------------\n // Config-gated (EVALUATE_ENABLED) — the drawer's only write action, POSTing\n // to a route that itself only exists when the server was configured with\n // evaluate. UI state lives in state.evaluate, keyed by spanId, so it\n // survives a re-render (e.g. switching to a sibling span and back).\n function extractLastSystemPrompt(span) {\n if (!Array.isArray(span.input)) return null;\n for (var i = span.input.length - 1; i >= 0; i--) {\n var m = span.input[i];\n if (m && typeof m === \"object\" && m.role === \"system\" && typeof m.content === \"string\") return m.content;\n }\n return null;\n }\n function evalState(spanId) {\n return state.evaluate[spanId] || (state.evaluate[spanId] = {\n open: false, instructions: EVALUATE_DEFAULT_INSTRUCTIONS, status: \"idle\", result: null, error: null\n });\n }\n function evalResultHtml(result) {\n var score = typeof result.score === \"number\" ? Math.round(result.score * 100) + \"%\" : \"n/a\";\n var issues = (result.issues || []).map(function (i) { return \"<li>\" + esc(i) + \"</li>\"; }).join(\"\");\n return '<div class=\"eval-result\"><span class=\"eval-score\">Score: ' + score + \"</span>\"\n + (issues ? '<ul class=\"eval-issues\">' + issues + \"</ul>\" : \"\") + \"</div>\";\n }\n function evalSectionHtml(span) {\n if (!EVALUATE_ENABLED) return \"\";\n var sysPrompt = extractLastSystemPrompt(span);\n if (!sysPrompt) return \"\";\n var st = evalState(span.spanId);\n var btn = '<button type=\"button\" class=\"eval-btn\" data-evaluate-toggle=\"' + esc(span.spanId) + '\">'\n + (st.open ? \"Hide evaluate\" : \"Evaluate system prompt\") + \"</button>\";\n if (!st.open) return btn;\n var running = st.status === \"running\";\n return btn + '<div class=\"eval-panel\">'\n + '<textarea id=\"eval-instructions\" placeholder=\"Grading instructions (optional — falls back to the configured default)\"' + (running ? \" disabled\" : \"\") + \">\" + esc(st.instructions || \"\") + \"</textarea>\"\n + '<div class=\"eval-actions\">'\n + '<button type=\"button\" class=\"eval-btn\" data-evaluate-run=\"' + esc(span.spanId) + '\"' + (running ? \" disabled\" : \"\") + \">\" + (running ? \"Evaluating…\" : \"Run\") + \"</button>\"\n + (st.error ? '<span class=\"eval-error\">' + esc(st.error) + \"</span>\" : \"\")\n + \"</div>\"\n + (st.result ? evalResultHtml(st.result) : \"\")\n + \"</div>\";\n }\n function rerenderDetail() {\n var t = findTrace(state.selectedId);\n if (!t) return;\n var sel = findSpan(t.root, state.selectedSpanId) || t.root;\n document.getElementById(\"drawer-detail\").innerHTML = renderDetail(sel, t);\n }\n function toggleEvalPanel(spanId) {\n evalState(spanId).open = !evalState(spanId).open;\n rerenderDetail();\n }\n function runEvaluate(traceId, spanId) {\n var st = evalState(spanId);\n var textarea = document.getElementById(\"eval-instructions\");\n if (textarea) st.instructions = textarea.value;\n st.status = \"running\"; st.error = null;\n rerenderDetail();\n fetchAuthed(API + \"/traces/\" + encodeURIComponent(traceId) + \"/spans/\" + encodeURIComponent(spanId) + \"/evaluate\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ instructions: st.instructions })\n }).then(function (r) {\n return r.json().then(function (data) { return { ok: r.ok, data: data }; });\n }).then(function (res) {\n if (res.ok) { st.status = \"done\"; st.result = res.data; st.error = null; }\n else { st.status = \"error\"; st.error = (res.data && (res.data.message || res.data.error)) || \"evaluate failed\"; st.result = null; }\n rerenderDetail();\n }).catch(function () {\n st.status = \"error\"; st.error = \"network error\"; st.result = null;\n rerenderDetail();\n });\n }\n\n function renderDetail(span, trace) {\n var path = findPath(trace.root, span.spanId) || [span];\n var crumb = path.map(function (p, i) { return (i ? '<span class=\"sep\"> › </span>' : \"\") + \"<span>\" + esc(p.name) + \"</span>\"; }).join(\"\");\n var io = \"\";\n if (span.input !== undefined) io += '<div class=\"piol\" style=\"color:var(--tok-in)\">input</div><div class=\"io-body\">' + smartValue(span.input) + \"</div>\";\n if (span.output !== undefined) io += '<div class=\"piol\" style=\"color:var(--tok-out)\">output</div><div class=\"io-body\">' + smartValue(span.output) + \"</div>\";\n if (span.error) io += '<div class=\"piol\" style=\"color:var(--fail)\">error</div><div class=\"io-body\">' + smartValue(span.error) + \"</div>\";\n\n // Tokens + cost line — omitted entirely when the node has neither\n // (e.g. a free tool with zero usage), so the panel stays uncluttered.\n var tk = tokens(span.usage);\n var c = rollupCost(span);\n var metaLine = (tk || c > 0)\n ? '<div style=\"margin-bottom:4px\">' + tk + (c > 0 ? '<span class=\"cost\">' + (tk ? \" · \" : \"\") + money(c) + \"</span>\" : \"\") + \"</div>\"\n : \"\";\n\n return '<div class=\"crumb\">' + crumb + \"</div>\"\n + '<div class=\"dhead-row\">' + typeLabel(span.type) + '<span style=\"font-weight:600\">' + esc(span.name) + \"</span>\"\n + '<span class=\"badge ' + statusClass(span.status) + '\">' + esc(statusText(span.status)) + \"</span>\"\n + '<span class=\"dim\">· ' + dur(span.duration) + \"</span></div>\"\n + metaLine\n + io\n + evalSectionHtml(span, trace)\n + renderMeta(span, trace);\n }\n\n function headHtml(t) {\n var cost = traceCost(t);\n return typeLabel(t.root.type)\n + '<span class=\"badge ' + statusClass(t.root.status) + '\">' + esc(statusText(t.root.status)) + \"</span>\"\n + '<span style=\"font-weight:600\">' + esc(t.root.name) + \"</span>\"\n + '<span class=\"dim\">· ' + dur(t.duration) + \"</span>\"\n + tokens(t.usage)\n + (cost > 0 ? '<span class=\"cost\">· ' + money(cost) + \"</span>\" : \"\")\n + '<button class=\"drawer-close\" type=\"button\" title=\"Close (Esc)\">✕ Close</button>';\n }\n\n function viewSwitcher() {\n var tree = state.view === \"tree\" ? \" active\" : \"\";\n var tl = state.view === \"timeline\" ? \" active\" : \"\";\n return '<div class=\"dview\" id=\"drawer-view\">'\n + '<button type=\"button\" class=\"' + tree.trim() + '\" data-view=\"tree\">Tree</button>'\n + '<button type=\"button\" class=\"' + tl.trim() + '\" data-view=\"timeline\">Timeline</button>'\n + '<span class=\"legend\" title=\"Node colour = relative cost\"><span>cost</span><span class=\"grad\"></span></span>'\n + \"</div>\";\n }\n function leftPaneHtml(t) {\n if (state.view === \"timeline\") return renderGantt(t.root);\n currentTreeMax = maxNodeCost(t.root);\n return renderTree(t.root);\n }\n function renderDrawer(t) {\n document.getElementById(\"drawer-head\").innerHTML = headHtml(t);\n var sel = findSpan(t.root, state.selectedSpanId) || t.root;\n document.getElementById(\"drawer-body\").innerHTML =\n '<div class=\"dsplit\"><div class=\"dtree\" id=\"drawer-tree\">' + viewSwitcher() + leftPaneHtml(t) + \"</div>\"\n + '<div class=\"ddetail\" id=\"drawer-detail\">' + renderDetail(sel, t) + \"</div></div>\";\n }\n // Re-render only the left pane (after a view switch) without disturbing\n // the detail panel or scroll position of the detail side.\n function renderLeftPane() {\n var t = findTrace(state.selectedId);\n if (!t) return;\n document.getElementById(\"drawer-tree\").innerHTML = viewSwitcher() + leftPaneHtml(t);\n }\n\n function selectSpan(id) {\n state.selectedSpanId = id;\n var t = findTrace(state.selectedId);\n if (!t) return;\n document.getElementById(\"drawer-detail\").innerHTML = renderDetail(findSpan(t.root, id) || t.root, t);\n // Highlight in whichever left pane is active (tree nodes or Gantt rows).\n var nodes = document.querySelectorAll(\".tnode[data-span], .grow[data-span]\");\n for (var i = 0; i < nodes.length; i++) {\n if (nodes[i].getAttribute(\"data-span\") === id) nodes[i].classList.add(\"selected\");\n else nodes[i].classList.remove(\"selected\");\n }\n writeHash();\n }\n function toggleSpan(id) {\n state.collapsed[id] = !state.collapsed[id];\n var t = findTrace(state.selectedId);\n if (t) renderLeftPane();\n }\n function setView(v) {\n if (state.view === v) return;\n state.view = v;\n renderLeftPane();\n }\n\n function openTrace(id, spanId) {\n state.selectedId = id;\n var t = findTrace(id);\n if (t) {\n state.selectedSpanId = (spanId && findSpan(t.root, spanId)) ? spanId : t.root.spanId;\n state.collapsed = {};\n renderDrawer(t);\n state.sig = traceSig(t);\n }\n document.getElementById(\"drawer\").classList.add(\"open\");\n document.getElementById(\"drawer\").setAttribute(\"aria-hidden\", \"false\");\n document.getElementById(\"backdrop\").classList.add(\"open\");\n markSelectedRow();\n writeHash();\n }\n function closeDrawer() {\n state.selectedId = null; state.selectedSpanId = null; state.sig = null;\n document.getElementById(\"drawer\").classList.remove(\"open\");\n document.getElementById(\"drawer\").setAttribute(\"aria-hidden\", \"true\");\n document.getElementById(\"backdrop\").classList.remove(\"open\");\n markSelectedRow();\n writeHash();\n }\n\n // --- Deep-links: reflect the open trace + span in the URL hash -------\n // #trace=<id>&span=<id>. Written on open/close/select; read on load and\n // on manual hash edits (back/forward). A guard flag stops writeHash from\n // re-triggering our own hashchange handler in a loop.\n var suppressHash = false;\n function writeHash() {\n var h = \"\";\n if (state.selectedId) {\n h = \"#trace=\" + encodeURIComponent(state.selectedId);\n if (state.selectedSpanId && state.selectedSpanId !== state.selectedId) {\n h += \"&span=\" + encodeURIComponent(state.selectedSpanId);\n }\n }\n suppressHash = true;\n try {\n if (history && history.replaceState) history.replaceState(null, \"\", h || (location.pathname + location.search));\n else location.hash = h;\n } catch (e) { location.hash = h; }\n suppressHash = false;\n }\n function readHash() {\n var raw = (location.hash || \"\").replace(/^#/, \"\");\n var out = { trace: null, span: null };\n raw.split(\"&\").forEach(function (kv) {\n var i = kv.indexOf(\"=\");\n if (i === -1) return;\n var k = kv.slice(0, i), v = decodeURIComponent(kv.slice(i + 1));\n if (k === \"trace\") out.trace = v;\n else if (k === \"span\") out.span = v;\n });\n return out;\n }\n // Open whatever the hash points at, if that trace is loaded. Returns\n // true when it acted so the caller can mark the one-time load as done.\n function applyHash() {\n var h = readHash();\n if (!h.trace) {\n if (state.selectedId) closeDrawer();\n return true;\n }\n if (!findTrace(h.trace)) return false; // not polled yet — retry next poll\n openTrace(h.trace, h.span || undefined);\n return true;\n }\n function markSelectedRow() {\n var rows = document.querySelectorAll(\".trace-row\");\n for (var i = 0; i < rows.length; i++) {\n if (rows[i].getAttribute(\"data-id\") === state.selectedId) rows[i].classList.add(\"selected\");\n else rows[i].classList.remove(\"selected\");\n }\n }\n\n document.getElementById(\"traces\").addEventListener(\"click\", function (e) {\n var head = e.target.closest ? e.target.closest(\".sgroup-head\") : null;\n if (head) {\n var g = head.getAttribute(\"data-group\");\n state.collapsedGroups[g] = !state.collapsedGroups[g];\n renderList();\n return;\n }\n var row = e.target.closest ? e.target.closest(\".trace-row\") : null;\n if (row) openTrace(row.getAttribute(\"data-id\"));\n });\n document.getElementById(\"drawer-head\").addEventListener(\"click\", function (e) {\n if (e.target.closest && e.target.closest(\".drawer-close\")) closeDrawer();\n });\n document.getElementById(\"drawer-body\").addEventListener(\"click\", function (e) {\n var vb = e.target.closest ? e.target.closest(\"[data-view]\") : null;\n if (vb) { setView(vb.getAttribute(\"data-view\")); return; }\n var tog = e.target.closest ? e.target.closest(\"[data-toggle]\") : null;\n if (tog) { toggleSpan(tog.getAttribute(\"data-toggle\")); return; }\n var evalToggle = e.target.closest ? e.target.closest(\"[data-evaluate-toggle]\") : null;\n if (evalToggle) { toggleEvalPanel(evalToggle.getAttribute(\"data-evaluate-toggle\")); return; }\n var evalRun = e.target.closest ? e.target.closest(\"[data-evaluate-run]\") : null;\n if (evalRun && state.selectedId) { runEvaluate(state.selectedId, evalRun.getAttribute(\"data-evaluate-run\")); return; }\n var node = e.target.closest ? e.target.closest(\".tnode[data-span], .grow[data-span]\") : null;\n if (node) selectSpan(node.getAttribute(\"data-span\"));\n });\n // Track the instructions textarea live (no re-render on keystroke, so\n // typing never loses focus/cursor position).\n document.getElementById(\"drawer-body\").addEventListener(\"input\", function (e) {\n if (e.target && e.target.id === \"eval-instructions\" && state.selectedSpanId) {\n evalState(state.selectedSpanId).instructions = e.target.value;\n }\n });\n document.getElementById(\"backdrop\").addEventListener(\"click\", closeDrawer);\n document.addEventListener(\"keydown\", function (e) { if (e.key === \"Escape\" || e.keyCode === 27) closeDrawer(); });\n\n // --- Filter / toolbar wiring -----------------------------------------\n function toggleMapKey(map, key) { if (map[key]) delete map[key]; else map[key] = 1; }\n document.getElementById(\"status-chips\").addEventListener(\"click\", function (e) {\n if (!e.target.closest) return;\n // The \"Errors only\" shortcut chip lives in the status group now.\n if (e.target.closest(\"[data-errors]\")) {\n state.filter.errorsOnly = !state.filter.errorsOnly;\n renderChips(); renderList();\n return;\n }\n var b = e.target.closest(\"[data-status]\");\n if (!b) return;\n toggleMapKey(state.filter.statuses, b.getAttribute(\"data-status\"));\n renderChips(); renderList();\n });\n document.getElementById(\"type-chips\").addEventListener(\"click\", function (e) {\n var b = e.target.closest ? e.target.closest(\"[data-type]\") : null;\n if (!b) return;\n toggleMapKey(state.filter.types, b.getAttribute(\"data-type\"));\n renderChips(); renderList();\n });\n document.getElementById(\"session-chips\").addEventListener(\"click\", function (e) {\n var b = e.target.closest ? e.target.closest(\"[data-session]\") : null;\n if (!b) return;\n var s = b.getAttribute(\"data-session\");\n state.filter.sessionId = (state.filter.sessionId === s) ? null : s;\n renderChips(); renderList();\n });\n document.getElementById(\"prompt-chips\").addEventListener(\"click\", function (e) {\n var b = e.target.closest ? e.target.closest(\"[data-prompt]\") : null;\n if (!b) return;\n var p = b.getAttribute(\"data-prompt\");\n state.filter.promptKey = (state.filter.promptKey === p) ? null : p;\n renderChips(); renderList();\n });\n document.getElementById(\"search\").addEventListener(\"input\", function (e) {\n state.filter.text = e.target.value || \"\";\n renderList();\n });\n // One mutually-exclusive grouping dimension, chosen from the Group dropdown.\n // \"\" = no grouping; renderList's precedence chain only ever matches one.\n // Switching dimensions drops stale collapsed-header keys.\n function setGrouping(dim) {\n state.groupBySession = dim === \"session\";\n state.groupByPrompt = dim === \"prompt\";\n state.groupByType = dim === \"type\";\n state.collapsedGroups = {};\n renderList();\n }\n document.getElementById(\"group-by\").addEventListener(\"change\", function (e) {\n setGrouping(e.target.value);\n });\n document.getElementById(\"show-stats\").addEventListener(\"change\", function (e) {\n state.showStats = !!e.target.checked;\n renderStatsPanel();\n });\n document.getElementById(\"clear-filters\").addEventListener(\"click\", function () {\n state.filter = { text: \"\", statuses: {}, types: {}, sessionId: null, promptKey: null, errorsOnly: false };\n document.getElementById(\"search\").value = \"\";\n renderChips(); renderList();\n });\n\n // Back/forward or a manual hash edit re-syncs the open trace/span.\n window.addEventListener(\"hashchange\", function () {\n if (suppressHash) return;\n applyHash();\n });\n\n var THEME_KEY = \"panoptic-theme\";\n var mql = window.matchMedia ? window.matchMedia(\"(prefers-color-scheme: light)\") : null;\n function applyTheme(mode) {\n try { localStorage.setItem(THEME_KEY, mode); } catch (e) {}\n var light = mode === \"light\" || (mode === \"system\" && mql && mql.matches);\n document.documentElement.setAttribute(\"data-theme\", light ? \"light\" : \"dark\");\n var btns = document.querySelectorAll(\"[data-theme-set]\");\n for (var i = 0; i < btns.length; i++) btns[i].classList.toggle(\"active\", btns[i].getAttribute(\"data-theme-set\") === mode);\n }\n document.getElementById(\"theme\").addEventListener(\"click\", function (e) {\n var b = e.target.closest ? e.target.closest(\"[data-theme-set]\") : null;\n if (b) applyTheme(b.getAttribute(\"data-theme-set\"));\n });\n if (mql && mql.addEventListener) mql.addEventListener(\"change\", function () {\n var cur = \"system\";\n try { cur = localStorage.getItem(THEME_KEY) || \"system\"; } catch (e) {}\n if (cur === \"system\") applyTheme(\"system\");\n });\n var savedTheme = \"system\";\n try { savedTheme = localStorage.getItem(THEME_KEY) || \"system\"; } catch (e) {}\n applyTheme(savedTheme);\n\n function poll() {\n Promise.all([\n fetchAuthed(API + \"/aggregate\").then(function (r) { return r.json(); }),\n fetchAuthed(API + \"/traces\").then(function (r) { return r.json(); })\n ]).then(function (res) {\n renderStats(res[0]);\n state.traces = res[1] || [];\n document.getElementById(\"meta\").textContent = state.traces.length + \" trace(s) · live\";\n renderChips();\n renderList();\n // Open whatever the URL hash deep-links to, once the target trace\n // has actually arrived in a poll (it may not be in the first batch).\n if (!state.hashApplied) {\n if (applyHash()) state.hashApplied = true;\n } else if (state.selectedId) {\n var t = findTrace(state.selectedId);\n if (t) { var sig = traceSig(t); if (sig !== state.sig) { renderDrawer(t); state.sig = sig; } }\n }\n }).catch(function (e) {\n document.getElementById(\"meta\").textContent = \"disconnected\";\n });\n }\n\n // FOLLOW-UP: a live socket tail (SSE / WebSocket push) is out of scope\n // for this pass; the dashboard stays on the 2s JSON poll below. When\n // added, it should reuse renderList/renderDrawer and keep the poll as a\n // reconnect fallback.\n poll();\n setInterval(poll, 2000);\n})();\n</script>\n</body>\n</html>`;\n}\n\n/** Escape a string for safe interpolation into static HTML text. */\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\n/**\n * Encode free-form text (e.g. `evaluate.instructions`, which an operator\n * could type anything into) as a JS EXPRESSION that reconstructs it at\n * runtime, WITHOUT ever emitting a literal backtick into the served page —\n * the client script is itself built from a TS template literal, so a raw\n * backtick in the output would be a real syntax hazard (see the `BT =\n * String.fromCharCode(96)` construction already in the client script for\n * the same reason). `JSON.stringify` alone doesn't escape backticks (they\n * aren't JSON-significant), so a value containing one is split around it\n * and rejoined with the client's own `BT` constant. No backticks in the\n * input ⇒ a single plain `JSON.stringify(value)` — no unnecessary\n * concatenation in the common case.\n */\nfunction encodeForInlineScript(value: string): string {\n return value\n .split(\"`\")\n .map(part => JSON.stringify(part))\n .join(\" + BT + \");\n}\n","import { timingSafeEqual } from \"node:crypto\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { EvaluateConfig } from \"../evaluate/evaluate.type\";\nimport { evaluateSystemPrompt } from \"../evaluate/evaluate-system-prompt\";\nimport { extractLastSystemPrompt } from \"../evaluate/extract-last-system-prompt\";\nimport { findSpanById } from \"../evaluate/find-span-by-id\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport { parseQuery } from \"./parse-query\";\nimport { dashboardHtml } from \"./ui.html\";\n\n/** Fully-resolved routing config the request handler closes over. */\nexport type ServeConfig = {\n /** Normalized mount path, always ending in `/` (e.g. `\"/\"`). */\n basePath: string;\n /** Header title baked into the served page. */\n title: string;\n /** Bearer token required on every request when set (S4). */\n authToken?: string;\n /** `Host` header allowlist — defends against DNS-rebinding (S4). */\n allowedHosts: string[];\n /**\n * Enables the one write route: `POST\n * {basePath}api/traces/:traceId/spans/:spanId/evaluate`. Absent ⇒ the\n * route 405s (POST isn't accepted by any route) and the served page never\n * renders the Evaluate button.\n */\n evaluate?: EvaluateConfig;\n};\n\n/** Request body accepted by the evaluate route — everything optional. */\ntype EvaluateRequestBody = {\n /** Per-run instructions override; falls back to `config.evaluate.instructions`. */\n instructions?: string;\n};\n\n/** Hard cap on the evaluate route's request body — well beyond a real instructions string. */\nconst MAX_EVALUATE_BODY_BYTES = 64 * 1024;\n\n/**\n * Security response headers added to every dashboard response (S4):\n * block MIME-sniffing, framing, referrer leakage, and lock the page's\n * content sources down to itself (the UI is fully self-contained — no CDN).\n */\nconst SECURITY_HEADERS: Record<string, string> = {\n \"x-content-type-options\": \"nosniff\",\n \"x-frame-options\": \"DENY\",\n \"referrer-policy\": \"no-referrer\",\n \"content-security-policy\":\n \"default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; connect-src 'self'; img-src 'self' data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'\",\n};\n\n/** Extract the host (no port) from a `Host` header value. */\nfunction hostHeaderName(hostHeader: string | undefined): string | undefined {\n if (!hostHeader) return undefined;\n // IPv6 literal `[::1]:4319` → `[::1]`; otherwise strip `:port`.\n if (hostHeader.startsWith(\"[\")) {\n return hostHeader.slice(0, hostHeader.indexOf(\"]\") + 1);\n }\n const colon = hostHeader.indexOf(\":\");\n return colon === -1 ? hostHeader : hostHeader.slice(0, colon);\n}\n\n/**\n * Constant-time string equality (S4) — guards against a timing\n * side-channel that could otherwise let a network-adjacent attacker\n * recover the token byte-by-byte via repeated timed guesses. Plain `===`\n * short-circuits on the first differing byte, so response latency leaks\n * how many leading bytes matched; `timingSafeEqual` does not. Lengths are\n * compared first (a length mismatch is not secret and `timingSafeEqual`\n * requires equal-length buffers anyway).\n */\nfunction constantTimeEqual(a: string, b: string): boolean {\n const bufA = Buffer.from(a);\n const bufB = Buffer.from(b);\n\n if (bufA.length !== bufB.length) return false;\n\n return timingSafeEqual(bufA, bufB);\n}\n\n/**\n * Bearer-token check (S4): `Authorization: Bearer <token>` header, or\n * (only when `allowQueryToken`) a `?token=` query param.\n *\n * The query-string form only exists for the one request that structurally\n * cannot carry a custom header — the initial browser navigation that\n * loads the HTML shell (a typed/clicked/bookmarked URL). Every other\n * request the served page makes is same-origin `fetch()`, which can and\n * does set `Authorization` (see `ui.html.ts`'s `fetchAuthed`), so the API\n * routes never need to accept a query-string token. Restricting the\n * fallback to just the page route minimizes the token's exposure in\n * server access logs, proxy logs, and browser history to a single route\n * instead of every poll.\n */\nfunction isAuthorized(\n req: IncomingMessage,\n url: URL,\n token: string,\n allowQueryToken: boolean,\n): boolean {\n const header = req.headers.authorization;\n if (header && constantTimeEqual(header, `Bearer ${token}`)) return true;\n if (!allowQueryToken) return false;\n const queryToken = url.searchParams.get(\"token\");\n return queryToken !== null && constantTimeEqual(queryToken, token);\n}\n\n/**\n * Build the `node:http` request handler for the dashboard over a given\n * trace store. Kept separate from the server lifecycle so it can be unit\n * tested by feeding it a fake `req`/`res` without binding a port.\n *\n * Routes (all under `config.basePath`):\n *\n * - `GET api/traces` → `store.query(parseQuery(searchParams))`\n * - `GET api/traces/:id` → `store.get(id)` or `404`\n * - `GET api/aggregate` → `store.aggregate(parseQuery(searchParams))`\n * - `GET {basePath}` → the self-contained HTML page\n * - `POST api/traces/:traceId/spans/:spanId/evaluate` → ONLY when\n * `config.evaluate` is set; grades the span's last captured system\n * prompt and returns a {@link EvaluateVerdict}. The dashboard's one\n * write route — absent config, POST 405s like it would against any\n * other route (the path is never even pattern-matched).\n *\n * Anything else → `404`. A method the matched route doesn't accept → `405`.\n * Host allowlist + bearer-token auth (S4) are checked for EVERY request,\n * regardless of method, before any routing. The store shapes\n * ({@link Trace}/{@link TraceSpan}) are already JSON-safe, so GET responses\n * are a plain `JSON.stringify` with no serializer.\n *\n * @example\n * const handler = createRequestHandler(store, { basePath: \"/\", title: \"Panoptic\" });\n * http.createServer(handler).listen(4319, \"127.0.0.1\");\n */\nexport function createRequestHandler(\n store: TraceStoreContract,\n config: ServeConfig,\n): (req: IncomingMessage, res: ServerResponse) => void {\n const base = config.basePath;\n const apiPrefix = `${base}api`;\n\n return function handle(req: IncomingMessage, res: ServerResponse): void {\n // `req.url` is path + query only; a dummy origin lets URL parse it.\n const url = new URL(req.url ?? \"/\", \"http://localhost\");\n const pathname = url.pathname;\n\n // Host-header allowlist — blocks DNS-rebinding attacks that point a\n // hostile domain at this loopback port (S4). Checked for every method.\n const host = hostHeaderName(req.headers.host);\n if (!host || !config.allowedHosts.includes(host.toLowerCase())) {\n sendJson(res, 403, { error: \"host_not_allowed\" });\n\n return;\n }\n\n // Bearer-token auth when configured (always required off loopback, S4).\n // `?token=` is only honored on the HTML page route — see `isAuthorized`.\n const isPageRoute = pathname === base || pathname === base.replace(/\\/$/, \"\");\n if (config.authToken && !isAuthorized(req, url, config.authToken, isPageRoute)) {\n sendJson(res, 401, { error: \"unauthorized\" });\n\n return;\n }\n\n // The one write route — opt-in via `config.evaluate`, so it must be\n // checked before the blanket GET-only gate below.\n if (req.method === \"POST\" && config.evaluate) {\n const match = matchEvaluatePath(pathname, apiPrefix);\n\n if (match) {\n void handleEvaluate(store, config.evaluate, match.traceId, match.spanId, req, res);\n\n return;\n }\n }\n\n if (req.method !== \"GET\") {\n sendJson(res, 405, { error: \"method_not_allowed\" });\n\n return;\n }\n\n if (pathname === `${apiPrefix}/traces`) {\n sendJson(res, 200, store.query(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname.startsWith(`${apiPrefix}/traces/`)) {\n const traceId = decodeURIComponent(pathname.slice(`${apiPrefix}/traces/`.length));\n const trace = traceId.length > 0 ? store.get(traceId) : undefined;\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n sendJson(res, 200, trace);\n\n return;\n }\n\n if (pathname === `${apiPrefix}/aggregate`) {\n sendJson(res, 200, store.aggregate(parseQuery(url.searchParams)));\n\n return;\n }\n\n if (pathname === base || pathname === base.replace(/\\/$/, \"\")) {\n res.writeHead(200, {\n \"content-type\": \"text/html; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(dashboardHtml(base, config.title, Boolean(config.evaluate), config.evaluate?.instructions ?? \"\"));\n\n return;\n }\n\n sendJson(res, 404, { error: \"not_found\" });\n };\n}\n\n/** Write a JSON response with the given status code and security headers. */\nfunction sendJson(res: ServerResponse, status: number, body: unknown): void {\n res.writeHead(status, {\n \"content-type\": \"application/json; charset=utf-8\",\n ...SECURITY_HEADERS,\n });\n res.end(JSON.stringify(body));\n}\n\n/**\n * Match `{apiPrefix}/traces/:traceId/spans/:spanId/evaluate` — the one\n * write route. Returns `undefined` for anything else, including the plain\n * `{apiPrefix}/traces/:id` read route (no `/spans/.../evaluate` suffix),\n * so the two never collide.\n */\nfunction matchEvaluatePath(\n pathname: string,\n apiPrefix: string,\n): { traceId: string; spanId: string } | undefined {\n const prefix = `${apiPrefix}/traces/`;\n\n if (!pathname.startsWith(prefix)) {\n return undefined;\n }\n\n const match = /^([^/]+)\\/spans\\/([^/]+)\\/evaluate$/.exec(pathname.slice(prefix.length));\n\n if (!match) {\n return undefined;\n }\n\n return {\n traceId: decodeURIComponent(match[1]),\n spanId: decodeURIComponent(match[2]),\n };\n}\n\n/**\n * Collect and JSON-parse a request body, capped at\n * {@link MAX_EVALUATE_BODY_BYTES} so an oversized body can't hold the\n * connection open indefinitely. An empty body resolves to `undefined` —\n * the evaluate route treats that as \"no per-run override\".\n */\nfunction readJsonBody<T>(req: IncomingMessage): Promise<T | undefined> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let size = 0;\n\n req.on(\"data\", (chunk: Buffer) => {\n size += chunk.length;\n\n if (size > MAX_EVALUATE_BODY_BYTES) {\n req.destroy();\n reject(new Error(\"payload_too_large\"));\n\n return;\n }\n\n chunks.push(chunk);\n });\n\n req.on(\"end\", () => {\n if (chunks.length === 0) {\n resolve(undefined);\n\n return;\n }\n\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString(\"utf-8\")) as T);\n } catch {\n reject(new Error(\"invalid_json\"));\n }\n });\n\n req.on(\"error\", reject);\n });\n}\n\n/**\n * Handle `POST {apiPrefix}/traces/:traceId/spans/:spanId/evaluate`. Looks\n * up the trace + span, extracts its last captured system prompt, and\n * grades it via {@link evaluateSystemPrompt}. `judgePromptBody` itself\n * never throws (a broken judge degrades to an issues-only outcome) — the\n * try/catch here only guards `config.evaluate.model` resolution, the one\n * step that CAN throw (e.g. a factory constructing an SDK client).\n */\nasync function handleEvaluate(\n store: TraceStoreContract,\n evaluate: EvaluateConfig,\n traceId: string,\n spanId: string,\n req: IncomingMessage,\n res: ServerResponse,\n): Promise<void> {\n const trace = store.get(traceId);\n\n if (trace === undefined) {\n sendJson(res, 404, { error: \"trace_not_found\", traceId });\n\n return;\n }\n\n const span = findSpanById(trace.root, spanId);\n\n if (span === undefined) {\n sendJson(res, 404, { error: \"span_not_found\", spanId });\n\n return;\n }\n\n const systemPrompt = extractLastSystemPrompt(span);\n\n if (systemPrompt === undefined) {\n sendJson(res, 422, { error: \"no_system_prompt\" });\n\n return;\n }\n\n let body: EvaluateRequestBody | undefined;\n\n try {\n body = await readJsonBody<EvaluateRequestBody>(req);\n } catch (error) {\n const message = error instanceof Error ? error.message : \"invalid_json\";\n sendJson(res, message === \"payload_too_large\" ? 413 : 400, { error: message });\n\n return;\n }\n\n try {\n const verdict = await evaluateSystemPrompt(systemPrompt, evaluate, body?.instructions);\n sendJson(res, 200, verdict);\n } catch (error) {\n sendJson(res, 502, {\n error: \"evaluate_failed\",\n message: error instanceof Error ? error.message : String(error),\n });\n }\n}\n","import { createServer } from \"node:http\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport type { DashboardHandle, DashboardOptions } from \"./dashboard.type\";\nimport { createRequestHandler } from \"./serve\";\n\nconst DEFAULT_PORT = 4319;\nconst DEFAULT_HOST = \"127.0.0.1\";\nconst DEFAULT_TITLE = \"Panoptic\";\n\n/**\n * Start the local Panoptic dashboard over a {@link TraceStoreContract} —\n * a zero-dependency `node:http` server that serves a read-only JSON API\n * and one self-contained HTML page polling it. The store is the live\n * object the collector fills, so each poll reflects the latest completed\n * traces with no extra wiring.\n *\n * This is a low-level building block. The documented path is\n * `ai.config({ panoptic: { dashboard } })`, which constructs (or reuses)\n * the store and calls this for you. Use it directly only when you manage\n * the store yourself.\n *\n * Binds loopback-only by default (`127.0.0.1`) so prompt content is never\n * exposed to the LAN. Pass `port: 0` for an ephemeral port — the resolved\n * port comes back on the handle. A port already in use rejects with a\n * clear, actionable `Error` rather than the raw `EADDRINUSE`.\n *\n * @example\n * const store = createInMemoryTraceStore();\n * const handle = await dashboard(store, { port: 4319, open: true });\n * console.log(handle.url); // http://127.0.0.1:4319/\n * // ...later:\n * await handle.close();\n */\nexport function dashboard(\n store: TraceStoreContract,\n options: DashboardOptions = {},\n): Promise<DashboardHandle> {\n const port = options.port ?? DEFAULT_PORT;\n const host = options.host ?? DEFAULT_HOST;\n const title = options.title ?? DEFAULT_TITLE;\n const basePath = normalizeBasePath(options.basePath);\n\n // Secure-by-default off loopback (S4): a non-loopback bind without an\n // auth token would expose raw prompt content to the network, so refuse\n // to start rather than silently exposing it.\n if (!isLoopbackHost(host) && !options.authToken) {\n return Promise.reject(\n new Error(\n `Panoptic dashboard: binding to a non-loopback host (\"${host}\") requires an \\`authToken\\` ` +\n \"(the dashboard exposes raw prompt content). Pass `authToken`, or bind to 127.0.0.1.\",\n ),\n );\n }\n\n const allowedHosts = (options.allowedHosts ?? defaultAllowedHosts(host)).map(h =>\n h.toLowerCase(),\n );\n\n const handler = createRequestHandler(store, {\n basePath,\n title,\n authToken: options.authToken,\n allowedHosts,\n evaluate: options.evaluate,\n });\n const server = createServer(handler);\n\n return new Promise<DashboardHandle>((resolve, reject) => {\n const onError = (error: NodeJS.ErrnoException): void => {\n server.off(\"error\", onError);\n\n if (error.code === \"EADDRINUSE\") {\n reject(\n new Error(\n `Panoptic dashboard: port ${port} in use; pass { port: 0 } for an ephemeral port`,\n ),\n );\n\n return;\n }\n\n reject(error);\n };\n\n server.on(\"error\", onError);\n\n server.listen(port, host, () => {\n server.off(\"error\", onError);\n\n const address = server.address();\n const resolvedPort = typeof address === \"object\" && address !== null ? address.port : port;\n const url = `http://${host}:${resolvedPort}${basePath}`;\n\n if (options.open) {\n openBrowser(url);\n }\n\n resolve({\n url,\n port: resolvedPort,\n close(): Promise<void> {\n return new Promise<void>((closeResolve, closeReject) => {\n server.close((closeError) => {\n if (closeError) {\n closeReject(closeError);\n\n return;\n }\n\n closeResolve();\n });\n });\n },\n });\n });\n });\n}\n\n/** Loopback hosts the dashboard may bind without an auth token. */\nfunction isLoopbackHost(host: string): boolean {\n const h = host.toLowerCase();\n return h === \"127.0.0.1\" || h === \"::1\" || h === \"[::1]\" || h === \"localhost\";\n}\n\n/**\n * Default `Host` allowlist for a given bind host. A loopback bind accepts\n * the loopback names a browser would send; a non-loopback bind defaults to\n * just the bound host (callers can widen via `allowedHosts`).\n */\nfunction defaultAllowedHosts(host: string): string[] {\n if (isLoopbackHost(host)) {\n return [\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"];\n }\n return [host];\n}\n\n/**\n * Normalize a caller `basePath` to a leading-and-trailing-slash form the\n * router can prefix routes with. `undefined` / `\"\"` → `\"/\"`.\n */\nfunction normalizeBasePath(basePath?: string): string {\n if (basePath === undefined || basePath.length === 0 || basePath === \"/\") {\n return \"/\";\n }\n\n const withLeading = basePath.startsWith(\"/\") ? basePath : `/${basePath}`;\n\n return withLeading.endsWith(\"/\") ? withLeading : `${withLeading}/`;\n}\n\n/**\n * Best-effort open of the default browser at `url`. Fire-and-forget and\n * fully swallowed — failing to open a browser must never reject the\n * dashboard start. Uses the platform's native opener via a lazy\n * `node:child_process` import so the dependency is paid only when\n * `open: true`.\n */\nfunction openBrowser(url: string): void {\n void import(\"node:child_process\")\n .then(({ spawn }) => {\n const command =\n process.platform === \"win32\" ? \"cmd\" : process.platform === \"darwin\" ? \"open\" : \"xdg-open\";\n const args = process.platform === \"win32\" ? [\"/c\", \"start\", \"\", url] : [url];\n\n const child = spawn(command, args, { stdio: \"ignore\", detached: true });\n child.on(\"error\", () => {\n // Swallow — opening a browser is best-effort.\n });\n child.unref();\n })\n .catch(() => {\n // Swallow — opening a browser is best-effort.\n });\n}\n","import type { ReportStatus, ReportType } from \"@warlock.js/ai\";\nimport type { Trace, TraceSpan } from \"../contracts/trace.type\";\n\n/**\n * Client-side trace-list filter criteria. The dashboard polls the full\n * trace list every 2s and narrows it in the browser with these — search\n * box, filter chips, and the errors-only header toggle all feed one\n * {@link TraceFilter}. Kept as a pure, framework-free module so the exact\n * matching rules are unit-tested here and the inlined dashboard JS in\n * {@link import(\"./ui.html\").dashboardHtml} mirrors them 1:1.\n *\n * Every field is optional — an absent field is \"don't care\". An empty\n * filter therefore matches every trace.\n */\nexport type TraceFilter = {\n /**\n * Free-text needle matched case-insensitively against a trace's root\n * span name and session id. Whitespace-trimmed; an empty/blank string\n * is treated as absent.\n */\n text?: string;\n /**\n * Restrict to these terminal statuses (root span status). Empty array\n * is treated as \"don't care\" (matches every status).\n */\n statuses?: ReportStatus[];\n /**\n * Restrict to these executable types (root span type). Empty array is\n * treated as \"don't care\".\n */\n types?: ReportType[];\n /**\n * Exact session-id chip. When set, only traces whose `sessionId`\n * equals this value match. Distinct from {@link TraceFilter.text},\n * which is a fuzzy substring across name + session.\n */\n sessionId?: string;\n /**\n * Exact prompt chip — a `name@version` key (see {@link tracePromptKey}).\n * When set, only traces whose root span resolved that exact named prompt\n * version match. Mirrors {@link TraceFilter.sessionId} but over the\n * prompt-version-linkage attributes the collector stamps on agent spans.\n */\n promptKey?: string;\n /**\n * Errors-only header toggle. When `true`, only `failed` / `cancelled`\n * traces match — independent of (and intersected with) `statuses`.\n */\n errorsOnly?: boolean;\n};\n\n/** Statuses the errors-only toggle keeps. */\nconst ERROR_STATUSES: readonly ReportStatus[] = [\"failed\", \"cancelled\"] as const;\n\n/**\n * Span-attribute keys the collector stamps with prompt-version linkage when\n * an agent ran against a *named* `ai.prompts` builder. Read here (not\n * imported) so this pure module stays decoupled from the collector.\n */\nconst PROMPT_NAME_ATTR = \"agent.promptName\";\nconst PROMPT_VERSION_ATTR = \"agent.promptVersion\";\n\n/**\n * The `name@version` prompt key a trace falls under, read from its root\n * span's prompt-version-linkage attributes (`agent.promptName` /\n * `agent.promptVersion`). Returns `undefined` when the run carried no named\n * prompt (raw-string / anonymous / no prompt), so callers can bucket those\n * under a synthetic \"no prompt\" key or skip them.\n *\n * The version is included so two runs of the same prompt name at different\n * versions are distinct keys — that is the whole point of the linkage: to\n * group / filter by the *exact* prompt revision that produced a run.\n */\nexport function tracePromptKey(trace: Trace): string | undefined {\n const attributes = trace.root.attributes;\n\n if (attributes === undefined) {\n return undefined;\n }\n\n const name = attributes[PROMPT_NAME_ATTR];\n\n if (typeof name !== \"string\" || name.length === 0) {\n return undefined;\n }\n\n const version = attributes[PROMPT_VERSION_ATTR];\n const versionLabel = typeof version === \"string\" && version.length > 0 ? version : \"1\";\n\n return `${name}@${versionLabel}`;\n}\n\n/**\n * Does a single trace pass the given filter? Pure predicate — all\n * conditions are ANDed; an unset field never excludes. Used by\n * {@link filterTraces} and mirrored by the dashboard's inlined JS.\n */\nexport function matchesFilter(trace: Trace, filter: TraceFilter): boolean {\n const root = trace.root;\n\n if (filter.errorsOnly === true && !ERROR_STATUSES.includes(root.status)) {\n return false;\n }\n\n if (filter.statuses !== undefined && filter.statuses.length > 0) {\n if (!filter.statuses.includes(root.status)) {\n return false;\n }\n }\n\n if (filter.types !== undefined && filter.types.length > 0) {\n if (!filter.types.includes(root.type)) {\n return false;\n }\n }\n\n if (filter.sessionId !== undefined && filter.sessionId.length > 0) {\n if (trace.sessionId !== filter.sessionId) {\n return false;\n }\n }\n\n if (filter.promptKey !== undefined && filter.promptKey.length > 0) {\n if (tracePromptKey(trace) !== filter.promptKey) {\n return false;\n }\n }\n\n const text = filter.text?.trim().toLowerCase();\n if (text !== undefined && text.length > 0) {\n const haystack = `${root.name} ${trace.sessionId ?? \"\"}`.toLowerCase();\n if (!haystack.includes(text)) {\n return false;\n }\n }\n\n return true;\n}\n\n/** Narrow a trace list to those matching the filter, order preserved. */\nexport function filterTraces(traces: Trace[], filter: TraceFilter): Trace[] {\n return traces.filter((trace) => matchesFilter(trace, filter));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by\n * session. Traces without a `sessionId` collapse into one synthetic\n * \"(no session)\" bucket so they remain visible.\n */\nexport const NO_SESSION_KEY = \"(no session)\";\n\n/** One session bucket — its key plus the traces under it, order preserved. */\nexport type SessionGroup = {\n /** The `sessionId`, or {@link NO_SESSION_KEY} for sessionless traces. */\n sessionId: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by session id,\n * preserving first-seen order of both the groups and the traces within\n * each. Sessionless traces bucket under {@link NO_SESSION_KEY}.\n */\nexport function groupBySession(traces: Trace[]): SessionGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.sessionId ?? NO_SESSION_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((sessionId) => ({ sessionId, traces: byKey.get(sessionId) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by prompt\n * version. Runs that resolved no named prompt collapse into one synthetic\n * \"(no prompt)\" bucket so they remain visible.\n */\nexport const NO_PROMPT_KEY = \"(no prompt)\";\n\n/** One prompt-version bucket — its `name@version` key plus its traces. */\nexport type PromptGroup = {\n /** The `name@version` key, or {@link NO_PROMPT_KEY} for unlinked traces. */\n promptKey: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by prompt version —\n * the `name@version` key {@link tracePromptKey} derives from each run's\n * prompt-version-linkage attributes. Preserves first-seen order of both the\n * groups and the traces within each. Runs with no named prompt bucket under\n * {@link NO_PROMPT_KEY}.\n *\n * The dashboard offers this as a second group-by dimension beside session,\n * so a reviewer can see every run of \"support@2\" together and compare its\n * cost / failure rate against \"support@3\".\n */\nexport function groupByPrompt(traces: Trace[]): PromptGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = tracePromptKey(trace) ?? NO_PROMPT_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((promptKey) => ({ promptKey, traces: byKey.get(promptKey) ?? [] }));\n}\n\n/**\n * The grouping key a trace falls under when the list is grouped by root\n * type. `root.type` is always present, so this is only a defensive\n * fallback for a malformed trace with no typed root.\n */\nexport const NO_TYPE_KEY = \"(no type)\";\n\n/** One root-type bucket — its type discriminator plus its traces. */\nexport type TypeGroup = {\n /** The root span's `type` (e.g. `agent` / `workflow` / `planner`), or {@link NO_TYPE_KEY}. */\n type: string;\n /** Traces in this group, in the order they appeared in the input. */\n traces: Trace[];\n};\n\n/**\n * Group an (already filtered, newest-first) trace list by root type —\n * the `agent` / `workflow` / `supervisor` / `orchestrator` / `planner` /\n * `tool` / `batch` / `callback` discriminator on each trace's root span.\n * Preserves first-seen order of both the groups and the traces within each.\n *\n * The dashboard offers this as the coarsest group-by dimension beside\n * session and prompt: when a benchmark floods the flat list with hundreds\n * of one primitive (e.g. standalone `agent` runs), this collapses them\n * behind a per-type header so the structural primitives (the lone planner,\n * the team, the supervisors) are one click away instead of a scroll past.\n */\nexport function groupByType(traces: Trace[]): TypeGroup[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => ({ type, traces: byKey.get(type) ?? [] }));\n}\n\n/** One row of the per-type aggregate-stats panel. */\nexport type TypeStat = {\n /** Root type discriminator, or {@link NO_TYPE_KEY}. */\n type: string;\n /** Number of traces of this type. */\n count: number;\n /** Traces whose status is `failed` / `cancelled`. */\n failed: number;\n /** `failed / count` in `[0, 1]`. */\n failRate: number;\n /** Median (p50) root duration in ms. */\n p50: number;\n /** p95 root duration in ms. */\n p95: number;\n /** Summed `usage.total` tokens across the bucket. */\n tokens: number;\n /** Summed per-trace USD cost across the bucket. */\n cost: number;\n};\n\n/**\n * Nearest-rank percentile of `values` at `p` (0–100). Returns 0 for an\n * empty input and sorts a copy so the caller's array is left untouched.\n */\nexport function percentile(values: number[], p: number): number {\n if (values.length === 0) {\n return 0;\n }\n\n const sorted = values.slice().sort((a, b) => a - b);\n const rank = Math.ceil((p / 100) * sorted.length) - 1;\n const index = Math.min(Math.max(rank, 0), sorted.length - 1);\n\n return sorted[index];\n}\n\n/**\n * One trace's total USD cost: its explicit root-usage cost when priced,\n * else the rollup of its subtree. Mirrors the dashboard's `traceCost` so the\n * stats panel and the trace rows agree to the cent.\n */\nexport function traceCost(trace: Trace): number {\n const own = costSumObj(trace.usage.cost);\n return own > 0 ? own : rollupCost(trace.root);\n}\n\n/**\n * Aggregate an (already filtered) trace list into one {@link TypeStat} row\n * per root type — count, failure rate, p50/p95 latency, total tokens, total\n * cost — preserving first-seen type order. Powers the dashboard's per-type\n * stats panel; computed over the same filtered list the trace view shows, so\n * the panel always reflects the active filters.\n */\nexport function aggregateByType(traces: Trace[]): TypeStat[] {\n const order: string[] = [];\n const byKey = new Map<string, Trace[]>();\n\n for (const trace of traces) {\n const key = trace.root.type || NO_TYPE_KEY;\n let bucket = byKey.get(key);\n\n if (bucket === undefined) {\n bucket = [];\n byKey.set(key, bucket);\n order.push(key);\n }\n\n bucket.push(trace);\n }\n\n return order.map((type) => {\n const bucket = byKey.get(type) ?? [];\n const durations = bucket.map((trace) => trace.duration);\n let failed = 0;\n let tokens = 0;\n let cost = 0;\n\n for (const trace of bucket) {\n if (ERROR_STATUSES.includes(trace.root.status)) {\n failed += 1;\n }\n\n tokens += trace.usage.total ?? 0;\n cost += traceCost(trace);\n }\n\n return {\n type,\n count: bucket.length,\n failed,\n failRate: bucket.length > 0 ? failed / bucket.length : 0,\n p50: percentile(durations, 50),\n p95: percentile(durations, 95),\n tokens,\n cost,\n };\n });\n}\n\n/**\n * Sum every priced lane of a usage cost object into one USD number.\n * Mirrors the dashboard's `costSumObj` so heatmap intensities computed\n * here match what the page renders.\n */\nfunction costSumObj(cost: TraceSpan[\"usage\"][\"cost\"]): number {\n if (cost === undefined) {\n return 0;\n }\n\n return (\n (cost.input ?? 0) +\n (cost.output ?? 0) +\n (cost.cachedInput ?? 0) +\n (cost.cachedOutput ?? 0) +\n (cost.reasoning ?? 0)\n );\n}\n\n/**\n * Rollup-aware subtree cost: a node's own cost when it carries one\n * (it already rolls up its trips), otherwise the sum of its children.\n * Avoids double-counting wrapper nodes (workflow/supervisor roots carry\n * tokens but no cost). Mirrors the dashboard's `rollupCost`.\n */\nexport function rollupCost(span: TraceSpan): number {\n const own = costSumObj(span.usage.cost);\n if (own > 0) {\n return own;\n }\n\n let sum = 0;\n for (const child of span.children) {\n sum += rollupCost(child);\n }\n\n return sum;\n}\n\n/**\n * Heatmap intensity in `[0, 1]` for a node's `rollupCost` relative to the\n * trace's most-expensive node. The dashboard tints each tree node's left\n * accent by this. `max <= 0` (a free trace) yields `0` for every node so\n * the heatmap simply stays cold rather than dividing by zero.\n */\nexport function heatIntensity(nodeCost: number, maxCost: number): number {\n if (maxCost <= 0 || nodeCost <= 0) {\n return 0;\n }\n\n const ratio = nodeCost / maxCost;\n\n return ratio > 1 ? 1 : ratio;\n}\n\n/**\n * The largest single-node {@link rollupCost} anywhere in a span tree —\n * the denominator for {@link heatIntensity}. Walks the whole subtree.\n */\nexport function maxNodeCost(root: TraceSpan): number {\n let max = rollupCost(root);\n\n for (const child of root.children) {\n const childMax = maxNodeCost(child);\n if (childMax > max) {\n max = childMax;\n }\n }\n\n return max;\n}\n","import type { ExecutionReport, Observer } from \"@warlock.js/ai\";\nimport { registerObserver, setObserveAll } from \"@warlock.js/ai\";\nimport { log } from \"@warlock.js/logger\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport { dashboard } from \"../dashboard/dashboard\";\nimport type { DashboardHandle, DashboardOptions } from \"../dashboard/dashboard.type\";\nimport { panoptic } from \"../panoptic/panoptic\";\nimport type { Panoptic } from \"../panoptic/panoptic.type\";\nimport { createCacheTraceStore } from \"../store/cache-trace-store\";\nimport type { CacheTraceStoreHandle } from \"../store/cache-trace-store\";\nimport { createInMemoryTraceStore } from \"../store/in-memory-trace-store\";\nimport type { TraceStoreContract } from \"../store/trace-store.contract\";\nimport type { PanopticConfig } from \"./panoptic-config.type\";\n\n/**\n * Module-level applied state. Applying panoptic config is idempotent:\n * the collector is registered as a core `Observer` exactly once, the\n * dashboard is started at most once, and repeat calls only refresh the\n * `observeAll` flag (the latest config wins) without double-registering.\n */\ntype AppliedState = {\n /** The single collector registered as a core `Observer`. */\n panopticInstance: Panoptic;\n /** The dashboard handle, once started. Started at most once. */\n dashboardHandle?: DashboardHandle;\n};\n\nlet applied: AppliedState | undefined;\n\n/**\n * Read a {@link PanopticConfig} and wire panoptic onto the core observe\n * seam — the bridge `ai.config({ panoptic })` resolves through. Builds a\n * collector via the EXISTING {@link panoptic} factory (reusing its\n * collection pipeline, not reinventing it), registers it once via core's\n * `registerObserver` (the subscriber's `collect(report)` structurally IS\n * an {@link Observer}, threaded through a thin wrapper), sets\n * `observeAll`, and — when `config.dashboard` is set — starts the\n * dashboard over the config's store (a store-shaped exporter, or a fresh\n * in-memory store panoptic registers when none was supplied).\n *\n * **Idempotent.** Safe to call on every `onConfigApplied` notification:\n * the observer is registered once, the dashboard started once. Repeat\n * calls update `observeAll` to the latest value but never double-register\n * or double-start. `undefined` config is a no-op (nothing wired).\n *\n * @returns the resolved store-shaped exporter when the dashboard needs\n * one and a fresh store was created — primarily for tests; callers can\n * ignore it.\n */\nexport function applyPanopticConfig(config?: PanopticConfig): void {\n if (config === undefined) {\n return;\n }\n\n // observeAll always tracks the latest config, even on a repeat call.\n setObserveAll(Boolean(config.observeAll));\n\n if (applied === undefined) {\n const exporters = [...(config.exporters ?? [])];\n let store = findStore(exporters);\n\n // Track a cache store separately so we can await its hydration before\n // the dashboard reads it — the in-memory store needs no warm-up.\n let cacheStore: CacheTraceStoreHandle | undefined;\n\n // The dashboard needs a queryable store. If none was supplied via\n // exporters but a dashboard is requested, create one and register it\n // as an exporter so the collector fills it. A configured `cache` picks\n // the durable cache-backed store; otherwise fall back to in-memory so\n // the dashboard works with no cache configured.\n if (store === undefined && config.dashboard) {\n if (config.cache !== undefined) {\n const onError =\n config.onError ??\n ((error: unknown) => log.error(\"ai-panoptic\", \"cacheStore\", error));\n\n cacheStore = createCacheTraceStore(config.cache, { onError });\n store = cacheStore;\n } else {\n store = createInMemoryTraceStore();\n }\n\n exporters.push(store as unknown as ExporterContract);\n }\n\n const panopticInstance = panoptic({\n exporters,\n captureContent: config.captureContent,\n fullHistory: config.fullHistory,\n });\n registerObserver(toObserver(panopticInstance));\n\n applied = { panopticInstance };\n\n if (config.dashboard && store !== undefined) {\n const resolvedStore = store;\n\n // Hydrate the cache mirror (if any) before serving, so a restart\n // surfaces previously-persisted traces immediately. A bare in-memory\n // store resolves the readyGate instantly.\n const readyGate =\n cacheStore !== undefined ? cacheStore.ready() : Promise.resolve();\n\n void readyGate\n .catch(() => {\n // Hydration is best-effort — never block the dashboard on it.\n })\n .then(() => startDashboard(resolvedStore, config.dashboard ?? {}))\n .then((handle) => {\n if (applied !== undefined) {\n applied.dashboardHandle = handle;\n }\n });\n }\n }\n}\n\n/**\n * Reset the applied state — internal, test-only. Closes any running\n * dashboard and forgets the registered collector so a fresh\n * `applyPanopticConfig` starts clean. NOT part of the public surface and\n * does NOT unregister from the core observer registry (use core's\n * `clearObservers` for that in tests).\n */\nexport async function resetAppliedPanopticConfig(): Promise<void> {\n const handle = applied?.dashboardHandle;\n applied = undefined;\n\n if (handle !== undefined) {\n await handle.close();\n }\n}\n\n/**\n * The currently-running dashboard handle, or `undefined` when no\n * dashboard is active. Internal, test-only — lets a spec await the\n * asynchronously-started dashboard. NOT part of the public surface.\n */\nexport function getActiveDashboardHandle(): DashboardHandle | undefined {\n return applied?.dashboardHandle;\n}\n\n/**\n * Wrap a {@link Panoptic} subscriber as a core {@link Observer}. The\n * subscriber already exposes `collect(report)` with a matching signature,\n * but the thin wrapper makes the structural adaptation explicit and keeps\n * the registered object a minimal `Observer` rather than the whole\n * subscriber surface.\n */\nfunction toObserver(instance: Panoptic): Observer {\n return {\n collect(report: ExecutionReport): Promise<void> {\n return instance.collect(report);\n },\n };\n}\n\n/**\n * Find the first exporter that satisfies the {@link TraceStoreContract}\n * read surface (`query` / `get` / `aggregate`) — the in-memory store\n * doubles as an exporter, so a store passed via `exporters` is reused for\n * the dashboard rather than creating a second one.\n */\nfunction findStore(exporters: ExporterContract[]): TraceStoreContract | undefined {\n for (const exporter of exporters) {\n const candidate = exporter as unknown as Partial<TraceStoreContract>;\n\n if (\n typeof candidate.query === \"function\" &&\n typeof candidate.get === \"function\" &&\n typeof candidate.aggregate === \"function\"\n ) {\n return candidate as TraceStoreContract;\n }\n }\n\n return undefined;\n}\n\n/** Start the dashboard, normalizing the `true | DashboardOptions` switch. */\nfunction startDashboard(\n store: TraceStoreContract,\n dashboardConfig: boolean | DashboardOptions,\n): Promise<DashboardHandle> {\n const options: DashboardOptions =\n typeof dashboardConfig === \"object\" ? dashboardConfig : {};\n\n return dashboard(store, options);\n}\n","import { getAIConfig, onConfigApplied } from \"@warlock.js/ai\";\nimport { applyPanopticConfig } from \"./config/apply-panoptic-config\";\n\n// Side-effect wiring. Importing `@warlock.js/ai-panoptic` (even bare,\n// `import \"@warlock.js/ai-panoptic\"`) subscribes panoptic to the core\n// config seam so a later `ai.config({ panoptic })` wires the collector +\n// dashboard onto the observe registry — without app code calling\n// `applyPanopticConfig` by hand.\n//\n// 1. React to every future `ai.config(...)` merge.\nonConfigApplied((config) => {\n applyPanopticConfig(config.panoptic);\n});\n\n// 2. Catch config that was applied BEFORE this import ran (e.g. the app\n// called `ai.config({ panoptic })` and only then imported panoptic).\napplyPanopticConfig(getAIConfig().panoptic);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAcA,UAAiB,UAAU,MAAuC;CAChE,MAAM;CAEN,KAAK,MAAM,SAAS,KAAK,UACvB,OAAO,UAAU,KAAK;AAE1B;;;;;;;;;;;;;;;;;;;;;;;;ACGA,SAAgB,eAAe,OAA4C;CACzE,IAAI,UAAU,UAAa,UAAU,MACnC;CAGF,IAAI,OAAO,UAAU,UACnB,OAAO;EACL,MAAM;EACN,0CAAsB,OAAO,KAAK,CAAC;CACrC;CAGF,MAAM,YAAY;CAQlB,MAAM,OAAO,WAAW,UAAU,IAAI,KAAK,WAAW,UAAU,IAAI,KAAK;CACzE,MAAM,UAAU,WAAW,UAAU,OAAO,KAAK;CACjD,MAAM,QAAQ,WAAW,UAAU,KAAK;CAKxC,MAAM,aAA6B;EACjC;EACA,0CAAsB,OAAO;CAC/B;CAEA,IAAI,UAAU,QACZ,WAAW,yCAAqB,KAAK;CAGvC,IAAI,UAAU,UAAU,UAAa,UAAU,UAAU,MACvD,WAAW,QACT,OAAO,UAAU,UAAU,sCAChB,UAAU,KAAK,qCACT,OAAO,UAAU,KAAK,CAAC;CAG5C,OAAO;AACT;;;;;AAMA,SAAS,WAAW,OAAoC;CACtD,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,OAAO;AAIX;;;;;;;;;;;;;;;;;;;;;;;;;;ACxBA,SAAgB,sBAAsB,QAAyD;CAC7F,MAAM,aAAa;CACnB,MAAM,aAAsC,CAAC;CAE7C,IAAI,OAAO,aAAa,UAAa,OAAO,SAAS,SAAS,GAC5D,WAAW,aAAa,OAAO,SAAS;CAG1C,QAAQ,OAAO,MAAf;EACE,KAAK;GACH,mBAAmB,YAAY,UAAU;GACzC;EAGF,KAAK;GACH,sBAAsB,YAAY,UAAU;GAC5C;EAGF,KAAK;EACL,KAAK;GAGH,wBAAwB,YAAY,UAAU;GAC9C;EAGF,KAAK;GACH,0BAA0B,YAAY,UAAU;GAChD;EAGF,KAAK;GACH,kBAAkB,YAAY,UAAU;GACxC;EAGF,SACE;CAEJ;CAEA,IAAI,OAAO,KAAK,UAAU,EAAE,WAAW,GACrC;CAGF,OAAO;AACT;AAEA,SAAS,mBAAmB,YAAqC,YAAoC;CACnG,IAAI,MAAM,QAAQ,WAAW,KAAK,GAChC,WAAW,iBAAiB,WAAW,MAAM;CAG/C,IAAI,WAAW,OAAO,SAAS,QAC7B,WAAW,sBAAsB,WAAW,MAAM;CAGpD,IAAI,WAAW,OAAO,aAAa,QACjC,WAAW,0BAA0B,WAAW,MAAM;CAOxD,IAAI,WAAW,eAAe,QAC5B,WAAW,sBAAsB,WAAW;CAG9C,IAAI,WAAW,kBAAkB,QAC/B,WAAW,yBAAyB,WAAW;AAEnD;AAEA,SAAS,sBAAsB,YAAqC,YAAoC;CACtG,IAAI,WAAW,iBAAiB,QAC9B,WAAW,mBAAmB,WAAW;CAG3C,IAAI,WAAW,cAAc,QAC3B,WAAW,wBAAwB,WAAW;CAGhD,IAAI,WAAW,UAAU,QACvB,WAAW,oBAAoB,OAAO,KAAK,WAAW,KAAK,EAAE;AAEjE;AAEA,SAAS,wBAAwB,YAAqC,YAAoC;CACxG,IAAI,WAAW,mBAAmB,QAChC,WAAW,qBAAqB,WAAW;CAG7C,IAAI,WAAW,iBAAiB,QAC9B,WAAW,6BAA6B,WAAW;CAGrD,IAAI,WAAW,eAAe,QAC5B,WAAW,2BAA2B,WAAW;AAErD;AAEA,SAAS,0BAA0B,YAAqC,YAAoC;CAC1G,IAAI,WAAW,cAAc,QAC3B,WAAW,4BAA4B,WAAW;CAGpD,IAAI,WAAW,cAAc,QAC3B,WAAW,4BAA4B,WAAW;CAGpD,IAAI,MAAM,QAAQ,WAAW,KAAK,GAChC,WAAW,wBAAwB,WAAW,MAAM;AAExD;AAEA,SAAS,kBAAkB,YAAqC,YAAoC;CAClG,IAAI,WAAW,cAAc,QAC3B,WAAW,oBAAoB,WAAW;CAG5C,IAAI,WAAW,kBAAkB,QAC/B,WAAW,wBAAwB,WAAW;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACjJA,SAAgB,aAAa,QAAoB,SAA4C;CAC3F,MAAM,OAAkB;EACtB,QAAQ,OAAO;EACf,SAAS,OAAO;EAChB,MAAM,OAAO;EACb,MAAM,OAAO;EACb,QAAQ,OAAO;EACf,WAAW,OAAO;EAClB,SAAS,OAAO;EAChB,UAAU,OAAO;EACjB,OAAO,OAAO;EACd,UAAU,OAAO,SAAS,KAAK,UAAU,aAAa,OAAO,OAAO,CAAC;CACvE;CAEA,IAAI,OAAO,gBAAgB,QACzB,KAAK,eAAe,OAAO;CAG7B,IAAI,OAAO,cAAc,QACvB,KAAK,YAAY,OAAO;CAG1B,IAAI,OAAO,YAAY,QACrB,KAAK,UAAU,OAAO;CAGxB,MAAM,QAAQ,eAAgB,OAA+B,KAAK;CAClE,IAAI,UAAU,QACZ,KAAK,QAAQ;CAGf,MAAM,aAAa,sBAAsB,MAAM;CAC/C,IAAI,eAAe,QACjB,KAAK,aAAa;CAGpB,IAAI,SAAS,gBACX,eAAe,MAAM,QAAQ,OAAO;CAGtC,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;AAyCA,SAAS,eACP,MACA,QACA,SACM;CACN,MAAM,OAAO;CACb,MAAM,SAAS,QAAQ;CAEvB,IAAI;CACJ,IAAI;CAEJ,IAAI,OAAO,SAAS,QAAQ;EAC1B,QAAQ,KAAK;EACb,SAAS,KAAK;CAChB,OAAO,IACL,QAAQ,eACR,MAAM,QAAQ,KAAK,QAAQ,KAC3B,KAAK,SAAS,SAAS,GACvB;EAGA,QAAQ,KAAK;EACb,SAAS,MAAM,QAAQ,KAAK,KAAK,IAAI,mBAAmB,KAAK,KAAK,IAAI;CACxE,OAAO,IAAI,MAAM,QAAQ,KAAK,KAAK,KAAK,KAAK,MAAM,SAAS,GAAG;EAC7D,MAAM,YAAY,KAAK,MAAM,IAAI;EAIjC,QACE,OAAO,KAAK,iBAAiB,YAAY,KAAK,aAAa,SAAS,IAChE,CACE;GAAE,MAAM;GAAU,SAAS,KAAK;EAAa,GAC7C;GAAE,MAAM;GAAQ,SAAS;EAAU,CACrC,IACA;EACN,SAAS,mBAAmB,KAAK,KAAK;CACxC;CAEA,IAAI,UAAU,QAAW;EACvB,MAAM,QAAQ,SAAS,OAAO,OAAO;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;GAAM,OAAO;EAAQ,CAAC,IAAI;EAC7F,IAAI,UAAU,QACZ,KAAK,QAAQ;CAEjB;CAEA,IAAI,WAAW,QAAW;EACxB,MAAM,QAAQ,SAAS,OAAO,QAAQ;GAAE,MAAM,KAAK;GAAM,MAAM,KAAK;GAAM,OAAO;EAAS,CAAC,IAAI;EAC/F,IAAI,UAAU,QACZ,KAAK,SAAS;CAElB;AACF;;;;;;;;;AAUA,SAAS,mBAAmB,OAA6C;CACvE,KAAK,IAAI,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;EAC7C,MAAM,MAAM,MAAM,IAAI;EAGtB,IAFgB,OAAO,QAAQ,WAAW,IAAI,SAAS,IAAI,QAAQ,QAGjE,OAAO;CAEX;AAGF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACzJA,SAAgB,cACd,QACA,WACA,SACO;CACP,MAAM,OAAO,aAAa,QAAQ,OAAO;CAEzC,IAAI,KAAK,UAAU,QAAW;EAC5B,MAAM,QAAQ,eAAe,SAAS;EAEtC,IAAI,UAAU,QACZ,KAAK,QAAQ;CAEjB;CAEA,MAAM,QAAe;EACnB,SAAS,KAAK;EACd;EACA,WAAW,KAAK;EAChB,SAAS,KAAK;EACd,UAAU,KAAK;EACf,OAAO,KAAK;CACd;CAEA,IAAI,KAAK,cAAc,QACrB,MAAM,YAAY,KAAK;CAGzB,IAAI,OAAO,wBAAwB,QACjC,MAAM,sBAAsB,OAAO;CAGrC,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;ACvBA,IAAM,YAAN,MAA6C;CAmBP;;;;;;CAbpC,AAAiB,4BAAY,IAAI,IAA8B;;;;;CAM/D,AAAiB,kCAAkB,IAAI,IAAY;;;;;;CAOnD,AAAO,YAAY,AAAiB,UAA4B,CAAC,GAAG;EAAhC;CAAiC;CAErE,AAAO,IAAI,UAAkC;EAC3C,IAAI,CAAC,KAAK,UAAU,IAAI,SAAS,IAAI,GACnC,KAAK,UAAU,IAAI,SAAS,MAAM,QAAQ;EAG5C,OAAO;CACT;CAEA,AAAO,QAAQ,QAAoB,WAA4B;EAC7D,OAAO,cAAc,QAAQ,WAAW,KAAK,OAAO;CACtD;CAEA,MAAa,QAAQ,QAAoB,WAAoC;EAC3E,MAAM,QAAQ,KAAK,QAAQ,QAAQ,SAAS;EAE5C,MAAM,KAAK,SAAS,KAAK;CAC3B;CAEA,MAAa,QAAuB;EAClC,MAAM,KAAK,WAAW,aAAa,SAAS,QAAQ,CAAC;CACvD;CAEA,MAAa,WAA0B;EACrC,MAAM,KAAK,MAAM;EAEjB,MAAM,KAAK,WAAW,aAAa,SAAS,WAAW,CAAC;EAExD,KAAK,UAAU,MAAM;CACvB;;;;;;;;;;;CAYA,MAAc,SAAS,OAA6B;EAClD,MAAM,KAAK,UAAU,OAAO,aAAa;GACvC,MAAM,SAAS,OAAO,KAAK;GAE3B,IAAI,SAAS,eAAe,QAC1B,KAAK,MAAM,QAAQ,UAAU,MAAM,IAAI,GACrC,MAAM,SAAS,WAAW,IAAI;EAGpC,CAAC;CACH;;;;;;;CAQA,MAAc,UACZ,MACe;EACf,MAAM,OAAO,CAAC,GAAG,KAAK,UAAU,QAAQ,CAAC,EAAE,KAAK,CAAC,MAAM,cACrD,QAAQ,QAAQ,EACb,WAAW,KAAK,QAAQ,CAAC,EACzB,OAAO,UAAmB,KAAK,oBAAoB,MAAM,KAAK,CAAC,CACpE;EAEA,MAAM,QAAQ,WAAW,IAAI;CAC/B;;;;;;;;;CAUA,AAAQ,oBAAoB,MAAc,OAAsB;EAC9D,IAAI,KAAK,QAAQ,SAAS;GACxB,IAAI;IACF,KAAK,QAAQ,QAAQ,MAAM,KAAK;GAClC,QAAQ,CAER;GACA;EACF;EAEA,IAAI,KAAK,gBAAgB,IAAI,IAAI,GAC/B;EAGF,KAAK,gBAAgB,IAAI,IAAI;EAC7B,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EACrE,QAAQ,KAAK,wBAAwB,KAAK,6BAA6B,SAAS;CAClF;AACF;;;;;;;;;;;;;AAcA,SAAgB,gBAAgB,UAA4B,CAAC,GAAsB;CACjF,OAAO,IAAI,UAAU,OAAO;AAC9B;;;;;;;;;;;;;;;;;AClKA,SAAgB,WAAW,OAAc,QAA8B;CACrE,IAAI,CAAC,QACH,OAAO;CAGT,IAAI,OAAO,YAAY,UAAa,MAAM,YAAY,OAAO,SAC3D,OAAO;CAGT,IAAI,OAAO,cAAc,UAAa,MAAM,cAAc,OAAO,WAC/D,OAAO;CAGT,IAAI,OAAO,WAAW,UAAa,CAAC,cAAc,MAAM,KAAK,QAAQ,OAAO,MAAM,GAChF,OAAO;CAGT,MAAM,YAAY,KAAK,MAAM,MAAM,SAAS;CAE5C,IAAI,OAAO,iBAAiB,UAAa,YAAY,QAAQ,OAAO,YAAY,GAC9E,OAAO;CAGT,IAAI,OAAO,kBAAkB,UAAa,YAAY,QAAQ,OAAO,aAAa,GAChF,OAAO;CAGT,OAAO;AACT;;;;;AAMA,SAAS,cAAc,QAAsB,QAAgD;CAC3F,IAAI,MAAM,QAAQ,MAAM,GACtB,OAAO,OAAO,SAAS,MAAM;CAG/B,OAAO,WAAW;AACpB;;;;;AAMA,SAAS,QAAQ,OAA8B;CAC7C,IAAI,iBAAiB,MACnB,OAAO,MAAM,QAAQ;CAGvB,OAAO,KAAK,MAAM,KAAK;AACzB;;;;;;;;;;;;;;;;;;;;;;;;AC/CA,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,0CAAsB,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,YAAY,aAAiC,MAA8C;CAClG,IAAI,gBAAgB,UAAa,SAAS,QACxC;CAGF,QAAQ,eAAe,MAAM,QAAQ;AACvC;;;;ACnBA,MAAM,iBAAiB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCvB,IAAM,kBAAN,MAAsE;;CAEpE,AAAgB,OAAO;;;;;;CAOvB,AAAiB,yBAAS,IAAI,IAAmB;CAEjD,AAAiB;CAEjB,AAAiB;;;;;;;CAQjB,AAAQ,aAAa;;CAGrB,AAAiB;CAIjB,AAAQ;CAGR,AAAQ;CAER,AAAiB;CAEjB,AAAO,YACL,OACA,UAA2E,CAAC,GAC5E;EACA,KAAK,QAAQ;EACb,KAAK,SAAS,QAAQ,UAAU;EAChC,KAAK,WAAW,QAAQ,YAAY;EACpC,KAAK,UAAU,QAAQ;CACzB;CAEA,IAAW,OAAe;EACxB,OAAO,KAAK,OAAO;CACrB;;;;;;;;;CAUA,MAAa,QAAuB;EAClC,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,cAAc;GACxC,MAAM,QAAQ,MAAM,OAAO,IAAkB,KAAK,SAAS,CAAC;GAE5D,IAAI,CAAC,MAAM,QAAQ,KAAK,GACtB;GAKF,MAAM,UAAU,CAAC,GAAG,KAAK,EAAE,MAAM,MAAM,UAAU,KAAK,UAAU,MAAM,OAAO;GAE7E,KAAK,MAAM,SAAS,SAAS;IAC3B,MAAM,QAAQ,MAAM,OAAO,IAAW,KAAK,SAAS,MAAM,EAAE,CAAC;IAE7D,IAAI,UAAU,QAAQ,UAAU,QAAW;KACzC,KAAK,OAAO,OAAO,MAAM,OAAO;KAChC,KAAK,OAAO,IAAI,MAAM,SAAS,KAAK;IACtC;IAEA,IAAI,MAAM,WAAW,KAAK,YACxB,KAAK,aAAa,MAAM,UAAU;GAEtC;GAEA,KAAK,cAAc;EACrB,SAAS,OAAO;GACd,KAAK,YAAY,KAAK;EACxB;CACF;CAEA,AAAO,IAAI,OAAoB;EAE7B,KAAK,OAAO,OAAO,MAAM,OAAO;EAChC,KAAK,OAAO,IAAI,MAAM,SAAS,KAAK;EAEpC,MAAM,UAAU,KAAK;EACrB,KAAK,cAAc;EAEnB,MAAM,UAAU,KAAK,cAAc;EAGnC,AAAK,KAAK,QAAQ,OAAO,SAAS,OAAO;CAC3C;;;;;CAMA,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,MAAM,MAAM,CAAC,GAAG,KAAK,OAAO,KAAK,CAAC;EAClC,KAAK,OAAO,MAAM;EAElB,AAAK,KAAK,MAAM,GAAG;CACrB;;;;;;;CAQA,MAAc,QACZ,OACA,SACA,WACe;EACf,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,cAAc;GAExC,MAAM,OAAO,IAAI,KAAK,SAAS,MAAM,OAAO,GAAG,KAAK;GAEpD,IAAI,cAAc,QAChB,MAAM,OAAO,OAAO,KAAK,SAAS,SAAS,CAAC;GAG9C,MAAM,OAAO,IAAI,KAAK,SAAS,GAAG,KAAK,WAAW,OAAO,CAAC;EAC5D,SAAS,OAAO;GACd,KAAK,YAAY,KAAK;EACxB;CACF;;CAGA,MAAc,MAAM,KAA8B;EAChD,IAAI;GACF,MAAM,SAAS,MAAM,KAAK,cAAc;GAExC,KAAK,MAAM,MAAM,KACf,MAAM,OAAO,OAAO,KAAK,SAAS,EAAE,CAAC;GAGvC,MAAM,OAAO,OAAO,KAAK,SAAS,CAAC;EACrC,SAAS,OAAO;GACd,KAAK,YAAY,KAAK;EACxB;CACF;;;;;;;CAQA,AAAQ,WAAW,eAAqC;EACtD,MAAM,MAAM,CAAC,GAAG,KAAK,OAAO,KAAK,CAAC;EAClC,MAAM,OAAO,iBAAiB,IAAI,SAAS;EAE3C,OAAO,IAAI,KAAK,IAAI,YAAY;GAAE;GAAI,SAAS,OAAO;EAAO,EAAE;CACjE;;;;;;CAQA,MAAc,gBAAgD;EAC5D,IAAI,KAAK,mBAAmB,QAC1B,OAAO,KAAK;EAGd,IAAI,KAAK,kBAAkB,QACzB,OAAO,KAAK;EAGd,MAAM,YACJ,OAAO,KAAK,UAAU,aACjB,KAAK,MAI8B,IACpC,KAAK;EAEX,KAAK,gBAAgB,QAAQ,QAAQ,SAAS;EAE9C,IAAI;GACF,KAAK,iBAAiB,MAAM,KAAK;GAEjC,OAAO,KAAK;EACd,UAAU;GACR,KAAK,gBAAgB;EACvB;CACF;;CAGA,AAAQ,SAAS,SAAyB;EACxC,OAAO,GAAG,KAAK,OAAO,SAAS;CACjC;;CAGA,AAAQ,WAAmB;EACzB,OAAO,GAAG,KAAK,OAAO;CACxB;;CAGA,AAAQ,YAAY,OAAsB;EACxC,IAAI,KAAK,YAAY,QACnB,KAAK,QAAQ,KAAK;CAEtB;;;;;;CAOA,AAAQ,YAAY,WAA2B,OAAoB;EACjE,QAAQ,MAAM,KAAK,QAAnB;GACE,KAAK;IACH,UAAU,aAAa;IACvB;GAGF,KAAK;IACH,UAAU,UAAU;IACpB;GAGF,KAAK;IACH,UAAU,aAAa;IACvB;GAGF,SACE;EAEJ;CACF;;;;;CAMA,AAAQ,gBAAgB,QAA0B;EAChD,OAAO,OAAO,MAAM,MAAM,UAAU,KAAK,MAAM,MAAM,SAAS,IAAI,KAAK,MAAM,KAAK,SAAS,CAAC;CAC9F;;;;;;;CAQA,AAAQ,gBAAoC;EAC1C,IAAI,KAAK,YAAY,GACnB;EAGF,IAAI;EAEJ,OAAO,KAAK,OAAO,OAAO,KAAK,UAAU;GACvC,MAAM,SAAS,KAAK,OAAO,KAAK,EAAE,KAAK,EAAE;GAEzC,IAAI,WAAW,QACb,OAAO;GAGT,KAAK,OAAO,OAAO,MAAM;GACzB,UAAU;EACZ;EAEA,OAAO;CACT;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,SAAgB,sBACd,OACA,UAA2E,CAAC,GACrD;CACvB,OAAO,IAAI,gBAAgB,OAAO,OAAO;AAC3C;;;;;;;;;;;;;;;;;;;;ACpbA,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;;;;;;;;CASA,AAAQ,YAAY,WAA2B,OAAoB;EACjE,QAAQ,MAAM,KAAK,QAAnB;GACE,KAAK;IACH,UAAU,aAAa;IACvB;GAGF,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,EAAE,KAAK,EAAE;GAEzC,IAAI,WAAW,QACb;GAGF,KAAK,OAAO,OAAO,MAAM;EAC3B;CACF;AACF;;;;;;;;;;;;;AAcA,SAAgB,yBAAyB,SAA4E;CACnH,OAAO,IAAI,mBAAmB,OAAO;AACvC;;;;;;;;;;;;;;;;;ACzLA,SAAgB,aAAa,OAAkC;CAC7D,MAAM,OAAO,MAAM;CAEnB,IAAI,CAAC,MACH;CAGF,QACG,KAAK,SAAS,MACd,KAAK,UAAU,MACf,KAAK,eAAe,MACpB,KAAK,gBAAgB,MACrB,KAAK,aAAa;AAEvB;;;;;;;;;;;;AClBA,MAAa,oBAAoB;CAC/B,eAAe;CACf,QAAQ;CACR,cAAc;CACd,eAAe;CACf,kBAAkB;CAClB,mBAAmB;CACnB,gBAAgB;;CAEhB,QAAQ;;CAER,YAAY;AACd;;;;;;AAOA,MAAa,qBAAqB;CAChC,YAAY;CACZ,SAAS;CACT,YAAY;CACZ,aAAa;CACb,cAAc;CACd,iBAAiB;CACjB,SAAS;AACX;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,kBAAkB,MAAiD;CACjF,MAAM,aAA6C;GAChD,mBAAmB,aAAa,KAAK;GACrC,mBAAmB,aAAa,KAAK;GACrC,mBAAmB,cAAc,KAAK,MAAM;GAC5C,kBAAkB,mBAAmB,KAAK,MAAM;GAChD,kBAAkB,oBAAoB,KAAK,MAAM;CACpD;CAEA,IAAI,KAAK,YAAY,QACnB,WAAW,mBAAmB,WAAW,KAAK;CAGhD,IAAI,KAAK,cAAc,QACrB,WAAW,kBAAkB,kBAAkB,KAAK;CAGtD,IAAI,KAAK,MAAM,iBAAiB,QAC9B,WAAW,mBAAmB,gBAAgB,KAAK,MAAM;CAG3D,IAAI,KAAK,MAAM,oBAAoB,QACjC,WAAW,mBAAmB,mBAAmB,KAAK,MAAM;CAG9D,MAAM,OAAO,aAAa,KAAK,KAAK;CAEpC,IAAI,SAAS,QACX,WAAW,mBAAmB,WAAW;CAG3C,sBAAsB,YAAY,KAAK,UAAU;CAEjD,OAAO;AACT;;;;;;;;AASA,SAAS,sBACP,QACA,QACM;CACN,IAAI,CAAC,QACH;CAGF,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO,OAAO;AAGpB;;;;;AC1HA,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;AAuB7B,SAAgB,aAAa,MAAiB,QAAQ,GAAG,WAAW,sBAAgC;CAClG,MAAM,QAAkB,CAAC;CACzB,MAAM,SAAS,KAAK,OAAO,QAAQ,CAAC;CAEpC,IAAI,KAAK,UAAU,QACjB,MAAM,KAAK,GAAG,OAAO,OAAO,QAAQ,KAAK,OAAO,QAAQ,GAAG;CAG7D,IAAI,KAAK,WAAW,QAClB,MAAM,KAAK,GAAG,OAAO,OAAO,QAAQ,KAAK,QAAQ,QAAQ,GAAG;CAG9D,OAAO;AACT;;;;;;;AAQA,SAAS,QAAQ,OAAgB,UAA0B;CAEzD,MAAM,aADO,OAAO,UAAU,WAAW,QAAQ,UAAU,KAAK,GACzC,QAAQ,QAAQ,GAAG,EAAE,KAAK;CAEjD,OAAO,UAAU,SAAS,WAAW,GAAG,UAAU,MAAM,GAAG,QAAQ,EAAE,KAAK;AAC5E;AAEA,SAAS,UAAU,OAAwB;CACzC,IAAI;EACF,OAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;CAC9C,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;;;;;;;;;;;;;;;AC9CA,SAAgB,eAAe,MAAiB,QAAQ,GAAW;CACjE,MAAM,SAAS,KAAK,OAAO,KAAK;CAChC,MAAM,SAAS,aAAa,KAAK,MAAM;CACvC,MAAM,OAAO,aAAa,KAAK,KAAK;CACpC,MAAM,aAAa,SAAS,SAAY,KAAK,MAAM,KAAK,QAAQ,CAAC;CAEjE,IAAI,OAAO,GAAG,SAAS,OAAO,GAAG,KAAK,KAAK,IAAI,KAAK,KAAK,MAAM,KAAK,SAAS,MAAM,KAAK,MAAM,MAAM,MAAM;CAE1G,IAAI,KAAK,OACP,QAAQ,KAAK,KAAK,MAAM,KAAK,IAAI,KAAK,MAAM,QAAQ;CAGtD,OAAO;AACT;;;;;AAMA,SAAS,aAAa,QAAqC;CACzD,QAAQ,QAAR;EACE,KAAK,aACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,KAAK,aACH,OAAO;EACT,KAAK,kBACH,OAAO;EACT,KAAK,kBACH,OAAO;EACT,SACE,OAAO;CACX;AACF;;;;AC1CA,MAAMA,kBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BtB,SAAgB,gBAAgB,UAAkC,CAAC,GAAqB;CACtF,MAAM,OAAoB,QAAQ,WAAW;CAC7C,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,KAAK,QAAQ,MAAM;CACzB,MAAM,aAAa,QAAQ;CAE3B,MAAM,WAA6B;EACjC,MAAMA;EACN,OAAO,OAAoB;GACzB,WAAW,MAAM,OAAO,MAAM,IAAI,UAAU;EAC9C;CACF;CAEA,IAAI,QAAQ,WACV,SAAS,cAAc,SAA0B;EAC/C,KAAK,IAAI,eAAe,IAAI,CAAC;EAE7B,IAAI,IACF,KAAK,MAAM,QAAQ,aAAa,MAAM,GAAG,UAAU,GACjD,KAAK,IAAI,IAAI;CAGnB;CAGF,OAAO;AACT;;;;;;;AAQA,SAAS,WACP,MACA,OACA,MACA,IACA,YACM;CACN,IAAI,CAAC,MAAM;EACT,UAAU,MAAM,MAAM,MAAM,GAAG,IAAI,UAAU;EAC7C;CACF;CAEA,KAAK,MAAM,QAAQ,UAAU,MAAM,IAAI,GAErC,UAAU,MAAM,MADF,UAAU,MAAM,MAAM,KAAK,MACf,GAAG,IAAI,UAAU;AAE/C;;;;;;AAOA,SAAS,UACP,MACA,MACA,OACA,IACA,YACM;CACN,gBAAgB,MAAM,KAAK,QAAQ,eAAe,MAAM,KAAK,CAAC;CAE9D,IAAI,IACF,KAAK,MAAM,QAAQ,aAAa,MAAM,OAAO,UAAU,GACrD,gBAAgB,MAAM,KAAK,QAAQ,IAAI;AAG7C;;;;;AAMA,SAAS,gBAAgB,MAAmB,QAA6B,MAAoB;CAC3F,IAAI,WAAW,YAAY,WAAW,aAAa;EACjD,KAAK,MAAM,IAAI;EACf;CACF;CAEA,KAAK,IAAI,IAAI;AACf;;;;;AAMA,SAAS,UAAU,MAAiB,cAAsB,QAAQ,GAAW;CAC3E,IAAI,KAAK,WAAW,cAClB,OAAO;CAGT,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,MAAM,QAAQ,UAAU,OAAO,cAAc,QAAQ,CAAC;EAEtD,IAAI,QAAQ,GACV,OAAO;CAEX;CAEA,OAAO;AACT;;;;ACpIA,MAAMC,kBAAgB;;;;;;;;;;;;;;;;AAiBtB,SAAgB,aAAa,SAAgD;CAC3E,MAAM,SAAS,IAAI,gBAAgB,OAAO;CAE1C,OAAO;EACL,MAAMA;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,GAAE,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,EAAE,KAAK,EAAE;EAEvE,uCAAiB,KAAK,MAAM,SAAS,MAAM;CAC7C;;;;;CAMA,MAAc,kBAAiC;EAC7C,IAAI,KAAK,gBACP;EAGF,yDAAoB,KAAK,IAAI,GAAG,EAAE,WAAW,KAAK,CAAC;EACnD,KAAK,iBAAiB;CACxB;;;;;;CAOA,AAAQ,UAAU,QAA6B;EAK7C,OAAO,GAJM,KAAK,SACd,KAAK,UAAU,QAAQ,QAAW,CAAC,IACnC,KAAK,UAAU,MAAM,EAEV;CACjB;AACF;;;;ACvGA,MAAMC,kBAAgB;AAMtB,IAAI;AACJ,IAAIC,mBAAiC;AACrC,IAAIC;AAEJ,MAAM,gCAAgC;;;;;;;;;;EAUpC,KAAK;;;;;;;AAQP,SAAS,eAA8B;CACrC,IAAID,qBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAIC,kBACF,OAAOA;CAGT,oBAAkB,YAAY;EAC5B,IAAI;GACF,cAAc,MAAM,OAAO;GAC3B,mBAAiB;EACnB,QAAQ;GACN,mBAAiB;EACnB;CACF,GAAG;CAEH,OAAOA;AACT;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,iBAAiB,SAAoD;CACnF,IAAI,SAAyC,QAAQ;CAErD,IAAI,CAAC,QACH,aAAa;CAGf,MAAM,gBAAgB,YAAyC;EAC7D,IAAI,QACF,OAAO;EAGT,MAAM,aAAa;EAEnB,IAAI,CAACD,kBACH,MAAM,IAAI,MAAM,6BAA6B;EAG/C,SAAS,IAAI,YAAY,SAAS;GAChC,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,SAAS,QAAQ;EACnB,CAAC;EAED,OAAO;CACT;CAEA,OAAO;EACL,MAAMD;EACN,MAAM,OAAO,OAA6B;GAExC,UAAU,MADiB,cAAc,GACjB,KAAK;EAC/B;EACA,MAAM,QAAuB;GAC3B,IAAI,CAAC,QACH;GAGF,MAAM,OAAO,WAAW;EAC1B;EACA,MAAM,WAA0B;GAC9B,IAAI,CAAC,QACH;GAGF,MAAM,OAAO,cAAc;EAC7B;CACF;AACF;;;;;AAMA,SAAS,UAAU,QAA4B,OAAoB;CACjE,MAAM,OAAO,MAAM;CAEnB,MAAM,YAA+B;EACnC,IAAI,KAAK;EACT,MAAM,KAAK;EACX,WAAW,MAAM;EACjB,SAAS,KAAK;EACd,WAAW,IAAI,KAAK,KAAK,SAAS;EAClC,UAAU,iBAAiB,IAAI;CACjC;CAOA,IAAI,KAAK,UAAU,QACjB,UAAU,QAAQ,KAAK;CAGzB,IAAI,KAAK,WAAW,QAClB,UAAU,SAAS,KAAK;CAW1B,gBARsB,OAAO,MAAM,SAQP,GAAG,IAAI;AACrC;;;;;;;AAQA,SAAS,gBACP,QACA,MACM;CACN,MAAM,OAAgC;EACpC,IAAI,KAAK;EACT,MAAM,KAAK;EACX,WAAW,IAAI,KAAK,KAAK,SAAS;EAClC,SAAS,IAAI,KAAK,KAAK,OAAO;EAC9B,OAAO,QAAQ,IAAI;EACnB,eAAe,KAAK,OAAO;EAC3B,SAAS,KAAK;EACd,UAAU,iBAAiB,IAAI;CACjC;CAIA,IAAI,KAAK,UAAU,QACjB,KAAK,QAAQ,KAAK;CAGpB,IAAI,KAAK,WAAW,QAClB,KAAK,SAAS,KAAK;CAGrB,IAAI;CAOJ,MAAM,MAAM,SAAS,IAAI;CAEzB,IAAI,IAAI,QAAQ,GAAG;EACjB,KAAK,QAAQ;GACX,OAAO,IAAI;GACX,QAAQ,IAAI;GACZ,OAAO,IAAI;GACX,MAAM;EACR;EACA,cAAc,OAAO,WAAW,IAAI;CACtC,OACE,cAAc,OAAO,KAAK,IAAI;CAGhC,KAAK,MAAM,SAAS,KAAK,UACvB,gBAAgB,aAAa,KAAK;CAQpC,YAAY,IAAI,EAAE,SAAS,KAAK,QAAQ,CAAC;AAC3C;;;;;;;;;;;;;;;AAgBA,SAAS,SAAS,MAAmE;CACnF,IAAI,aAAa;CACjB,IAAI,cAAc;CAClB,IAAI,aAAa;CAEjB,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,cAAc,MAAM,MAAM;EAC1B,eAAe,MAAM,MAAM;EAC3B,cAAc,MAAM,MAAM;CAC5B;CAEA,OAAO;EACL,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,UAAU;EAChD,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,WAAW;EACnD,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,UAAU;CAClD;AACF;;;;;;;;AASA,MAAM,yBAAyB,IAAI,IAAY;CAC7C,kBAAkB;CAClB,kBAAkB;CAClB,mBAAmB;CACnB,mBAAmB;CACnB,mBAAmB;CACnB,mBAAmB;AACrB,CAAC;;;;;;;AAQD,SAAS,iBAAiB,MAAiD;CACzE,MAAM,MAAM,kBAAkB,IAAI;CAClC,MAAM,WAA2C,CAAC;CAElD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAC3C,IAAI,CAAC,uBAAuB,IAAI,GAAG,GACjC,SAAS,OAAO;CAIpB,OAAO;AACT;;;;;AAMA,SAAS,QAAQ,MAA2C;CAC1D,IAAI,KAAK,WAAW,YAAY,KAAK,WAAW,aAC9C,OAAO;CAGT,OAAO;AACT;;;;ACjTA,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,GAAG;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,EAAE,QAAQ;AACxC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClNA,SAAgB,yBACd,WACA,OAAO,YACU;CACjB,MAAM,iBAAiB,QAAiB,cAA8B;EACpE,IAAI,CAAC,SAAS,MAAM,GAClB;EAGF,AAAK,UAAU,QAAQ,QAAQ,SAAS,EAAE,YAAY,CAGtD,CAAC;CACH;CAEA,MAAM,YAAY,WAA0B;EAG1C,cACG,OAAgC,QAChC,OAA+B,KAClC;CACF;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;;;;;;;;;;;;;;;;ACvEA,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,kBACH,QAAQ,mBAAmB,CAAC,GAAG,wBAAwB;EACzD,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,EAAE,YAAY,CAE3D,CAAC;CACH;AACF;;;;;;;AAQA,SAAS,gBAAgB,SAA2B;CAGlD,QAFgB,SAA4C,SAEpB;AAC1C;;;;;;;AAQA,SAAS,WAAW,SAA0C;CAE5D,MAAM,UADU,SAA4C,SACX;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;;;;;;;;;;;;;;;;AClLA,eAAsB,qBACpB,cACA,QACA,sBAC0B;CAI1B,2CAAuB,cAHT,OAAO,OAAO,UAAU,aAAa,MAAM,OAAO,MAAM,IAAI,OAAO,OAC5D,sBAAsB,KAAK,KAAK,OAAO,YAEJ;AAC1D;;;;;;;;;;;;;;;;ACVA,SAAgB,wBAAwB,MAAqC;CAC3E,IAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,GAC3B;CAGF,KAAK,IAAI,QAAQ,KAAK,MAAM,SAAS,GAAG,SAAS,GAAG,SAAS,GAAG;EAC9D,MAAM,QAAQ,KAAK,MAAM;EAEzB,IAAI,SAAS,OAAO,UAAU,YAAY,MAAM,SAAS,UACvD,OAAO,OAAO,MAAM,YAAY,WAAW,MAAM,UAAU;CAE/D;AAGF;;;;;ACzBA,SAAgB,aAAa,MAAiB,QAAuC;CACnF,IAAI,KAAK,WAAW,QAClB,OAAO;CAGT,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,MAAM,QAAQ,aAAa,OAAO,MAAM;EAExC,IAAI,UAAU,QACZ,OAAO;CAEX;AAGF;;;;;;;;ACVA,MAAM,iBAA0C;CAC9C;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,WAAW,QAAqC;CAC9D,MAAM,QAAoB,CAAC;CAE3B,MAAM,UAAU,OAAO,IAAI,SAAS;CACpC,IAAI,YAAY,QAAQ,QAAQ,SAAS,GACvC,MAAM,UAAU;CAGlB,MAAM,YAAY,OAAO,IAAI,WAAW;CACxC,IAAI,cAAc,QAAQ,UAAU,SAAS,GAC3C,MAAM,YAAY;CAGpB,MAAM,WAAW,OACd,OAAO,QAAQ,EACf,QAAQ,UAAkC,eAAqC,SAAS,KAAK,CAAC;CACjG,IAAI,SAAS,WAAW,GACtB,MAAM,SAAS,SAAS;MACnB,IAAI,SAAS,SAAS,GAC3B,MAAM,SAAS;CAGjB,MAAM,eAAe,OAAO,IAAI,cAAc;CAC9C,IAAI,iBAAiB,QAAQ,aAAa,SAAS,GACjD,MAAM,eAAe;CAGvB,MAAM,gBAAgB,OAAO,IAAI,eAAe;CAChD,IAAI,kBAAkB,QAAQ,cAAc,SAAS,GACnD,MAAM,gBAAgB;CAGxB,OAAO;AACT;;;;;;;;;;;AChEA,MAAa,wBACX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACiCF,SAAgB,cACd,UACA,OACA,kBAA2B,OAC3B,8BAAsC,IAC9B;CACR,MAAM,UAAU,GAAG,SAAS;CAC5B,MAAM,YAAY,WAAW,KAAK;CAElC,OAAO;;;;;SAKA,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;2BA+MQ,sBAAsB;QACzC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAwCJ,KAAK,UAAU,OAAO,EAAE;2BACX,KAAK,UAAU,eAAe,EAAE;;wCAEnB,sBAAsB,2BAA2B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2oC3F;;AAGA,SAAS,WAAW,OAAuB;CACzC,OAAO,MACJ,QAAQ,MAAM,OAAO,EACrB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,MAAM,EACpB,QAAQ,MAAM,QAAQ,EACtB,QAAQ,MAAM,OAAO;AAC1B;;;;;;;;;;;;;;AAeA,SAAS,sBAAsB,OAAuB;CACpD,OAAO,MACJ,MAAM,GAAG,EACT,KAAI,SAAQ,KAAK,UAAU,IAAI,CAAC,EAChC,KAAK,UAAU;AACpB;;;;;ACv7CA,MAAM,0BAA0B,KAAK;;;;;;AAOrC,MAAM,mBAA2C;CAC/C,0BAA0B;CAC1B,mBAAmB;CACnB,mBAAmB;CACnB,2BACE;AACJ;;AAGA,SAAS,eAAe,YAAoD;CAC1E,IAAI,CAAC,YAAY,OAAO;CAExB,IAAI,WAAW,WAAW,GAAG,GAC3B,OAAO,WAAW,MAAM,GAAG,WAAW,QAAQ,GAAG,IAAI,CAAC;CAExD,MAAM,QAAQ,WAAW,QAAQ,GAAG;CACpC,OAAO,UAAU,KAAK,aAAa,WAAW,MAAM,GAAG,KAAK;AAC9D;;;;;;;;;;AAWA,SAAS,kBAAkB,GAAW,GAAoB;CACxD,MAAM,OAAO,OAAO,KAAK,CAAC;CAC1B,MAAM,OAAO,OAAO,KAAK,CAAC;CAE1B,IAAI,KAAK,WAAW,KAAK,QAAQ,OAAO;CAExC,wCAAuB,MAAM,IAAI;AACnC;;;;;;;;;;;;;;;AAgBA,SAAS,aACP,KACA,KACA,OACA,iBACS;CACT,MAAM,SAAS,IAAI,QAAQ;CAC3B,IAAI,UAAU,kBAAkB,QAAQ,UAAU,OAAO,GAAG,OAAO;CACnE,IAAI,CAAC,iBAAiB,OAAO;CAC7B,MAAM,aAAa,IAAI,aAAa,IAAI,OAAO;CAC/C,OAAO,eAAe,QAAQ,kBAAkB,YAAY,KAAK;AACnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,qBACd,OACA,QACqD;CACrD,MAAM,OAAO,OAAO;CACpB,MAAM,YAAY,GAAG,KAAK;CAE1B,OAAO,SAAS,OAAO,KAAsB,KAA2B;EAEtE,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;EACtD,MAAM,WAAW,IAAI;EAIrB,MAAM,OAAO,eAAe,IAAI,QAAQ,IAAI;EAC5C,IAAI,CAAC,QAAQ,CAAC,OAAO,aAAa,SAAS,KAAK,YAAY,CAAC,GAAG;GAC9D,SAAS,KAAK,KAAK,EAAE,OAAO,mBAAmB,CAAC;GAEhD;EACF;EAIA,MAAM,cAAc,aAAa,QAAQ,aAAa,KAAK,QAAQ,OAAO,EAAE;EAC5E,IAAI,OAAO,aAAa,CAAC,aAAa,KAAK,KAAK,OAAO,WAAW,WAAW,GAAG;GAC9E,SAAS,KAAK,KAAK,EAAE,OAAO,eAAe,CAAC;GAE5C;EACF;EAIA,IAAI,IAAI,WAAW,UAAU,OAAO,UAAU;GAC5C,MAAM,QAAQ,kBAAkB,UAAU,SAAS;GAEnD,IAAI,OAAO;IACT,AAAK,eAAe,OAAO,OAAO,UAAU,MAAM,SAAS,MAAM,QAAQ,KAAK,GAAG;IAEjF;GACF;EACF;EAEA,IAAI,IAAI,WAAW,OAAO;GACxB,SAAS,KAAK,KAAK,EAAE,OAAO,qBAAqB,CAAC;GAElD;EACF;EAEA,IAAI,aAAa,GAAG,UAAU,UAAU;GACtC,SAAS,KAAK,KAAK,MAAM,MAAM,WAAW,IAAI,YAAY,CAAC,CAAC;GAE5D;EACF;EAEA,IAAI,SAAS,WAAW,GAAG,UAAU,SAAS,GAAG;GAC/C,MAAM,UAAU,mBAAmB,SAAS,MAAM,GAAG,UAAU,UAAU,MAAM,CAAC;GAChF,MAAM,QAAQ,QAAQ,SAAS,IAAI,MAAM,IAAI,OAAO,IAAI;GAExD,IAAI,UAAU,QAAW;IACvB,SAAS,KAAK,KAAK;KAAE,OAAO;KAAmB;IAAQ,CAAC;IAExD;GACF;GAEA,SAAS,KAAK,KAAK,KAAK;GAExB;EACF;EAEA,IAAI,aAAa,GAAG,UAAU,aAAa;GACzC,SAAS,KAAK,KAAK,MAAM,UAAU,WAAW,IAAI,YAAY,CAAC,CAAC;GAEhE;EACF;EAEA,IAAI,aAAa,QAAQ,aAAa,KAAK,QAAQ,OAAO,EAAE,GAAG;GAC7D,IAAI,UAAU,KAAK;IACjB,gBAAgB;IAChB,GAAG;GACL,CAAC;GACD,IAAI,IAAI,cAAc,MAAM,OAAO,OAAO,QAAQ,OAAO,QAAQ,GAAG,OAAO,UAAU,gBAAgB,EAAE,CAAC;GAExG;EACF;EAEA,SAAS,KAAK,KAAK,EAAE,OAAO,YAAY,CAAC;CAC3C;AACF;;AAGA,SAAS,SAAS,KAAqB,QAAgB,MAAqB;CAC1E,IAAI,UAAU,QAAQ;EACpB,gBAAgB;EAChB,GAAG;CACL,CAAC;CACD,IAAI,IAAI,KAAK,UAAU,IAAI,CAAC;AAC9B;;;;;;;AAQA,SAAS,kBACP,UACA,WACiD;CACjD,MAAM,SAAS,GAAG,UAAU;CAE5B,IAAI,CAAC,SAAS,WAAW,MAAM,GAC7B;CAGF,MAAM,QAAQ,sCAAsC,KAAK,SAAS,MAAM,OAAO,MAAM,CAAC;CAEtF,IAAI,CAAC,OACH;CAGF,OAAO;EACL,SAAS,mBAAmB,MAAM,EAAE;EACpC,QAAQ,mBAAmB,MAAM,EAAE;CACrC;AACF;;;;;;;AAQA,SAAS,aAAgB,KAA8C;CACrE,OAAO,IAAI,SAAS,SAAS,WAAW;EACtC,MAAM,SAAmB,CAAC;EAC1B,IAAI,OAAO;EAEX,IAAI,GAAG,SAAS,UAAkB;GAChC,QAAQ,MAAM;GAEd,IAAI,OAAO,yBAAyB;IAClC,IAAI,QAAQ;IACZ,uBAAO,IAAI,MAAM,mBAAmB,CAAC;IAErC;GACF;GAEA,OAAO,KAAK,KAAK;EACnB,CAAC;EAED,IAAI,GAAG,aAAa;GAClB,IAAI,OAAO,WAAW,GAAG;IACvB,QAAQ,MAAS;IAEjB;GACF;GAEA,IAAI;IACF,QAAQ,KAAK,MAAM,OAAO,OAAO,MAAM,EAAE,SAAS,OAAO,CAAC,CAAM;GAClE,QAAQ;IACN,uBAAO,IAAI,MAAM,cAAc,CAAC;GAClC;EACF,CAAC;EAED,IAAI,GAAG,SAAS,MAAM;CACxB,CAAC;AACH;;;;;;;;;AAUA,eAAe,eACb,OACA,UACA,SACA,QACA,KACA,KACe;CACf,MAAM,QAAQ,MAAM,IAAI,OAAO;CAE/B,IAAI,UAAU,QAAW;EACvB,SAAS,KAAK,KAAK;GAAE,OAAO;GAAmB;EAAQ,CAAC;EAExD;CACF;CAEA,MAAM,OAAO,aAAa,MAAM,MAAM,MAAM;CAE5C,IAAI,SAAS,QAAW;EACtB,SAAS,KAAK,KAAK;GAAE,OAAO;GAAkB;EAAO,CAAC;EAEtD;CACF;CAEA,MAAM,eAAe,wBAAwB,IAAI;CAEjD,IAAI,iBAAiB,QAAW;EAC9B,SAAS,KAAK,KAAK,EAAE,OAAO,mBAAmB,CAAC;EAEhD;CACF;CAEA,IAAI;CAEJ,IAAI;EACF,OAAO,MAAM,aAAkC,GAAG;CACpD,SAAS,OAAO;EACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU;EACzD,SAAS,KAAK,YAAY,sBAAsB,MAAM,KAAK,EAAE,OAAO,QAAQ,CAAC;EAE7E;CACF;CAEA,IAAI;EAEF,SAAS,KAAK,KAAK,MADG,qBAAqB,cAAc,UAAU,MAAM,YAAY,CAC3D;CAC5B,SAAS,OAAO;EACd,SAAS,KAAK,KAAK;GACjB,OAAO;GACP,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;EAChE,CAAC;CACH;AACF;;;;ACrWA,MAAM,eAAe;AACrB,MAAM,eAAe;AACrB,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;AA0BtB,SAAgB,UACd,OACA,UAA4B,CAAC,GACH;CAC1B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,QAAQ,QAAQ,SAAS;CAC/B,MAAM,WAAW,kBAAkB,QAAQ,QAAQ;CAKnD,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC,QAAQ,WACpC,OAAO,QAAQ,uBACb,IAAI,MACF,wDAAwD,KAAK,mHAE/D,CACF;CAGF,MAAM,gBAAgB,QAAQ,gBAAgB,oBAAoB,IAAI,GAAG,KAAI,MAC3E,EAAE,YAAY,CAChB;CASA,MAAM,qCAPU,qBAAqB,OAAO;EAC1C;EACA;EACA,WAAW,QAAQ;EACnB;EACA,UAAU,QAAQ;CACpB,CACkC,CAAC;CAEnC,OAAO,IAAI,SAA0B,SAAS,WAAW;EACvD,MAAM,WAAW,UAAuC;GACtD,OAAO,IAAI,SAAS,OAAO;GAE3B,IAAI,MAAM,SAAS,cAAc;IAC/B,uBACE,IAAI,MACF,4BAA4B,KAAK,gDACnC,CACF;IAEA;GACF;GAEA,OAAO,KAAK;EACd;EAEA,OAAO,GAAG,SAAS,OAAO;EAE1B,OAAO,OAAO,MAAM,YAAY;GAC9B,OAAO,IAAI,SAAS,OAAO;GAE3B,MAAM,UAAU,OAAO,QAAQ;GAC/B,MAAM,eAAe,OAAO,YAAY,YAAY,YAAY,OAAO,QAAQ,OAAO;GACtF,MAAM,MAAM,UAAU,KAAK,GAAG,eAAe;GAE7C,IAAI,QAAQ,MACV,YAAY,GAAG;GAGjB,QAAQ;IACN;IACA,MAAM;IACN,QAAuB;KACrB,OAAO,IAAI,SAAe,cAAc,gBAAgB;MACtD,OAAO,OAAO,eAAe;OAC3B,IAAI,YAAY;QACd,YAAY,UAAU;QAEtB;OACF;OAEA,aAAa;MACf,CAAC;KACH,CAAC;IACH;GACF,CAAC;EACH,CAAC;CACH,CAAC;AACH;;AAGA,SAAS,eAAe,MAAuB;CAC7C,MAAM,IAAI,KAAK,YAAY;CAC3B,OAAO,MAAM,eAAe,MAAM,SAAS,MAAM,WAAW,MAAM;AACpE;;;;;;AAOA,SAAS,oBAAoB,MAAwB;CACnD,IAAI,eAAe,IAAI,GACrB,OAAO;EAAC;EAAa;EAAa;EAAS;CAAK;CAElD,OAAO,CAAC,IAAI;AACd;;;;;AAMA,SAAS,kBAAkB,UAA2B;CACpD,IAAI,aAAa,UAAa,SAAS,WAAW,KAAK,aAAa,KAClE,OAAO;CAGT,MAAM,cAAc,SAAS,WAAW,GAAG,IAAI,WAAW,IAAI;CAE9D,OAAO,YAAY,SAAS,GAAG,IAAI,cAAc,GAAG,YAAY;AAClE;;;;;;;;AASA,SAAS,YAAY,KAAmB;CACtC,AAAK,OAAO,sBACT,MAAM,EAAE,YAAY;EAKnB,MAAM,QAAQ,MAHZ,QAAQ,aAAa,UAAU,QAAQ,QAAQ,aAAa,WAAW,SAAS,YACrE,QAAQ,aAAa,UAAU;GAAC;GAAM;GAAS;GAAI;EAAG,IAAI,CAAC,GAAG,GAExC;GAAE,OAAO;GAAU,UAAU;EAAK,CAAC;EACtE,MAAM,GAAG,eAAe,CAExB,CAAC;EACD,MAAM,MAAM;CACd,CAAC,EACA,YAAY,CAEb,CAAC;AACL;;;;;ACzHA,MAAM,iBAA0C,CAAC,UAAU,WAAW;;;;;;AAOtE,MAAM,mBAAmB;AACzB,MAAM,sBAAsB;;;;;;;;;;;;AAa5B,SAAgB,eAAe,OAAkC;CAC/D,MAAM,aAAa,MAAM,KAAK;CAE9B,IAAI,eAAe,QACjB;CAGF,MAAM,OAAO,WAAW;CAExB,IAAI,OAAO,SAAS,YAAY,KAAK,WAAW,GAC9C;CAGF,MAAM,UAAU,WAAW;CAG3B,OAAO,GAAG,KAAK,GAFM,OAAO,YAAY,YAAY,QAAQ,SAAS,IAAI,UAAU;AAGrF;;;;;;AAOA,SAAgB,cAAc,OAAc,QAA8B;CACxE,MAAM,OAAO,MAAM;CAEnB,IAAI,OAAO,eAAe,QAAQ,CAAC,eAAe,SAAS,KAAK,MAAM,GACpE,OAAO;CAGT,IAAI,OAAO,aAAa,UAAa,OAAO,SAAS,SAAS,GAC5D;MAAI,CAAC,OAAO,SAAS,SAAS,KAAK,MAAM,GACvC,OAAO;CACT;CAGF,IAAI,OAAO,UAAU,UAAa,OAAO,MAAM,SAAS,GACtD;MAAI,CAAC,OAAO,MAAM,SAAS,KAAK,IAAI,GAClC,OAAO;CACT;CAGF,IAAI,OAAO,cAAc,UAAa,OAAO,UAAU,SAAS,GAC9D;MAAI,MAAM,cAAc,OAAO,WAC7B,OAAO;CACT;CAGF,IAAI,OAAO,cAAc,UAAa,OAAO,UAAU,SAAS,GAC9D;MAAI,eAAe,KAAK,MAAM,OAAO,WACnC,OAAO;CACT;CAGF,MAAM,OAAO,OAAO,MAAM,KAAK,EAAE,YAAY;CAC7C,IAAI,SAAS,UAAa,KAAK,SAAS,GAEtC;MAAI,CADa,GAAG,KAAK,KAAK,GAAG,MAAM,aAAa,KAAK,YAC7C,EAAE,SAAS,IAAI,GACzB,OAAO;CACT;CAGF,OAAO;AACT;;AAGA,SAAgB,aAAa,QAAiB,QAA8B;CAC1E,OAAO,OAAO,QAAQ,UAAU,cAAc,OAAO,MAAM,CAAC;AAC9D;;;;;;AAOA,MAAa,iBAAiB;;;;;;AAe9B,SAAgB,eAAe,QAAiC;CAC9D,MAAM,QAAkB,CAAC;CACzB,MAAM,wBAAQ,IAAI,IAAqB;CAEvC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM;EAClB,IAAI,SAAS,MAAM,IAAI,GAAG;EAE1B,IAAI,WAAW,QAAW;GACxB,SAAS,CAAC;GACV,MAAM,IAAI,KAAK,MAAM;GACrB,MAAM,KAAK,GAAG;EAChB;EAEA,OAAO,KAAK,KAAK;CACnB;CAEA,OAAO,MAAM,KAAK,eAAe;EAAE;EAAW,QAAQ,MAAM,IAAI,SAAS,KAAK,CAAC;CAAE,EAAE;AACrF;;;;;;AAOA,MAAa,gBAAgB;;;;;;;;;;;;AAqB7B,SAAgB,cAAc,QAAgC;CAC5D,MAAM,QAAkB,CAAC;CACzB,MAAM,wBAAQ,IAAI,IAAqB;CAEvC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,eAAe,KAAK;EAChC,IAAI,SAAS,MAAM,IAAI,GAAG;EAE1B,IAAI,WAAW,QAAW;GACxB,SAAS,CAAC;GACV,MAAM,IAAI,KAAK,MAAM;GACrB,MAAM,KAAK,GAAG;EAChB;EAEA,OAAO,KAAK,KAAK;CACnB;CAEA,OAAO,MAAM,KAAK,eAAe;EAAE;EAAW,QAAQ,MAAM,IAAI,SAAS,KAAK,CAAC;CAAE,EAAE;AACrF;;;;;;AAOA,MAAa,cAAc;;;;;;;;;;;;;AAsB3B,SAAgB,YAAY,QAA8B;CACxD,MAAM,QAAkB,CAAC;CACzB,MAAM,wBAAQ,IAAI,IAAqB;CAEvC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM,KAAK;EACvB,IAAI,SAAS,MAAM,IAAI,GAAG;EAE1B,IAAI,WAAW,QAAW;GACxB,SAAS,CAAC;GACV,MAAM,IAAI,KAAK,MAAM;GACrB,MAAM,KAAK,GAAG;EAChB;EAEA,OAAO,KAAK,KAAK;CACnB;CAEA,OAAO,MAAM,KAAK,UAAU;EAAE;EAAM,QAAQ,MAAM,IAAI,IAAI,KAAK,CAAC;CAAE,EAAE;AACtE;;;;;AA0BA,SAAgB,WAAW,QAAkB,GAAmB;CAC9D,IAAI,OAAO,WAAW,GACpB,OAAO;CAGT,MAAM,SAAS,OAAO,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,CAAC;CAClD,MAAM,OAAO,KAAK,KAAM,IAAI,MAAO,OAAO,MAAM,IAAI;CAGpD,OAAO,OAFO,KAAK,IAAI,KAAK,IAAI,MAAM,CAAC,GAAG,OAAO,SAAS,CAExC;AACpB;;;;;;AAOA,SAAgB,UAAU,OAAsB;CAC9C,MAAM,MAAM,WAAW,MAAM,MAAM,IAAI;CACvC,OAAO,MAAM,IAAI,MAAM,WAAW,MAAM,IAAI;AAC9C;;;;;;;;AASA,SAAgB,gBAAgB,QAA6B;CAC3D,MAAM,QAAkB,CAAC;CACzB,MAAM,wBAAQ,IAAI,IAAqB;CAEvC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,MAAM,KAAK;EACvB,IAAI,SAAS,MAAM,IAAI,GAAG;EAE1B,IAAI,WAAW,QAAW;GACxB,SAAS,CAAC;GACV,MAAM,IAAI,KAAK,MAAM;GACrB,MAAM,KAAK,GAAG;EAChB;EAEA,OAAO,KAAK,KAAK;CACnB;CAEA,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,SAAS,MAAM,IAAI,IAAI,KAAK,CAAC;EACnC,MAAM,YAAY,OAAO,KAAK,UAAU,MAAM,QAAQ;EACtD,IAAI,SAAS;EACb,IAAI,SAAS;EACb,IAAI,OAAO;EAEX,KAAK,MAAM,SAAS,QAAQ;GAC1B,IAAI,eAAe,SAAS,MAAM,KAAK,MAAM,GAC3C,UAAU;GAGZ,UAAU,MAAM,MAAM,SAAS;GAC/B,QAAQ,UAAU,KAAK;EACzB;EAEA,OAAO;GACL;GACA,OAAO,OAAO;GACd;GACA,UAAU,OAAO,SAAS,IAAI,SAAS,OAAO,SAAS;GACvD,KAAK,WAAW,WAAW,EAAE;GAC7B,KAAK,WAAW,WAAW,EAAE;GAC7B;GACA;EACF;CACF,CAAC;AACH;;;;;;AAOA,SAAS,WAAW,MAA0C;CAC5D,IAAI,SAAS,QACX,OAAO;CAGT,QACG,KAAK,SAAS,MACd,KAAK,UAAU,MACf,KAAK,eAAe,MACpB,KAAK,gBAAgB,MACrB,KAAK,aAAa;AAEvB;;;;;;;AAQA,SAAgB,WAAW,MAAyB;CAClD,MAAM,MAAM,WAAW,KAAK,MAAM,IAAI;CACtC,IAAI,MAAM,GACR,OAAO;CAGT,IAAI,MAAM;CACV,KAAK,MAAM,SAAS,KAAK,UACvB,OAAO,WAAW,KAAK;CAGzB,OAAO;AACT;;;;;;;AAQA,SAAgB,cAAc,UAAkB,SAAyB;CACvE,IAAI,WAAW,KAAK,YAAY,GAC9B,OAAO;CAGT,MAAM,QAAQ,WAAW;CAEzB,OAAO,QAAQ,IAAI,IAAI;AACzB;;;;;AAMA,SAAgB,YAAY,MAAyB;CACnD,IAAI,MAAM,WAAW,IAAI;CAEzB,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,MAAM,WAAW,YAAY,KAAK;EAClC,IAAI,WAAW,KACb,MAAM;CAEV;CAEA,OAAO;AACT;;;;ACnaA,IAAI;;;;;;;;;;;;;;;;;;;;;AAsBJ,SAAgB,oBAAoB,QAA+B;CACjE,IAAI,WAAW,QACb;CAIF,kCAAc,QAAQ,OAAO,UAAU,CAAC;CAExC,IAAI,YAAY,QAAW;EACzB,MAAM,YAAY,CAAC,GAAI,OAAO,aAAa,CAAC,CAAE;EAC9C,IAAI,QAAQ,UAAU,SAAS;EAI/B,IAAI;EAOJ,IAAI,UAAU,UAAa,OAAO,WAAW;GAC3C,IAAI,OAAO,UAAU,QAAW;IAC9B,MAAM,UACJ,OAAO,aACL,UAAmBG,uBAAI,MAAM,eAAe,cAAc,KAAK;IAEnE,aAAa,sBAAsB,OAAO,OAAO,EAAE,QAAQ,CAAC;IAC5D,QAAQ;GACV,OACE,QAAQ,yBAAyB;GAGnC,UAAU,KAAK,KAAoC;EACrD;EAEA,MAAM,mBAAmB,SAAS;GAChC;GACA,gBAAgB,OAAO;GACvB,aAAa,OAAO;EACtB,CAAC;EACD,qCAAiB,WAAW,gBAAgB,CAAC;EAE7C,UAAU,EAAE,iBAAiB;EAE7B,IAAI,OAAO,aAAa,UAAU,QAAW;GAC3C,MAAM,gBAAgB;GAQtB,CAFE,eAAe,SAAY,WAAW,MAAM,IAAI,QAAQ,QAAQ,GAG/D,YAAY,CAEb,CAAC,EACA,WAAW,eAAe,eAAe,OAAO,aAAa,CAAC,CAAC,CAAC,EAChE,MAAM,WAAW;IAChB,IAAI,YAAY,QACd,QAAQ,kBAAkB;GAE9B,CAAC;EACL;CACF;AACF;;;;;;;;AAkCA,SAAS,WAAW,UAA8B;CAChD,OAAO,EACL,QAAQ,QAAwC;EAC9C,OAAO,SAAS,QAAQ,MAAM;CAChC,EACF;AACF;;;;;;;AAQA,SAAS,UAAU,WAA+D;CAChF,KAAK,MAAM,YAAY,WAAW;EAChC,MAAM,YAAY;EAElB,IACE,OAAO,UAAU,UAAU,cAC3B,OAAO,UAAU,QAAQ,cACzB,OAAO,UAAU,cAAc,YAE/B,OAAO;CAEX;AAGF;;AAGA,SAAS,eACP,OACA,iBAC0B;CAI1B,OAAO,UAAU,OAFf,OAAO,oBAAoB,WAAW,kBAAkB,CAAC,CAE5B;AACjC;;;;qCClLiB,WAAW;CAC1B,oBAAoB,OAAO,QAAQ;AACrC,CAAC;AAID,oDAAgC,EAAE,QAAQ"}
|