@warlock.js/ai-panoptic 5.32.0 → 5.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"collector.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/collector.ts"],"mappings":";;;;;;;;;YAcY,wBAAwB,sBAAsB;;YAG9C,mBAAmB;;;;;;;EAO7B,UAAU;;;;;;;;;;;;;;wBAyJI,gBAAgB,UAAS,mBAAwB"}
1
+ {"version":3,"file":"collector.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/collector.ts"],"mappings":";;;;;;;;;YAeY,wBAAwB,sBAAsB;;YAG9C,mBAAmB;;;;;;;EAO7B,UAAU;;;;;;;;;;;;;;wBA0JI,gBAAgB,UAAS,mBAAwB"}
@@ -1,5 +1,6 @@
1
1
  import { walkSpans } from "../exporters/utils/walk-spans.mjs";
2
2
  import { reportToTrace } from "./report-to-trace.mjs";
3
+ import { publishTrace } from "./trace-subscriptions.mjs";
3
4
 
4
5
  //#region ../../packages/ai-panoptic/src/collector/collector.ts
5
6
  /**
@@ -51,6 +52,7 @@ var Collector = class {
51
52
  }
52
53
  async collect(report, rootError) {
53
54
  const trace = this.toTrace(report, rootError);
55
+ publishTrace(trace);
54
56
  await this.dispatch(trace);
55
57
  }
56
58
  async flush() {
@@ -1 +1 @@
1
- {"version":3,"file":"collector.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/collector.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AA8CA,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,CAAC,CAAC,KAAK,CAAC,MAAM,cACrD,QAAQ,QAAQ,CAAC,CACd,WAAW,KAAK,QAAQ,CAAC,CAAC,CAC1B,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"}
1
+ {"version":3,"file":"collector.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/collector.ts"],"sourcesContent":["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\";\nimport { publishTrace } from \"./trace-subscriptions\";\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 publishTrace(trace);\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AA+CA,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,aAAa,KAAK;EAClB,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,CAAC,CAAC,KAAK,CAAC,MAAM,cACrD,QAAQ,QAAQ,CAAC,CACd,WAAW,KAAK,QAAQ,CAAC,CAAC,CAC1B,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"extract-span-attributes.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/extract-span-attributes.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;wBAwDgB,sBAAsB,QAAQ,aAAa"}
1
+ {"version":3,"file":"extract-span-attributes.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/extract-span-attributes.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;wBAyDgB,sBAAsB,QAAQ,aAAa"}
@@ -24,7 +24,16 @@
24
24
  function extractSpanAttributes(report) {
25
25
  const extensions = report;
26
26
  const attributes = {};
27
- if (report.attempts !== void 0 && report.attempts.length > 0) attributes["retries"] = report.attempts.length;
27
+ if (report.attempts !== void 0 && report.attempts.length > 0) {
28
+ attributes["retries"] = report.attempts.length;
29
+ attributes["retry.attempts"] = report.attempts.map((attempt) => ({
30
+ index: attempt.index,
31
+ startedAt: attempt.startedAt,
32
+ endedAt: attempt.endedAt,
33
+ duration: attempt.duration,
34
+ status: attempt.status
35
+ }));
36
+ }
28
37
  switch (report.type) {
29
38
  case "agent":
30
39
  addAgentAttributes(attributes, extensions);
@@ -45,7 +54,25 @@ function extractSpanAttributes(report) {
45
54
  return attributes;
46
55
  }
47
56
  function addAgentAttributes(attributes, extensions) {
48
- if (Array.isArray(extensions.trips)) attributes["agent.trips"] = extensions.trips.length;
57
+ if (Array.isArray(extensions.trips)) {
58
+ attributes["agent.trips"] = extensions.trips.length;
59
+ if (extensions.trips.some((trip) => trip.usage?.cachedTokens !== void 0)) attributes["agent.cacheHits"] = extensions.trips.filter((trip) => (trip.usage?.cachedTokens ?? 0) > 0).length;
60
+ const truncatedTrips = extensions.trips.filter((trip) => trip.finishReason === "length" || trip.finishReason === "context_exceeded").length;
61
+ if (truncatedTrips > 0) attributes["agent.truncatedTrips"] = truncatedTrips;
62
+ const providerCompactions = extensions.trips.reduce((count, trip) => count + (trip.blocks?.filter((block) => block.type === "compaction").length ?? 0), 0);
63
+ if (providerCompactions > 0) attributes["agent.providerCompactions"] = providerCompactions;
64
+ }
65
+ if (extensions.compactions?.length) attributes["agent.compactions"] = extensions.compactions.map((record) => ({
66
+ type: record.type,
67
+ tripIndex: record.tripIndex,
68
+ inputTokens: record.inputTokens,
69
+ counting: record.counting,
70
+ reason: record.reason,
71
+ ...record.compaction ? {
72
+ replacesFromIndex: record.compaction.replacesFromIndex,
73
+ replacesToIndex: record.compaction.replacesToIndex
74
+ } : {}
75
+ }));
49
76
  if (extensions.model?.name !== void 0) attributes["agent.model.name"] = extensions.model.name;
50
77
  if (extensions.model?.provider !== void 0) attributes["agent.model.provider"] = extensions.model.provider;
51
78
  if (extensions.promptName !== void 0) attributes["agent.promptName"] = extensions.promptName;
@@ -1 +1 @@
1
- {"version":3,"file":"extract-span-attributes.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/extract-span-attributes.ts"],"sourcesContent":["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 callId?: string;\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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): 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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): 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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): 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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): 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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): void {\n if (extensions.tripIndex !== undefined) {\n attributes[\"tool.tripIndex\"] = extensions.tripIndex;\n }\n\n if (extensions.callId !== undefined) {\n attributes[\"tool.callId\"] = extensions.callId;\n }\n\n if (extensions.recoveredFrom !== undefined) {\n attributes[\"tool.recoveredFrom\"] = extensions.recoveredFrom;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAwDA,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,QACH,kBAAkB,YAAY,UAAU;CAO5C;CAEA,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,WAAW,GACrC;CAGF,OAAO;AACT;AAEA,SAAS,mBACP,YACA,YACM;CACN,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,sBACP,YACA,YACM;CACN,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,CAAC,CAAC;AAEjE;AAEA,SAAS,wBACP,YACA,YACM;CACN,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,0BACP,YACA,YACM;CACN,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,kBACP,YACA,YACM;CACN,IAAI,WAAW,cAAc,QAC3B,WAAW,oBAAoB,WAAW;CAG5C,IAAI,WAAW,WAAW,QACxB,WAAW,iBAAiB,WAAW;CAGzC,IAAI,WAAW,kBAAkB,QAC/B,WAAW,wBAAwB,WAAW;AAElD"}
1
+ {"version":3,"file":"extract-span-attributes.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/extract-span-attributes.ts"],"sourcesContent":["import type { AgentCompactionRecord, BaseReport, LLMTrip } 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?: LLMTrip[];\n compactions?: AgentCompactionRecord[];\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 callId?: string;\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 attributes[\"retry.attempts\"] = report.attempts.map((attempt) => ({\n index: attempt.index,\n startedAt: attempt.startedAt,\n endedAt: attempt.endedAt,\n duration: attempt.duration,\n status: attempt.status,\n }));\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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): void {\n if (Array.isArray(extensions.trips)) {\n attributes[\"agent.trips\"] = extensions.trips.length;\n // Reported provider cache reads only. Zero-token synthetic responses do\n // not identify semantic-cache hits in the core report contract.\n if (extensions.trips.some((trip) => trip.usage?.cachedTokens !== undefined)) {\n attributes[\"agent.cacheHits\"] = extensions.trips.filter(\n (trip) => (trip.usage?.cachedTokens ?? 0) > 0,\n ).length;\n }\n const truncatedTrips = extensions.trips.filter(\n (trip) => trip.finishReason === \"length\" || trip.finishReason === \"context_exceeded\",\n ).length;\n if (truncatedTrips > 0) attributes[\"agent.truncatedTrips\"] = truncatedTrips;\n const providerCompactions = extensions.trips.reduce(\n (count, trip) =>\n count + (trip.blocks?.filter((block) => block.type === \"compaction\").length ?? 0),\n 0,\n );\n if (providerCompactions > 0) attributes[\"agent.providerCompactions\"] = providerCompactions;\n }\n\n if (extensions.compactions?.length) {\n // Do not copy summary text, callback errors or raw conversation content.\n attributes[\"agent.compactions\"] = extensions.compactions.map((record) => ({\n type: record.type,\n tripIndex: record.tripIndex,\n inputTokens: record.inputTokens,\n counting: record.counting,\n reason: record.reason,\n ...(record.compaction\n ? {\n replacesFromIndex: record.compaction.replacesFromIndex,\n replacesToIndex: record.compaction.replacesToIndex,\n }\n : {}),\n }));\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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): 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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): 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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): 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(\n attributes: Record<string, unknown>,\n extensions: ReportExtensions,\n): void {\n if (extensions.tripIndex !== undefined) {\n attributes[\"tool.tripIndex\"] = extensions.tripIndex;\n }\n\n if (extensions.callId !== undefined) {\n attributes[\"tool.callId\"] = extensions.callId;\n }\n\n if (extensions.recoveredFrom !== undefined) {\n attributes[\"tool.recoveredFrom\"] = extensions.recoveredFrom;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAyDA,SAAgB,sBAAsB,QAAyD;CAC7F,MAAM,aAAa;CACnB,MAAM,aAAsC,CAAC;CAE7C,IAAI,OAAO,aAAa,UAAa,OAAO,SAAS,SAAS,GAAG;EAC/D,WAAW,aAAa,OAAO,SAAS;EACxC,WAAW,oBAAoB,OAAO,SAAS,KAAK,aAAa;GAC/D,OAAO,QAAQ;GACf,WAAW,QAAQ;GACnB,SAAS,QAAQ;GACjB,UAAU,QAAQ;GAClB,QAAQ,QAAQ;EAClB,EAAE;CACJ;CAEA,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,QACH,kBAAkB,YAAY,UAAU;CAO5C;CAEA,IAAI,OAAO,KAAK,UAAU,CAAC,CAAC,WAAW,GACrC;CAGF,OAAO;AACT;AAEA,SAAS,mBACP,YACA,YACM;CACN,IAAI,MAAM,QAAQ,WAAW,KAAK,GAAG;EACnC,WAAW,iBAAiB,WAAW,MAAM;EAG7C,IAAI,WAAW,MAAM,MAAM,SAAS,KAAK,OAAO,iBAAiB,MAAS,GACxE,WAAW,qBAAqB,WAAW,MAAM,QAC9C,UAAU,KAAK,OAAO,gBAAgB,KAAK,CAC9C,CAAC,CAAC;EAEJ,MAAM,iBAAiB,WAAW,MAAM,QACrC,SAAS,KAAK,iBAAiB,YAAY,KAAK,iBAAiB,kBACpE,CAAC,CAAC;EACF,IAAI,iBAAiB,GAAG,WAAW,0BAA0B;EAC7D,MAAM,sBAAsB,WAAW,MAAM,QAC1C,OAAO,SACN,SAAS,KAAK,QAAQ,QAAQ,UAAU,MAAM,SAAS,YAAY,CAAC,CAAC,UAAU,IACjF,CACF;EACA,IAAI,sBAAsB,GAAG,WAAW,+BAA+B;CACzE;CAEA,IAAI,WAAW,aAAa,QAE1B,WAAW,uBAAuB,WAAW,YAAY,KAAK,YAAY;EACxE,MAAM,OAAO;EACb,WAAW,OAAO;EAClB,aAAa,OAAO;EACpB,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,GAAI,OAAO,aACP;GACE,mBAAmB,OAAO,WAAW;GACrC,iBAAiB,OAAO,WAAW;EACrC,IACA,CAAC;CACP,EAAE;CAGJ,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,sBACP,YACA,YACM;CACN,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,CAAC,CAAC;AAEjE;AAEA,SAAS,wBACP,YACA,YACM;CACN,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,0BACP,YACA,YACM;CACN,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,kBACP,YACA,YACM;CACN,IAAI,WAAW,cAAc,QAC3B,WAAW,oBAAoB,WAAW;CAG5C,IAAI,WAAW,WAAW,QACxB,WAAW,iBAAiB,WAAW;CAGzC,IAAI,WAAW,kBAAkB,QAC/B,WAAW,wBAAwB,WAAW;AAElD"}
@@ -4,4 +4,5 @@ import { reportToTrace } from "./report-to-trace.mjs";
4
4
  import { reportToSpan } from "./report-to-span.mjs";
5
5
  import { extractSpanAttributes } from "./extract-span-attributes.mjs";
6
6
  import { normalizeError } from "./normalize-error.mjs";
7
- export { type CollectorOptions, type ContentCaptureOptions, type ContentRedactor, type ExporterErrorHandler, createCollector, extractSpanAttributes, normalizeError, reportToSpan, reportToTrace };
7
+ import { TraceListener, subscribeTraces } from "./trace-subscriptions.mjs";
8
+ export { type CollectorOptions, type ContentCaptureOptions, type ContentRedactor, type ExporterErrorHandler, type TraceListener, createCollector, extractSpanAttributes, normalizeError, reportToSpan, reportToTrace, subscribeTraces };
@@ -2,6 +2,7 @@ import { normalizeError } from "./normalize-error.mjs";
2
2
  import { extractSpanAttributes } from "./extract-span-attributes.mjs";
3
3
  import { reportToSpan } from "./report-to-span.mjs";
4
4
  import { reportToTrace } from "./report-to-trace.mjs";
5
+ import { subscribeTraces } from "./trace-subscriptions.mjs";
5
6
  import { createCollector } from "./collector.mjs";
6
7
 
7
- export { createCollector, extractSpanAttributes, normalizeError, reportToSpan, reportToTrace };
8
+ export { createCollector, extractSpanAttributes, normalizeError, reportToSpan, reportToTrace, subscribeTraces };
@@ -0,0 +1,14 @@
1
+ import { Trace } from "../contracts/trace.type.mjs";
2
+ //#region ../../packages/ai-panoptic/src/collector/trace-subscriptions.d.ts
3
+ /** Receives a finalized trace in the collecting call's async context. Treat it as read-only. */
4
+ export type TraceListener = (trace: Trace) => void | Promise<void>;
5
+ /**
6
+ * Subscribe to traces from every built-in collector in this process, including
7
+ * collectors created before subscribing. No replay or retention. Called before
8
+ * exporter dispatch, so adapters can read request-local context synchronously.
9
+ * Listener failures are isolated; async listeners are not awaited. The returned
10
+ * disposer removes only this subscription. Does not enable flow observation.
11
+ */
12
+ export declare function subscribeTraces(listener: TraceListener): () => void;
13
+ //#endregion
14
+ //# sourceMappingURL=trace-subscriptions.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace-subscriptions.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/trace-subscriptions.ts"],"mappings":";;;YAGY,iBAAiB,OAAO,iBAAiB;;;;;;;;wBAWrC,gBAAgB,UAAU"}
@@ -0,0 +1,26 @@
1
+ //#region ../../packages/ai-panoptic/src/collector/trace-subscriptions.ts
2
+ const listeners = /* @__PURE__ */ new Set();
3
+ /**
4
+ * Subscribe to traces from every built-in collector in this process, including
5
+ * collectors created before subscribing. No replay or retention. Called before
6
+ * exporter dispatch, so adapters can read request-local context synchronously.
7
+ * Listener failures are isolated; async listeners are not awaited. The returned
8
+ * disposer removes only this subscription. Does not enable flow observation.
9
+ */
10
+ function subscribeTraces(listener) {
11
+ const subscription = (trace) => listener(trace);
12
+ listeners.add(subscription);
13
+ return () => {
14
+ listeners.delete(subscription);
15
+ };
16
+ }
17
+ /** Internal collector notification; pure toTrace projections never publish. */
18
+ function publishTrace(trace) {
19
+ for (const listener of listeners) try {
20
+ Promise.resolve(listener(trace)).catch(() => {});
21
+ } catch {}
22
+ }
23
+
24
+ //#endregion
25
+ export { publishTrace, subscribeTraces };
26
+ //# sourceMappingURL=trace-subscriptions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace-subscriptions.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/collector/trace-subscriptions.ts"],"sourcesContent":["import type { Trace } from \"../contracts/trace.type\";\n\n/** Receives a finalized trace in the collecting call's async context. Treat it as read-only. */\nexport type TraceListener = (trace: Trace) => void | Promise<void>;\n\nconst listeners = new Set<TraceListener>();\n\n/**\n * Subscribe to traces from every built-in collector in this process, including\n * collectors created before subscribing. No replay or retention. Called before\n * exporter dispatch, so adapters can read request-local context synchronously.\n * Listener failures are isolated; async listeners are not awaited. The returned\n * disposer removes only this subscription. Does not enable flow observation.\n */\nexport function subscribeTraces(listener: TraceListener): () => void {\n const subscription: TraceListener = (trace) => listener(trace);\n listeners.add(subscription);\n return () => {\n listeners.delete(subscription);\n };\n}\n\n/** Internal collector notification; pure toTrace projections never publish. */\nexport function publishTrace(trace: Trace): void {\n for (const listener of listeners) {\n try {\n void Promise.resolve(listener(trace)).catch(() => {});\n } catch {\n // A dashboard or adapter must never fail the originating AI run.\n }\n }\n}\n"],"mappings":";AAKA,MAAM,4BAAY,IAAI,IAAmB;;;;;;;;AASzC,SAAgB,gBAAgB,UAAqC;CACnE,MAAM,gBAA+B,UAAU,SAAS,KAAK;CAC7D,UAAU,IAAI,YAAY;CAC1B,aAAa;EACX,UAAU,OAAO,YAAY;CAC/B;AACF;;AAGA,SAAgB,aAAa,OAAoB;CAC/C,KAAK,MAAM,YAAY,WACrB,IAAI;EACF,AAAK,QAAQ,QAAQ,SAAS,KAAK,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC;CACtD,QAAQ,CAER;AAEJ"}
@@ -138,6 +138,8 @@ export type TraceSpanError = {
138
138
  * `.execute()` / `.invoke()` call).
139
139
  */
140
140
  export type Trace = {
141
+ /** HTTP request correlation supplied by an integration; independent of sessionId. */
142
+ requestId?: string;
141
143
  /** Trace id — equals the root span's `traceId` / `rootRunId`. */
142
144
  traceId: string;
143
145
  /** Session grouping id, when the originating run carried one. */
@@ -1 +1 @@
1
- {"version":3,"file":"trace.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/contracts/trace.type.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAgCY;;;;;EAKV;;;;;EAKA;;;;;;;EAOA;;;;;;EAMA;;EAEA;;;;;;EAMA;;EAEA,MAAM;;EAEN,QAAQ;;EAER;;EAEA;;EAEA;;EAEA,OAAO;;;;;;EAMP,QAAQ;;;;;;;;;;EAUR,aAAa;;;;;;;;EAQb;;;;;;EAMA;;;;;;EAMA,UAAU;;;;;;;YAQA;;EAEV;;EAEA;;EAEA;;;;;;;EAOA;;;;;;;;YASU;;EAEV;;EAEA;;EAEA,MAAM;;EAEN;;EAEA;;EAEA;;EAEA,OAAO;;;;;;EAMP"}
1
+ {"version":3,"file":"trace.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/contracts/trace.type.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAgCY;;;;;EAKV;;;;;EAKA;;;;;;;EAOA;;;;;;EAMA;;EAEA;;;;;;EAMA;;EAEA,MAAM;;EAEN,QAAQ;;EAER;;EAEA;;EAEA;;EAEA,OAAO;;;;;;EAMP,QAAQ;;;;;;;;;;EAUR,aAAa;;;;;;;;EAQb;;;;;;EAMA;;;;;;EAMA,UAAU;;;;;;;YAQA;;EAEV;;EAEA;;EAEA;;;;;;;EAOA;;;;;;;;YASU;;EAEV;;EAEA;;EAEA;;EAEA,MAAM;;EAEN;;EAEA;;EAEA;;EAEA,OAAO;;;;;;EAMP"}
package/esm/index.d.mts CHANGED
@@ -8,6 +8,7 @@ import { reportToTrace } from "./collector/report-to-trace.mjs";
8
8
  import { reportToSpan } from "./collector/report-to-span.mjs";
9
9
  import { extractSpanAttributes } from "./collector/extract-span-attributes.mjs";
10
10
  import { normalizeError } from "./collector/normalize-error.mjs";
11
+ import { TraceListener, subscribeTraces } from "./collector/trace-subscriptions.mjs";
11
12
  import "./collector/index.mjs";
12
13
  import { TraceAggregate } from "./store/trace-aggregate.type.mjs";
13
14
  import { TraceQuery } from "./store/trace-query.type.mjs";
@@ -48,5 +49,5 @@ import "./dashboard/index.mjs";
48
49
  import { PanopticConfig } from "./config/panoptic-config.type.mjs";
49
50
  import { applyPanopticConfig } from "./config/apply-panoptic-config.mjs";
50
51
  import "./config/index.mjs";
51
- export { type AttributeValue, type CacheDriverInput, type CacheTraceStoreHandle, type CacheTraceStoreOptions, type CollectorContract, type CollectorOptions, type CompletedEventPayload, type ConsoleExporterOptions, type ConsoleLike, type ContentCaptureOptions, type ContentRedactor, type DashboardHandle, type DashboardOptions, type EvaluateConfig, type EvaluateVerdict, type ExporterContract, type ExporterErrorHandler, type FileExporterOptions, GEN_AI_ATTRIBUTES, type InMemoryTraceStoreOptions, type LangfuseClientLike, type LangfuseExporterOptions, type LangfuseObservationBody, type LangfuseObservationEndBody, type LangfuseObservationLevel, type LangfuseObservationLike, type LangfuseTraceBody, type LangfuseTraceLike, type LangfuseUsageBody, NO_PROMPT_KEY, NO_SESSION_KEY, NO_TYPE_KEY, type OtelExporterOptions, type Panoptic, type PanopticConfig, type PanopticOptions, type PanopticTarget, type PromptGroup, type SessionGroup, type Trace, type TraceAggregate, type TraceFilter, type TraceQuery, type TraceRecord, type TraceSpan, type TraceSpanError, type TraceStoreContract, type TypeGroup, type TypeStat, WARLOCK_ATTRIBUTES, aggregateByType, applyPanopticConfig, consoleExporter, createCacheTraceStore, createCollector, createInMemoryTraceStore, createPanopticMiddleware, dashboard, emptyUsage, evaluateSystemPrompt, extractLastSystemPrompt, extractSpanAttributes, fileExporter, filterTraces, findSpanById, formatSpanIO, formatSpanLine, groupByPrompt, groupBySession, groupByType, heatIntensity, langfuseExporter, matchTrace, matchesFilter, maxNodeCost, normalizeError, otelExporter, panoptic, percentile, reportToSpan, reportToTrace, rollupCost, sumUsage, toGenAiAttributes, totalCostUsd, traceCost, tracePromptKey, walkSpans };
52
+ export { type AttributeValue, type CacheDriverInput, type CacheTraceStoreHandle, type CacheTraceStoreOptions, type CollectorContract, type CollectorOptions, type CompletedEventPayload, type ConsoleExporterOptions, type ConsoleLike, type ContentCaptureOptions, type ContentRedactor, type DashboardHandle, type DashboardOptions, type EvaluateConfig, type EvaluateVerdict, type ExporterContract, type ExporterErrorHandler, type FileExporterOptions, GEN_AI_ATTRIBUTES, type InMemoryTraceStoreOptions, type LangfuseClientLike, type LangfuseExporterOptions, type LangfuseObservationBody, type LangfuseObservationEndBody, type LangfuseObservationLevel, type LangfuseObservationLike, type LangfuseTraceBody, type LangfuseTraceLike, type LangfuseUsageBody, NO_PROMPT_KEY, NO_SESSION_KEY, NO_TYPE_KEY, type OtelExporterOptions, type Panoptic, type PanopticConfig, type PanopticOptions, type PanopticTarget, type PromptGroup, type SessionGroup, type Trace, type TraceAggregate, type TraceFilter, type TraceListener, type TraceQuery, type TraceRecord, type TraceSpan, type TraceSpanError, type TraceStoreContract, type TypeGroup, type TypeStat, WARLOCK_ATTRIBUTES, aggregateByType, applyPanopticConfig, consoleExporter, createCacheTraceStore, createCollector, createInMemoryTraceStore, createPanopticMiddleware, dashboard, emptyUsage, evaluateSystemPrompt, extractLastSystemPrompt, extractSpanAttributes, fileExporter, filterTraces, findSpanById, formatSpanIO, formatSpanLine, groupByPrompt, groupBySession, groupByType, heatIntensity, langfuseExporter, matchTrace, matchesFilter, maxNodeCost, normalizeError, otelExporter, panoptic, percentile, reportToSpan, reportToTrace, rollupCost, subscribeTraces, sumUsage, toGenAiAttributes, totalCostUsd, traceCost, tracePromptKey, walkSpans };
52
53
  import "./config/panoptic-config.type.mjs";
package/esm/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { normalizeError } from "./collector/normalize-error.mjs";
3
3
  import { extractSpanAttributes } from "./collector/extract-span-attributes.mjs";
4
4
  import { reportToSpan } from "./collector/report-to-span.mjs";
5
5
  import { reportToTrace } from "./collector/report-to-trace.mjs";
6
+ import { subscribeTraces } from "./collector/trace-subscriptions.mjs";
6
7
  import { createCollector } from "./collector/collector.mjs";
7
8
  import "./collector/index.mjs";
8
9
  import { matchTrace } from "./store/match-trace.mjs";
@@ -33,4 +34,4 @@ import { applyPanopticConfig } from "./config/apply-panoptic-config.mjs";
33
34
  import "./config/index.mjs";
34
35
  import "./register.mjs";
35
36
 
36
- export { GEN_AI_ATTRIBUTES, NO_PROMPT_KEY, NO_SESSION_KEY, NO_TYPE_KEY, WARLOCK_ATTRIBUTES, aggregateByType, applyPanopticConfig, consoleExporter, createCacheTraceStore, createCollector, createInMemoryTraceStore, createPanopticMiddleware, dashboard, emptyUsage, evaluateSystemPrompt, extractLastSystemPrompt, extractSpanAttributes, fileExporter, filterTraces, findSpanById, formatSpanIO, formatSpanLine, groupByPrompt, groupBySession, groupByType, heatIntensity, langfuseExporter, matchTrace, matchesFilter, maxNodeCost, normalizeError, otelExporter, panoptic, percentile, reportToSpan, reportToTrace, rollupCost, sumUsage, toGenAiAttributes, totalCostUsd, traceCost, tracePromptKey, walkSpans };
37
+ export { GEN_AI_ATTRIBUTES, NO_PROMPT_KEY, NO_SESSION_KEY, NO_TYPE_KEY, WARLOCK_ATTRIBUTES, aggregateByType, applyPanopticConfig, consoleExporter, createCacheTraceStore, createCollector, createInMemoryTraceStore, createPanopticMiddleware, dashboard, emptyUsage, evaluateSystemPrompt, extractLastSystemPrompt, extractSpanAttributes, fileExporter, filterTraces, findSpanById, formatSpanIO, formatSpanLine, groupByPrompt, groupBySession, groupByType, heatIntensity, langfuseExporter, matchTrace, matchesFilter, maxNodeCost, normalizeError, otelExporter, panoptic, percentile, reportToSpan, reportToTrace, rollupCost, subscribeTraces, sumUsage, toGenAiAttributes, totalCostUsd, traceCost, tracePromptKey, walkSpans };
@@ -14,6 +14,7 @@
14
14
  */
15
15
  function matchTrace(trace, filter) {
16
16
  if (!filter) return true;
17
+ if (filter.requestId !== void 0 && trace.requestId !== filter.requestId) return false;
17
18
  if (filter.traceId !== void 0 && trace.traceId !== filter.traceId) return false;
18
19
  if (filter.sessionId !== void 0 && trace.sessionId !== filter.sessionId) return false;
19
20
  if (filter.status !== void 0 && !statusMatches(trace.root.status, filter.status)) return false;
@@ -1 +1 @@
1
- {"version":3,"file":"match-trace.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/match-trace.ts"],"sourcesContent":["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"],"mappings":";;;;;;;;;;;;;;AAiBA,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"}
1
+ {"version":3,"file":"match-trace.mjs","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/match-trace.ts"],"sourcesContent":["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.requestId !== undefined && trace.requestId !== filter.requestId) {\n return false;\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"],"mappings":";;;;;;;;;;;;;;AAiBA,SAAgB,WAAW,OAAc,QAA8B;CACrE,IAAI,CAAC,QACH,OAAO;CAGT,IAAI,OAAO,cAAc,UAAa,MAAM,cAAc,OAAO,WAC/D,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"}
@@ -21,6 +21,8 @@ import { ReportStatus } from "@warlock.js/ai";
21
21
  * const sessionSpend = store.aggregate({ sessionId: "session-42" });
22
22
  */
23
23
  export type TraceQuery = {
24
+ /** Exact HTTP request correlation id; absent on background traces. */
25
+ requestId?: string;
24
26
  /**
25
27
  * Match a single trace by its `traceId` (equals the root run's
26
28
  * `runId`). The most specific filter — narrows to at most one trace.
@@ -1 +1 @@
1
- {"version":3,"file":"trace-query.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/trace-query.type.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;YAsBY;;;;;EAKV;;;;;EAKA;;;;;EAKA,SAAS,eAAe;;EAExB,wBAAwB;;EAExB,yBAAyB"}
1
+ {"version":3,"file":"trace-query.type.d.mts","names":[],"sources":["../../../../../../../../packages/ai-panoptic/src/store/trace-query.type.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;YAsBY;;EAEV;;;;;EAKA;;;;;EAKA;;;;;EAKA,SAAS,eAAe;;EAExB,wBAAwB;;EAExB,yBAAyB"}
package/llms-full.txt CHANGED
@@ -472,6 +472,41 @@ Add exporters after construction with `observe.use(exporter)` (chainable, dedupe
472
472
  - The `query-traces` topic — retaining traces in the queryable in-memory store and slicing them (`query` / `aggregate` / `capacity`).
473
473
  - The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent and the `BaseReport` shape Panoptic consumes.
474
474
 
475
+ ## Subscribe across collectors and inspect requests in devtools
476
+
477
+ `subscribeTraces(listener)` is a process-local adapter hook: it receives every
478
+ finalized trace from built-in collectors (including ones created earlier), in
479
+ the collecting async context, before exporter dispatch. It has no replay and
480
+ adds no retention. It does not enable observation or subscribe to pure
481
+ `toTrace()` calls. Treat incoming traces as read-only. Listener throws/rejections
482
+ are isolated; promises are not awaited.
483
+
484
+ ```ts
485
+ import { subscribeTraces } from "@warlock.js/ai-panoptic";
486
+ import type { TraceListener } from "@warlock.js/ai-panoptic";
487
+
488
+ const onTrace: TraceListener = (trace) => {
489
+ console.log(trace.traceId, trace.root.status, trace.usage);
490
+ };
491
+ const unsubscribe = subscribeTraces(onTrace);
492
+ // ...later, on adapter teardown:
493
+ unsubscribe();
494
+ ```
495
+
496
+ With `@warlock.js/devtools` installed, its **AI** tab at `/__warlock` subscribes
497
+ for you. Enable `ai.config({ panoptic: { observeAll: true } })` as above, or use
498
+ an explicit subscriber. No `dashboard: true` is needed. Request-local context
499
+ links runs to their HTTP request when available, independently of `sessionId`.
500
+ The panel never mounts in production.
501
+
502
+ New diagnostic attributes are `retry.attempts` (timings/outcomes),
503
+ `agent.cacheHits` (trips reporting positive provider cache reads),
504
+ `agent.truncatedTrips`, `agent.providerCompactions` and `agent.compactions`
505
+ (applied/skipped decisions, trip indices, token counts and replaced ranges).
506
+ They exclude compaction summaries and callback error text. Unknown cache hits
507
+ remain absent; do not infer semantic-cache hits from zero tokens. An incomplete
508
+ run is identified by its status or normalized `AGENT_INCOMPLETE` error code.
509
+
475
510
 
476
511
  ## query-traces `@warlock.js/ai-panoptic/query-traces/SKILL.md`
477
512
 
@@ -532,6 +567,7 @@ Every field is optional and **ANDed** together — an absent field is "don't car
532
567
  ```ts
533
568
  type TraceQuery = {
534
569
  traceId?: string; // exact one run
570
+ requestId?: string; // exact HTTP request id, when an integration supplied it
535
571
  sessionId?: string; // every run for a session
536
572
  status?: ReportStatus | ReportStatus[]; // single status or membership set
537
573
  startedAfter?: string | Date; // inclusive lower bound on root startedAt
@@ -691,6 +727,40 @@ console.log(stats.cost?.serverTools?.webSearch); // historical USD
691
727
  console.log(totalCost(stats.cost)); // includes hosted fees
692
728
  ```
693
729
 
730
+ ## Correlate traces with HTTP requests
731
+
732
+ `Trace.requestId` is an optional integration-supplied identity. It is independent
733
+ of `sessionId`; legacy and background traces omit it. In-memory and cache stores
734
+ use the same exact-match `requestId` filter in `query()` and `aggregate()`, and
735
+ cache hydration preserves it.
736
+
737
+ ```ts
738
+ import { AsyncLocalStorage } from "node:async_hooks";
739
+ import { createInMemoryTraceStore, subscribeTraces } from "@warlock.js/ai-panoptic";
740
+
741
+ // Your HTTP adapter runs the flow inside this context.
742
+ const httpContext = new AsyncLocalStorage<{ requestId: string }>();
743
+ const requests = createInMemoryTraceStore({ capacity: 200 });
744
+ const unsubscribe = subscribeTraces((trace) => {
745
+ requests.add({ ...trace, requestId: httpContext.getStore()?.requestId });
746
+ });
747
+
748
+ const runs = requests.query({ requestId: "request-42" });
749
+ const usage = requests.aggregate({ requestId: "request-42" });
750
+ unsubscribe();
751
+ ```
752
+
753
+ The callback reads request context synchronously, before exporters run. It sees
754
+ future collection from all built-in collectors, with no replay. Treat incoming
755
+ traces as read-only and add an enriched copy. Errors are isolated, async
756
+ callbacks are not awaited, and `toTrace()` does not notify subscribers.
757
+
758
+ For app development, `@warlock.js/devtools` already does this: open the **AI** tab
759
+ at `/__warlock`, or an HTTP request's **AI runs** section. Its bounded,
760
+ content-free store is separate from your exporter/cache stores. Clearing the
761
+ panel does not clear application trace history. Enable flow observation first;
762
+ installing the viewer alone does not collect runs.
763
+
694
764
 
695
765
  ## use-local-dashboard `@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`
696
766
 
@@ -923,4 +993,25 @@ These three findings (link sanitization + the two token-handling items above) ca
923
993
  - The `export-traces` topic — shipping traces to OTel / Langfuse / a file instead of (or alongside) the local dashboard.
924
994
  - The `evaluate-system-prompt` topic — the drawer's "Evaluate system prompt" action, the dashboard's one write route.
925
995
 
996
+ ## Use the app's devtools AI panel
997
+
998
+ When developing a Warlock HTTP app with `@warlock.js/devtools` installed, open
999
+ `/__warlock` → **AI** to inspect runs alongside their HTTP requests. This uses
1000
+ the existing app server and needs no `dashboard: true`:
1001
+
1002
+ ```ts
1003
+ import { ai } from "@warlock.js/ai";
1004
+ import "@warlock.js/ai-panoptic";
1005
+ ai.config({ panoptic: { observeAll: true } });
1006
+ ```
1007
+
1008
+ Restart `warlock dev` after installing the optional Panoptic package. Runs
1009
+ appear after collection, with model, tokens, historical cost, provider cache
1010
+ hits, tool timings, retries, incomplete/truncated status and compaction metadata.
1011
+ The panel uses a bounded content-free in-memory view (200 traces by default),
1012
+ not the standalone dashboard's persisted cache store. Clear/restart discards
1013
+ only that view. HTTP request identity is captured from the collecting async
1014
+ context; missing identities remain background runs. Devtools never mounts in
1015
+ production and retains its socket-loopback guard.
1016
+
926
1017
 
package/package.json CHANGED
@@ -19,9 +19,9 @@
19
19
  "peerDependencies": {
20
20
  "@opentelemetry/api": "*",
21
21
  "@opentelemetry/sdk-trace-base": "*",
22
- "@warlock.js/ai": "5.32.0",
23
- "@warlock.js/cache": "5.32.0",
24
- "@warlock.js/logger": "5.32.0",
22
+ "@warlock.js/ai": "5.33.0",
23
+ "@warlock.js/cache": "5.33.0",
24
+ "@warlock.js/logger": "5.33.0",
25
25
  "langfuse": "*"
26
26
  },
27
27
  "peerDependenciesMeta": {
@@ -41,7 +41,7 @@
41
41
  "dependencies": {
42
42
  "langfuse": "^3.38.20"
43
43
  },
44
- "version": "5.32.0",
44
+ "version": "5.33.0",
45
45
  "main": "./cjs/index.cjs",
46
46
  "module": "./esm/index.mjs",
47
47
  "types": "./esm/index.d.mts",
@@ -173,3 +173,38 @@ Add exporters after construction with `observe.use(exporter)` (chainable, dedupe
173
173
  - The `export-traces` topic — the sink end: choosing/writing an exporter, the `gen_ai.*` / `warlock.*` attribute mapping.
174
174
  - The `query-traces` topic — retaining traces in the queryable in-memory store and slicing them (`query` / `aggregate` / `capacity`).
175
175
  - The `run-ai-agent` topic of the `warlock-js-ai` skill — running the agent and the `BaseReport` shape Panoptic consumes.
176
+
177
+ ## Subscribe across collectors and inspect requests in devtools
178
+
179
+ `subscribeTraces(listener)` is a process-local adapter hook: it receives every
180
+ finalized trace from built-in collectors (including ones created earlier), in
181
+ the collecting async context, before exporter dispatch. It has no replay and
182
+ adds no retention. It does not enable observation or subscribe to pure
183
+ `toTrace()` calls. Treat incoming traces as read-only. Listener throws/rejections
184
+ are isolated; promises are not awaited.
185
+
186
+ ```ts
187
+ import { subscribeTraces } from "@warlock.js/ai-panoptic";
188
+ import type { TraceListener } from "@warlock.js/ai-panoptic";
189
+
190
+ const onTrace: TraceListener = (trace) => {
191
+ console.log(trace.traceId, trace.root.status, trace.usage);
192
+ };
193
+ const unsubscribe = subscribeTraces(onTrace);
194
+ // ...later, on adapter teardown:
195
+ unsubscribe();
196
+ ```
197
+
198
+ With `@warlock.js/devtools` installed, its **AI** tab at `/__warlock` subscribes
199
+ for you. Enable `ai.config({ panoptic: { observeAll: true } })` as above, or use
200
+ an explicit subscriber. No `dashboard: true` is needed. Request-local context
201
+ links runs to their HTTP request when available, independently of `sessionId`.
202
+ The panel never mounts in production.
203
+
204
+ New diagnostic attributes are `retry.attempts` (timings/outcomes),
205
+ `agent.cacheHits` (trips reporting positive provider cache reads),
206
+ `agent.truncatedTrips`, `agent.providerCompactions` and `agent.compactions`
207
+ (applied/skipped decisions, trip indices, token counts and replaced ranges).
208
+ They exclude compaction summaries and callback error text. Unknown cache hits
209
+ remain absent; do not infer semantic-cache hits from zero tokens. An incomplete
210
+ run is identified by its status or normalized `AGENT_INCOMPLETE` error code.
@@ -55,6 +55,7 @@ Every field is optional and **ANDed** together — an absent field is "don't car
55
55
  ```ts
56
56
  type TraceQuery = {
57
57
  traceId?: string; // exact one run
58
+ requestId?: string; // exact HTTP request id, when an integration supplied it
58
59
  sessionId?: string; // every run for a session
59
60
  status?: ReportStatus | ReportStatus[]; // single status or membership set
60
61
  startedAfter?: string | Date; // inclusive lower bound on root startedAt
@@ -213,3 +214,37 @@ console.log(stats.usage.serverToolUse?.webSearchRequests);
213
214
  console.log(stats.cost?.serverTools?.webSearch); // historical USD
214
215
  console.log(totalCost(stats.cost)); // includes hosted fees
215
216
  ```
217
+
218
+ ## Correlate traces with HTTP requests
219
+
220
+ `Trace.requestId` is an optional integration-supplied identity. It is independent
221
+ of `sessionId`; legacy and background traces omit it. In-memory and cache stores
222
+ use the same exact-match `requestId` filter in `query()` and `aggregate()`, and
223
+ cache hydration preserves it.
224
+
225
+ ```ts
226
+ import { AsyncLocalStorage } from "node:async_hooks";
227
+ import { createInMemoryTraceStore, subscribeTraces } from "@warlock.js/ai-panoptic";
228
+
229
+ // Your HTTP adapter runs the flow inside this context.
230
+ const httpContext = new AsyncLocalStorage<{ requestId: string }>();
231
+ const requests = createInMemoryTraceStore({ capacity: 200 });
232
+ const unsubscribe = subscribeTraces((trace) => {
233
+ requests.add({ ...trace, requestId: httpContext.getStore()?.requestId });
234
+ });
235
+
236
+ const runs = requests.query({ requestId: "request-42" });
237
+ const usage = requests.aggregate({ requestId: "request-42" });
238
+ unsubscribe();
239
+ ```
240
+
241
+ The callback reads request context synchronously, before exporters run. It sees
242
+ future collection from all built-in collectors, with no replay. Treat incoming
243
+ traces as read-only and add an enriched copy. Errors are isolated, async
244
+ callbacks are not awaited, and `toTrace()` does not notify subscribers.
245
+
246
+ For app development, `@warlock.js/devtools` already does this: open the **AI** tab
247
+ at `/__warlock`, or an HTTP request's **AI runs** section. Its bounded,
248
+ content-free store is separate from your exporter/cache stores. Clearing the
249
+ panel does not clear application trace history. Enable flow observation first;
250
+ installing the viewer alone does not collect runs.
@@ -226,3 +226,24 @@ These three findings (link sanitization + the two token-handling items above) ca
226
226
  - The `query-traces` topic — the `TraceStoreContract` (`query` / `get` / `aggregate`) the dashboard reads, the `TraceQuery` filter, and capacity eviction.
227
227
  - The `export-traces` topic — shipping traces to OTel / Langfuse / a file instead of (or alongside) the local dashboard.
228
228
  - The `evaluate-system-prompt` topic — the drawer's "Evaluate system prompt" action, the dashboard's one write route.
229
+
230
+ ## Use the app's devtools AI panel
231
+
232
+ When developing a Warlock HTTP app with `@warlock.js/devtools` installed, open
233
+ `/__warlock` → **AI** to inspect runs alongside their HTTP requests. This uses
234
+ the existing app server and needs no `dashboard: true`:
235
+
236
+ ```ts
237
+ import { ai } from "@warlock.js/ai";
238
+ import "@warlock.js/ai-panoptic";
239
+ ai.config({ panoptic: { observeAll: true } });
240
+ ```
241
+
242
+ Restart `warlock dev` after installing the optional Panoptic package. Runs
243
+ appear after collection, with model, tokens, historical cost, provider cache
244
+ hits, tool timings, retries, incomplete/truncated status and compaction metadata.
245
+ The panel uses a bounded content-free in-memory view (200 traces by default),
246
+ not the standalone dashboard's persisted cache store. Clear/restart discards
247
+ only that view. HTTP request identity is captured from the collecting async
248
+ context; missing identities remain background runs. Devtools never mounts in
249
+ production and retains its socket-loopback guard.