@warlock.js/ai-panoptic 4.4.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +39 -31
  2. package/cjs/index.cjs +2393 -31
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/collector/collector.d.mts +19 -2
  5. package/esm/collector/collector.d.mts.map +1 -1
  6. package/esm/collector/collector.mjs +40 -6
  7. package/esm/collector/collector.mjs.map +1 -1
  8. package/esm/collector/content-capture.type.d.mts +10 -0
  9. package/esm/collector/content-capture.type.d.mts.map +1 -1
  10. package/esm/collector/extract-span-attributes.d.mts.map +1 -1
  11. package/esm/collector/extract-span-attributes.mjs +3 -0
  12. package/esm/collector/extract-span-attributes.mjs.map +1 -1
  13. package/esm/collector/index.d.mts +1 -1
  14. package/esm/collector/index.mjs +7 -0
  15. package/esm/collector/normalize-error.d.mts.map +1 -1
  16. package/esm/collector/normalize-error.mjs +6 -3
  17. package/esm/collector/normalize-error.mjs.map +1 -1
  18. package/esm/collector/report-to-span.mjs +31 -10
  19. package/esm/collector/report-to-span.mjs.map +1 -1
  20. package/esm/collector/report-to-trace.d.mts +8 -5
  21. package/esm/collector/report-to-trace.d.mts.map +1 -1
  22. package/esm/collector/report-to-trace.mjs +8 -5
  23. package/esm/collector/report-to-trace.mjs.map +1 -1
  24. package/esm/config/apply-panoptic-config.d.mts +27 -0
  25. package/esm/config/apply-panoptic-config.d.mts.map +1 -0
  26. package/esm/config/apply-panoptic-config.mjs +89 -0
  27. package/esm/config/apply-panoptic-config.mjs.map +1 -0
  28. package/esm/config/index.d.mts +2 -0
  29. package/esm/config/index.mjs +3 -0
  30. package/esm/config/panoptic-config.type.d.mts +84 -0
  31. package/esm/config/panoptic-config.type.d.mts.map +1 -0
  32. package/esm/contracts/trace.type.d.mts +9 -2
  33. package/esm/contracts/trace.type.d.mts.map +1 -1
  34. package/esm/dashboard/dashboard.d.mts +32 -0
  35. package/esm/dashboard/dashboard.d.mts.map +1 -0
  36. package/esm/dashboard/dashboard.mjs +132 -0
  37. package/esm/dashboard/dashboard.mjs.map +1 -0
  38. package/esm/dashboard/dashboard.type.d.mts +63 -0
  39. package/esm/dashboard/dashboard.type.d.mts.map +1 -0
  40. package/esm/dashboard/index.d.mts +3 -0
  41. package/esm/dashboard/parse-query.d.mts +1 -0
  42. package/esm/dashboard/parse-query.mjs +54 -0
  43. package/esm/dashboard/parse-query.mjs.map +1 -0
  44. package/esm/dashboard/serve.d.mts +1 -0
  45. package/esm/dashboard/serve.mjs +111 -0
  46. package/esm/dashboard/serve.mjs.map +1 -0
  47. package/esm/dashboard/trace-filter.d.mts +187 -0
  48. package/esm/dashboard/trace-filter.d.mts.map +1 -0
  49. package/esm/dashboard/trace-filter.mjs +270 -0
  50. package/esm/dashboard/trace-filter.mjs.map +1 -0
  51. package/esm/dashboard/ui.html.mjs +1330 -0
  52. package/esm/dashboard/ui.html.mjs.map +1 -0
  53. package/esm/dashboard/warlock-logo.mjs +13 -0
  54. package/esm/dashboard/warlock-logo.mjs.map +1 -0
  55. package/esm/exporters/file/file-exporter.mjs +5 -2
  56. package/esm/exporters/file/file-exporter.mjs.map +1 -1
  57. package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -1
  58. package/esm/exporters/langfuse/langfuse-exporter.mjs +5 -2
  59. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
  60. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +3 -1
  61. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -1
  62. package/esm/exporters/otel/otel-api.shim.type.d.mts +60 -0
  63. package/esm/exporters/otel/otel-api.shim.type.d.mts.map +1 -0
  64. package/esm/exporters/otel/otel-exporter.d.mts.map +1 -1
  65. package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
  66. package/esm/exporters/otel/otel-exporter.type.d.mts +7 -3
  67. package/esm/exporters/otel/otel-exporter.type.d.mts.map +1 -1
  68. package/esm/index.d.mts +9 -2
  69. package/esm/index.mjs +9 -1
  70. package/esm/panoptic/index.mjs +4 -0
  71. package/esm/panoptic/panoptic.d.mts.map +1 -1
  72. package/esm/panoptic/panoptic.mjs +6 -1
  73. package/esm/panoptic/panoptic.mjs.map +1 -1
  74. package/esm/panoptic/panoptic.type.d.mts +15 -0
  75. package/esm/panoptic/panoptic.type.d.mts.map +1 -1
  76. package/esm/register.mjs +12 -0
  77. package/esm/register.mjs.map +1 -0
  78. package/esm/store/cache-trace-store.d.mts +81 -0
  79. package/esm/store/cache-trace-store.d.mts.map +1 -0
  80. package/esm/store/cache-trace-store.mjs +290 -0
  81. package/esm/store/cache-trace-store.mjs.map +1 -0
  82. package/esm/store/in-memory-trace-store.mjs +9 -2
  83. package/esm/store/in-memory-trace-store.mjs.map +1 -1
  84. package/esm/store/index.d.mts +1 -0
  85. package/esm/store/index.mjs +1 -0
  86. package/llms-full.txt +351 -4
  87. package/llms.txt +4 -3
  88. package/package.json +6 -2
  89. package/skills/README.md +7 -3
  90. package/skills/export-traces/SKILL.md +33 -1
  91. package/skills/observe-with-panoptic/SKILL.md +49 -2
  92. package/skills/query-traces/SKILL.md +52 -1
  93. package/skills/use-local-dashboard/SKILL.md +213 -0
@@ -1 +1 @@
1
- {"version":3,"file":"in-memory-trace-store.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/store/in-memory-trace-store.ts"],"sourcesContent":["import type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { matchTrace } from \"./match-trace\";\nimport { emptyUsage, sumUsage } from \"./sum-usage\";\nimport type { TraceAggregate } from \"./trace-aggregate.type\";\nimport type { TraceQuery } from \"./trace-query.type\";\nimport type { TraceStoreContract } from \"./trace-store.contract\";\n\n/**\n * Options for {@link createInMemoryTraceStore}.\n */\nexport type InMemoryTraceStoreOptions = {\n /**\n * Maximum number of traces to retain. When set and exceeded, the\n * oldest-ingested trace is evicted (insertion-order FIFO) so the\n * store stays bounded for long-lived processes. Absent / `0` =\n * unbounded (keep everything until `clear`).\n */\n capacity?: number;\n};\n\n/**\n * In-memory {@link TraceStoreContract} that doubles as an\n * {@link ExporterContract} — register it on a collector\n * (`collector.use(store)`) and it fills as traces complete, then query\n * or aggregate it after the fact.\n *\n * Backed by an insertion-ordered `Map` keyed by `traceId`, giving O(1)\n * `get` / `add` / overwrite and O(n) scans for `query` / `aggregate`\n * (the price of an in-memory store with no secondary indexes — fine for\n * the dev/test and modest-volume runtime use this targets). When a\n * `capacity` is configured, ingesting past the cap evicts the oldest\n * trace.\n *\n * Instantiated fresh per store via {@link createInMemoryTraceStore};\n * callers never see `new`.\n */\nclass InMemoryTraceStore implements TraceStoreContract, ExporterContract {\n /** Stable exporter id so a collector can dedupe / log this sink. */\n public readonly name = \"in-memory-trace-store\";\n\n /**\n * Retained traces keyed by `traceId`. A `Map` preserves insertion\n * order, which is what FIFO eviction and newest-first `query` ordering\n * both rely on.\n */\n private readonly traces = new Map<string, Trace>();\n\n private readonly capacity: number;\n\n public constructor(options?: InMemoryTraceStoreOptions) {\n this.capacity = options?.capacity ?? 0;\n }\n\n public get size(): number {\n return this.traces.size;\n }\n\n public add(trace: Trace): void {\n // Re-insert so an overwrite also refreshes insertion position —\n // keeps \"oldest\" honest for FIFO eviction.\n this.traces.delete(trace.traceId);\n this.traces.set(trace.traceId, trace);\n\n this.evictOverflow();\n }\n\n /**\n * `ExporterContract.export` — a collector dispatches a completed\n * trace here, which is exactly an `add`. Lets the store be wired into\n * a collector as a sink without an adapter.\n */\n public export(trace: Trace): void {\n this.add(trace);\n }\n\n public get(traceId: string): Trace | undefined {\n return this.traces.get(traceId);\n }\n\n public query(filter?: TraceQuery): Trace[] {\n const matched: Trace[] = [];\n\n for (const trace of this.traces.values()) {\n if (matchTrace(trace, filter)) {\n matched.push(trace);\n }\n }\n\n return this.sortNewestFirst(matched);\n }\n\n public aggregate(filter?: TraceQuery): TraceAggregate {\n const aggregate: TraceAggregate = {\n traces: 0,\n completed: 0,\n failed: 0,\n cancelled: 0,\n usage: emptyUsage(),\n totalDuration: 0,\n };\n\n for (const trace of this.traces.values()) {\n if (!matchTrace(trace, filter)) {\n continue;\n }\n\n aggregate.traces += 1;\n aggregate.totalDuration += trace.duration;\n aggregate.usage = sumUsage(aggregate.usage, trace.usage);\n\n this.countStatus(aggregate, trace);\n }\n\n if (aggregate.usage.cost !== undefined) {\n aggregate.cost = aggregate.usage.cost;\n }\n\n return aggregate;\n }\n\n public clear(): void {\n this.traces.clear();\n }\n\n /**\n * Increment the matching terminal-status counter for one trace.\n * Non-terminal statuses (`awaiting-input`, `max-iterations`) are\n * counted in `traces` but tracked by none of the three headline\n * counters — intentional, those three answer the common\n * \"succeeded / errored / aborted\" question.\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqCA,IAAM,qBAAN,MAAyE;CAavE,AAAO,YAAY,SAAqC;cAXjC;gCAOG,IAAI,IAAmB;EAK/C,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,CAAC,CAAC,KAAK,CAAC,CAAC;GAEzC,IAAI,WAAW,QACb;GAGF,KAAK,OAAO,OAAO,MAAM;EAC3B;CACF;AACF;;;;;;;;;;;;;AAcA,SAAgB,yBAAyB,SAA4E;CACnH,OAAO,IAAI,mBAAmB,OAAO;AACvC"}
1
+ {"version":3,"file":"in-memory-trace-store.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/store/in-memory-trace-store.ts"],"sourcesContent":["import type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { matchTrace } from \"./match-trace\";\nimport { emptyUsage, sumUsage } from \"./sum-usage\";\nimport type { TraceAggregate } from \"./trace-aggregate.type\";\nimport type { TraceQuery } from \"./trace-query.type\";\nimport type { TraceStoreContract } from \"./trace-store.contract\";\n\n/**\n * Options for {@link createInMemoryTraceStore}.\n */\nexport type InMemoryTraceStoreOptions = {\n /**\n * Maximum number of traces to retain. When set and exceeded, the\n * oldest-ingested trace is evicted (insertion-order FIFO) so the\n * store stays bounded for long-lived processes. Absent / `0` =\n * unbounded (keep everything until `clear`).\n */\n capacity?: number;\n};\n\n/**\n * In-memory {@link TraceStoreContract} that doubles as an\n * {@link ExporterContract} — register it on a collector\n * (`collector.use(store)`) and it fills as traces complete, then query\n * or aggregate it after the fact.\n *\n * Backed by an insertion-ordered `Map` keyed by `traceId`, giving O(1)\n * `get` / `add` / overwrite and O(n) scans for `query` / `aggregate`\n * (the price of an in-memory store with no secondary indexes — fine for\n * the dev/test and modest-volume runtime use this targets). When a\n * `capacity` is configured, ingesting past the cap evicts the oldest\n * trace.\n *\n * Instantiated fresh per store via {@link createInMemoryTraceStore};\n * callers never see `new`.\n */\nclass InMemoryTraceStore implements TraceStoreContract, ExporterContract {\n /** Stable exporter id so a collector can dedupe / log this sink. */\n public readonly name = \"in-memory-trace-store\";\n\n /**\n * Retained traces keyed by `traceId`. A `Map` preserves insertion\n * order, which is what FIFO eviction and newest-first `query` ordering\n * both rely on.\n */\n private readonly traces = new Map<string, Trace>();\n\n private readonly capacity: number;\n\n public constructor(options?: InMemoryTraceStoreOptions) {\n this.capacity = options?.capacity ?? 0;\n }\n\n public get size(): number {\n return this.traces.size;\n }\n\n public add(trace: Trace): void {\n // Re-insert so an overwrite also refreshes insertion position —\n // keeps \"oldest\" honest for FIFO eviction.\n this.traces.delete(trace.traceId);\n this.traces.set(trace.traceId, trace);\n\n this.evictOverflow();\n }\n\n /**\n * `ExporterContract.export` — a collector dispatches a completed\n * trace here, which is exactly an `add`. Lets the store be wired into\n * a collector as a sink without an adapter.\n */\n public export(trace: Trace): void {\n this.add(trace);\n }\n\n public get(traceId: string): Trace | undefined {\n return this.traces.get(traceId);\n }\n\n public query(filter?: TraceQuery): Trace[] {\n const matched: Trace[] = [];\n\n for (const trace of this.traces.values()) {\n if (matchTrace(trace, filter)) {\n matched.push(trace);\n }\n }\n\n return this.sortNewestFirst(matched);\n }\n\n public aggregate(filter?: TraceQuery): TraceAggregate {\n const aggregate: TraceAggregate = {\n traces: 0,\n completed: 0,\n failed: 0,\n cancelled: 0,\n usage: emptyUsage(),\n totalDuration: 0,\n };\n\n for (const trace of this.traces.values()) {\n if (!matchTrace(trace, filter)) {\n continue;\n }\n\n aggregate.traces += 1;\n aggregate.totalDuration += trace.duration;\n aggregate.usage = sumUsage(aggregate.usage, trace.usage);\n\n this.countStatus(aggregate, trace);\n }\n\n if (aggregate.usage.cost !== undefined) {\n aggregate.cost = aggregate.usage.cost;\n }\n\n return aggregate;\n }\n\n public clear(): void {\n this.traces.clear();\n }\n\n /**\n * Increment the matching terminal-status counter for one trace.\n * Non-terminal statuses (`awaiting-input`, `max-iterations`) are\n * counted in `traces` but tracked by none of the three headline\n * counters — intentional, those three answer the common\n * \"succeeded / errored / aborted\" question.\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"],"mappings":";;;;;;;;;;;;;;;;;;;;AAqCA,IAAM,qBAAN,MAAyE;;CAEvE,AAAgB,OAAO;;;;;;CAOvB,AAAiB,yBAAS,IAAI,IAAmB;CAEjD,AAAiB;CAEjB,AAAO,YAAY,SAAqC;EACtD,KAAK,WAAW,SAAS,YAAY;CACvC;CAEA,IAAW,OAAe;EACxB,OAAO,KAAK,OAAO;CACrB;CAEA,AAAO,IAAI,OAAoB;EAG7B,KAAK,OAAO,OAAO,MAAM,OAAO;EAChC,KAAK,OAAO,IAAI,MAAM,SAAS,KAAK;EAEpC,KAAK,cAAc;CACrB;;;;;;CAOA,AAAO,OAAO,OAAoB;EAChC,KAAK,IAAI,KAAK;CAChB;CAEA,AAAO,IAAI,SAAoC;EAC7C,OAAO,KAAK,OAAO,IAAI,OAAO;CAChC;CAEA,AAAO,MAAM,QAA8B;EACzC,MAAM,UAAmB,CAAC;EAE1B,KAAK,MAAM,SAAS,KAAK,OAAO,OAAO,GACrC,IAAI,WAAW,OAAO,MAAM,GAC1B,QAAQ,KAAK,KAAK;EAItB,OAAO,KAAK,gBAAgB,OAAO;CACrC;CAEA,AAAO,UAAU,QAAqC;EACpD,MAAM,YAA4B;GAChC,QAAQ;GACR,WAAW;GACX,QAAQ;GACR,WAAW;GACX,OAAO,WAAW;GAClB,eAAe;EACjB;EAEA,KAAK,MAAM,SAAS,KAAK,OAAO,OAAO,GAAG;GACxC,IAAI,CAAC,WAAW,OAAO,MAAM,GAC3B;GAGF,UAAU,UAAU;GACpB,UAAU,iBAAiB,MAAM;GACjC,UAAU,QAAQ,SAAS,UAAU,OAAO,MAAM,KAAK;GAEvD,KAAK,YAAY,WAAW,KAAK;EACnC;EAEA,IAAI,UAAU,MAAM,SAAS,QAC3B,UAAU,OAAO,UAAU,MAAM;EAGnC,OAAO;CACT;CAEA,AAAO,QAAc;EACnB,KAAK,OAAO,MAAM;CACpB;;;;;;;;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,CAAC,CAAC,KAAK,CAAC,CAAC;GAEzC,IAAI,WAAW,QACb;GAGF,KAAK,OAAO,OAAO,MAAM;EAC3B;CACF;AACF;;;;;;;;;;;;;AAcA,SAAgB,yBAAyB,SAA4E;CACnH,OAAO,IAAI,mBAAmB,OAAO;AACvC"}
@@ -1,6 +1,7 @@
1
1
  import { TraceAggregate } from "./trace-aggregate.type.mjs";
2
2
  import { TraceQuery } from "./trace-query.type.mjs";
3
3
  import { TraceStoreContract } from "./trace-store.contract.mjs";
4
+ import { CacheDriverInput, CacheTraceStoreHandle, CacheTraceStoreOptions, createCacheTraceStore } from "./cache-trace-store.mjs";
4
5
  import { InMemoryTraceStoreOptions, createInMemoryTraceStore } from "./in-memory-trace-store.mjs";
5
6
  import { matchTrace } from "./match-trace.mjs";
6
7
  import { emptyUsage, sumUsage } from "./sum-usage.mjs";
@@ -1,5 +1,6 @@
1
1
  import { matchTrace } from "./match-trace.mjs";
2
2
  import { emptyUsage, sumUsage } from "./sum-usage.mjs";
3
+ import { createCacheTraceStore } from "./cache-trace-store.mjs";
3
4
  import { createInMemoryTraceStore } from "./in-memory-trace-store.mjs";
4
5
 
5
6
  export { };
package/llms-full.txt CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  ---
10
10
  name: export-traces
11
- description: 'Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `toGenAiAttributes`, `walkSpans`, `totalCostUsd`, `GEN_AI_ATTRIBUTES`, `WARLOCK_ATTRIBUTES`, `reportToTrace`, `reportToSpan`, `extractSpanAttributes`, `normalizeError`, `formatSpanLine`, `createPanopticMiddleware`; ''export AI traces to OpenTelemetry'', ''send traces to Langfuse'', ''log traces to console / a file'', ''gen_ai semantic conventions'', ''write a custom exporter''; typical import `import { otelExporter, langfuseExporter, consoleExporter, fileExporter } from "@warlock.js/ai-panoptic"`. Skip: wiring the subscriber/collector into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`; the core report shape — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
11
+ description: 'Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters, including capturing prompt/response content onto spans. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `ContentCaptureOptions`, `captureContent`, `redactContent`, `fullHistory`, `ContentRedactor`, `toGenAiAttributes`, `walkSpans`, `totalCostUsd`, `GEN_AI_ATTRIBUTES`, `WARLOCK_ATTRIBUTES`, `reportToTrace`, `reportToSpan`, `extractSpanAttributes`, `normalizeError`, `formatSpanLine`, `createPanopticMiddleware`; ''export AI traces to OpenTelemetry'', ''send traces to Langfuse'', ''log traces to console / a file'', ''capture the full prompt/conversation onto a span'', ''gen_ai semantic conventions'', ''write a custom exporter''; typical import `import { otelExporter, langfuseExporter, consoleExporter, fileExporter } from "@warlock.js/ai-panoptic"`. Skip: wiring the subscriber/collector into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`; the core report shape — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
12
12
  ---
13
13
 
14
14
  # Export traces — the sink end of Panoptic
@@ -102,6 +102,38 @@ collector.use(langfuseExporter({ client: myLangfuse }));
102
102
  - Children nest under their parent observation; timing, `version`, and status (failed → `ERROR` level) are mapped.
103
103
  - `flush()` / `shutdown()` delegate to the client's `flushAsync()` / `shutdownAsync()` — call `collector.shutdown()` on teardown so buffered observations are sent.
104
104
 
105
+ ## Content capture — what reaches the `input` / `output` fields
106
+
107
+ Exporters surface `span.input` / `span.output` (console `in:` / `out:`, file JSON, OTel `gen_ai.prompt` / `gen_ai.completion`, Langfuse native `input` / `output`) only when **content capture** is enabled on the collector or subscriber. It's `ContentCaptureOptions`, set on `createCollector(...)` or `panoptic(...)`:
108
+
109
+ ```ts
110
+ panoptic({
111
+ exporters: [langfuseExporter({ /* ... */ })],
112
+ captureContent: true, // copy prompts/responses + tool args/results onto spans
113
+ redactContent: (value, { field }) => // optional masker, per value
114
+ field === "input" ? "[redacted]" : value,
115
+ fullHistory: true, // emit the WHOLE conversation as span.input (see below)
116
+ });
117
+ ```
118
+
119
+ Off by default — payloads are large and frequently sensitive. With `captureContent`, an agent span's `input` is a `[system, user]` chat array (or the bare user string when there's no system prompt) and its `output` is the last non-empty trip's text; a tool span carries its call args + result.
120
+
121
+ ### `fullHistory` — emit the entire `CapturedMessage[]`
122
+
123
+ `fullHistory: true` replaces the `[system, user]` first-trip array on `span.input` with the agent's **complete assembled conversation** — every trip, every role (system / user / assistant / tool). The `output` stays the final response text.
124
+
125
+ ```ts
126
+ // upstream: the agent run MUST opt into capturing its messages
127
+ const result = await agent.execute(input, { captureMessages: true });
128
+ // ^ core @warlock.js/ai — makes report.messages present
129
+
130
+ panoptic({ exporters: [...], captureContent: true, fullHistory: true });
131
+ ```
132
+
133
+ **Requires the upstream `captureMessages`.** `fullHistory` reads `report.messages` (the captured `CapturedMessage[]`). When the run did NOT opt into `captureMessages`, `report.messages` is absent and the branch **degrades gracefully** to today's first-trip `[system, user]` capture — never an error, just less history. The whole array is passed through `ContentRedactor` as a single value, so masking still applies to captured history.
134
+
135
+ Reach for `fullHistory` when a backend (Langfuse) should show the full multi-turn / tool-loop transcript a run actually sent, not just its opening prompt.
136
+
105
137
  ## Shared utilities
106
138
 
107
139
  Reusable when authoring a **custom exporter**:
@@ -164,12 +196,58 @@ Reach for `reportToTrace` when you already hold a finished `result.report` and w
164
196
 
165
197
  ---
166
198
  name: observe-with-panoptic
167
- description: 'Wire @warlock.js/ai-panoptic into an agent/workflow/supervisor/orchestrator with the one-call `panoptic({ exporters })` subscriber. Triggers: `panoptic`, `Panoptic`, `PanopticOptions`, `.attach`, `.middleware`, `.collect`, `.toTrace`, `observe.attach(agent)`, `panoptic().middleware()`, `completedEvents`, `middlewareName`; ''observe a warlock agent'', ''trace an agent run'', ''attach observability to a workflow/supervisor'', ''add a tracing middleware'', ''collect an orchestrator turn report'', ''wire OTel/Langfuse into my agent''; typical import `import { panoptic } from "@warlock.js/ai-panoptic"`. Skip: writing/choosing an exporter (the sink end) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the core report shape / running the agent itself — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
199
+ description: 'Wire @warlock.js/ai-panoptic into an agent/workflow/supervisor/orchestrator — declaratively via `ai.config({ panoptic })` (registers panoptic on core''s Observer seam) or with the one-call `panoptic({ exporters })` subscriber. Triggers: `panoptic`, `Panoptic`, `PanopticOptions`, `PanopticConfig`, `ai.config({ panoptic })`, `observeAll`, per-flow `observe`, `FlowObserveOption`, `registerObserver`, `Observer`, `.attach`, `.middleware`, `.collect`, `.toTrace`, `observe.attach(agent)`, `panoptic().middleware()`, `completedEvents`, `middlewareName`; ''observe a warlock agent'', ''trace an agent run'', ''attach observability to a workflow/supervisor'', ''observe every flow by default'', ''add a tracing middleware'', ''collect an orchestrator turn report'', ''wire OTel/Langfuse into my agent''; typical import `import { ai } from "@warlock.js/ai"` + `import "@warlock.js/ai-panoptic"` (or `import { panoptic } from "@warlock.js/ai-panoptic"`). Skip: the zero-setup local dashboard — `@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`; writing/choosing an exporter (the sink end) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the core report shape / running the agent itself — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
168
200
  ---
169
201
 
170
202
  # Observe a run with Panoptic — the one-call subscriber
171
203
 
172
- `panoptic(...)` is the entry point. It builds a collector, registers your exporters on it, and hands back a subscriber with **three ways to feed traces** in. All three converge on the same collector, so each run reaches every exporter exactly once.
204
+ There are two levels. **Declarative** (`ai.config({ panoptic })`) is the high-level path: panoptic registers itself on core's generic `Observer` seam and every observed flow routes its report in automatically — no per-target wiring. **Imperative** (`panoptic(...)`) is the underlying subscriber, with three explicit feed paths. The declarative path is sugar over the imperative one; both reuse the same collection pipeline.
205
+
206
+ ## Declarative — `ai.config({ panoptic })` + the Observer seam
207
+
208
+ The simplest wiring. Importing `@warlock.js/ai-panoptic` (even bare, for side effects) subscribes panoptic to core's config seam; a later `ai.config({ panoptic })` then builds the collector and **registers it once on core's `Observer` registry** via `registerObserver`. Core itself never imports panoptic — it depends only on the structural `Observer` interface (`{ collect(report) }`), the dependency inversion that keeps the two sides decoupled.
209
+
210
+ ```ts
211
+ import { ai } from "@warlock.js/ai";
212
+ import "@warlock.js/ai-panoptic"; // side-effect wiring — do this once at boot
213
+
214
+ ai.config({
215
+ panoptic: {
216
+ exporters: [consoleExporter()], // optional; a store-shaped exporter also feeds the dashboard
217
+ observeAll: true, // observe every flow by default
218
+ dashboard: true, // optional local viewer — see use-local-dashboard
219
+ },
220
+ });
221
+ ```
222
+
223
+ `PanopticConfig`: `exporters?` (the collector's sinks), `observeAll?` (default `false`), `dashboard?` (`boolean | DashboardOptions`). Applying it is **idempotent** — the observer registers once, the dashboard starts at most once; repeat `ai.config(...)` merges only refresh `observeAll`.
224
+
225
+ ### `observeAll` + per-flow `observe`
226
+
227
+ Once a collector is registered, which flows actually route to it is controlled by core's `observe` seam:
228
+
229
+ | Setting | Effect |
230
+ |---|---|
231
+ | `observeAll: true` (config) | Every `ai.agent` / `ai.workflow` / `ai.supervisor` / `ai.team` that left `observe` unset routes its completed report to the registered observers. |
232
+ | `observe: true` (per-flow) | Force-observe **this** flow even when `observeAll` is off → the globally registered observers. |
233
+ | `observe: false` (per-flow) | Opt **this** flow out entirely, even when `observeAll` is on. |
234
+ | `observe: <Observer>` (per-flow) | A **flow-local** collector — only this flow's report, routed only to it (the global observers are skipped). A panoptic flow-local collector implements `Observer`, so pass it directly. |
235
+ | (unset, `observeAll: false`) | Not observed — opt-in default, identical to legacy behavior. |
236
+
237
+ ```ts
238
+ ai.agent({ model, observe: true }); // always traced
239
+ ai.workflow({ name: "billing", steps, observe: true }); // always traced
240
+ ai.supervisor({ name: "triage", ..., observe: false }); // never traced, even under observeAll
241
+
242
+ const flowLocal = panoptic({ exporters: [fileExporter({ path: "billing.jsonl" })] });
243
+ ai.team({ name: "research", ..., observe: flowLocal }); // only this flow, only this collector
244
+ ```
245
+
246
+ The flow awaits each observer's `collect`, and a throwing observer is **swallowed** — observability never breaks the run (mirrors `onUsage` / `onComplete`). `observe` lives on **all four** flow configs — `ai.agent`, `ai.workflow`, `ai.supervisor`, and `ai.team` (the agent additionally supports the imperative `attach()` / `middleware()` paths below).
247
+
248
+ ## Imperative — `panoptic(...)`, the one-call subscriber
249
+
250
+ `panoptic(...)` builds a collector, registers your exporters on it, and hands back a subscriber with **three ways to feed traces** in. All three converge on the same collector, so each run reaches every exporter exactly once. Use it when you want explicit control, a flow-local collector, or to wire a target the declarative seam doesn't cover (the orchestrator).
173
251
 
174
252
  ```ts
175
253
  import { panoptic } from "@warlock.js/ai-panoptic";
@@ -287,6 +365,7 @@ Add exporters after construction with `observe.use(exporter)` (chainable, dedupe
287
365
 
288
366
  ## See also
289
367
 
368
+ - [`@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`](@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md) — the zero-setup local viewer the `ai.config({ panoptic: { dashboard } })` switch starts over the collector's store.
290
369
  - [`@warlock.js/ai-panoptic/export-traces/SKILL.md`](@warlock.js/ai-panoptic/export-traces/SKILL.md) — the sink end: choosing/writing an exporter, the `gen_ai.*` / `warlock.*` attribute mapping.
291
370
  - [`@warlock.js/ai-panoptic/query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md) — retaining traces in the queryable in-memory store and slicing them (`query` / `aggregate` / `capacity`).
292
371
  - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — running the agent and the `BaseReport` shape Panoptic consumes.
@@ -296,7 +375,7 @@ Add exporters after construction with `observe.use(exporter)` (chainable, dedupe
296
375
 
297
376
  ---
298
377
  name: query-traces
299
- description: 'Retain @warlock.js/ai-panoptic traces in a queryable in-memory store and slice them after the fact get one run by id, list a session, filter failed runs in a window, roll up usage + cost. Triggers: `createInMemoryTraceStore`, `TraceStoreContract`, `TraceQuery`, `TraceAggregate`, `InMemoryTraceStoreOptions`, `store.query`, `store.aggregate`, `store.get`, `store.add`, `store.clear`, `store.size`, `capacity`, `sumUsage`, `emptyUsage`, `matchTrace`; ''query collected traces'', ''aggregate AI cost per session'', ''how much did this session spend'', ''list failed runs'', ''retain traces in memory'', ''roll up token usage''; typical import `import { createInMemoryTraceStore } from "@warlock.js/ai-panoptic"`. Skip: sending traces to an external backend (OTel/Langfuse/console/file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; wiring the subscriber into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.'
378
+ description: 'Retain @warlock.js/ai-panoptic traces in a queryable store — in-memory, or cache-backed so they survive a process restart — and slice them after the fact: get one run by id, list a session, filter failed runs in a window, roll up usage + cost. Triggers: `createInMemoryTraceStore`, `createCacheTraceStore`, `CacheTraceStoreHandle`, `CacheTraceStoreOptions`, `CacheDriverInput`, `store.ready`, `TraceStoreContract`, `TraceQuery`, `TraceAggregate`, `InMemoryTraceStoreOptions`, `store.query`, `store.aggregate`, `store.get`, `store.add`, `store.clear`, `store.size`, `capacity`, `prefix`, `onError`, `sumUsage`, `emptyUsage`, `matchTrace`; ''query collected traces'', ''aggregate AI cost per session'', ''how much did this session spend'', ''list failed runs'', ''retain traces in memory'', ''persist traces across a restart'', ''cache-backed trace store'', ''Redis trace store'', ''roll up token usage''; typical import `import { createInMemoryTraceStore, createCacheTraceStore } from "@warlock.js/ai-panoptic"`. Skip: sending traces to an external backend (OTel/Langfuse/console/file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; wiring the subscriber into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.'
300
379
  ---
301
380
 
302
381
  # Query traces — the in-memory queryable store
@@ -426,6 +505,57 @@ Overwriting an existing `traceId` refreshes its insertion position, so a re-coll
426
505
 
427
506
  > The in-memory store has no secondary indexes: `get` / `add` are O(1), `query` / `aggregate` are O(n) scans. That is the right trade-off for dev/test and modest-volume runtime use. For high-volume or durable retention, write a custom `TraceStoreContract` (or export to a backend with [`export-traces`](@warlock.js/ai-panoptic/export-traces/SKILL.md)) backed by a real datastore.
428
507
 
508
+ ## Persisting across a restart — `createCacheTraceStore`
509
+
510
+ The in-memory store loses everything when the process exits. `createCacheTraceStore(cache, options?)` is a **durable** `TraceStoreContract` (and `ExporterContract`) backed by any `@warlock.js/cache` `CacheDriver` — Redis, file, Postgres, … — so traces survive a restart and the dashboard re-opens with history intact.
511
+
512
+ ```ts
513
+ import { createCacheTraceStore } from "@warlock.js/ai-panoptic";
514
+ import { RedisCacheDriver } from "@warlock.js/cache";
515
+
516
+ // A driver instance, or a (possibly async) FACTORY resolved lazily on first
517
+ // use — the factory form defers the Redis connect until the first trace is
518
+ // collected, so module import stays connection-free.
519
+ const store = createCacheTraceStore(async () => {
520
+ const driver = new RedisCacheDriver();
521
+ await driver.connect();
522
+ return driver;
523
+ });
524
+
525
+ await store.ready(); // hydrate the in-memory mirror from a prior run
526
+ collector.use(store); // fills as traces complete; reads stay synchronous
527
+
528
+ const failed = store.query({ status: "failed" }); // queryable like any store
529
+ ```
530
+
531
+ **Write-through over a sync contract.** The store keeps the same insertion-ordered in-memory **mirror** the in-memory store uses and serves every read (`get` / `query` / `aggregate` / `size`) from it synchronously — the dashboard polls these and can't await a cache. Writes go **through**: `add` updates the mirror immediately, then *fire-and-forget* persists the trace + a newest-first index to the cache. So:
532
+
533
+ - **Reads never wait on the cache** — the mirror is the runtime source of truth; the cache is the durable backing store.
534
+ - **A flaky cache never throws into the hot path** — a rejected write is routed to the optional `onError` hook and swallowed; the trace stays visible in the mirror for the life of the process, it just won't survive a restart.
535
+ - **`ready()` re-hydrates** the mirror from the persisted index on startup (await it once — `ai.config({ panoptic: { cache } })` does this for you before serving the dashboard).
536
+
537
+ ```ts
538
+ type CacheDriverInput =
539
+ | CacheDriver // a live driver
540
+ | (() => CacheDriver | Promise<CacheDriver>); // lazy (sync or async) factory
541
+
542
+ type CacheTraceStoreOptions = {
543
+ prefix?: string; // key namespace; per-trace keys `${prefix}:trace:${id}`,
544
+ // index at `${prefix}:index`. Default "panoptic".
545
+ capacity?: number; // FIFO cap (oldest-ingested evicted from cache + mirror);
546
+ // absent / 0 = unbounded.
547
+ onError?: (error: unknown) => void; // swallowed cache-write failures land here.
548
+ };
549
+
550
+ type CacheTraceStoreHandle = TraceStoreContract & ExporterContract & {
551
+ ready(): Promise<void>; // hydrate the mirror from the cache on startup
552
+ };
553
+ ```
554
+
555
+ Ordering uses an **internal monotonic counter**, not `Date.now()`, so the persisted index stays a stable total order even when many traces land in the same millisecond — FIFO eviction stays honest across a restart.
556
+
557
+ > The usual path is declarative: `ai.config({ panoptic: { cache, dashboard: true } })` constructs this store, awaits `ready()`, and wires it under the dashboard for you — see [`use-local-dashboard/SKILL.md`](@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md). Reach for `createCacheTraceStore` directly only when you manage the store / collector yourself.
558
+
429
559
  ## Summing usage yourself
430
560
 
431
561
  `sumUsage` / `emptyUsage` are the pure folds `aggregate` is built on — reuse them to roll a `Usage` set the store didn't produce (e.g. a list of traces you filtered by hand):
@@ -448,3 +578,220 @@ for (const trace of store.query({ sessionId })) {
448
578
  - [`@warlock.js/ai/run-ai-agent/SKILL.md`](@warlock.js/ai/run-ai-agent/SKILL.md) — the `BaseReport` the collector projects into the `Trace` this store retains.
449
579
 
450
580
 
581
+ ## use-local-dashboard `@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`
582
+
583
+ ---
584
+ name: use-local-dashboard
585
+ description: 'Run the zero-setup local Panoptic dashboard — a loopback `node:http` server over a trace store served from `ai.config({ panoptic: { dashboard } })`, with a two-pane call-tree + metadata UI (light/dark/system theme, search/filter, session + prompt-version grouping, cost heatmap, Gantt timeline, errors filter, hash deep-links) and an optional cache-backed persistent store. Triggers: `dashboard`, `DashboardOptions`, `DashboardHandle`, `ai.config({ panoptic })`, `panoptic.dashboard`, `panoptic.cache`, `port: 4319`, `basePath`, `open`, `/api/traces`, `/api/aggregate`, `/api/traces/:id`, `createRequestHandler`, `dashboardHtml`, `ServeConfig`, `TraceStoreContract`, `createCacheTraceStore`, theme, dark mode, call tree, metadata panel, cost heatmap, timeline / Gantt, errors only, deep-link, group by session, prompt version, group by type, type stats panel, started / ended timestamps, team type; ''open a local AI trace dashboard'', ''view agent traces in the browser'', ''self-hosted panoptic UI'', ''inspect traces without Langfuse'', ''spin up a dev observability server'', ''dark mode dashboard'', ''persist traces across restart'', ''group traces by session / prompt version / type'', ''cost heatmap'', ''timeline / waterfall view'', ''filter failed runs in the dashboard'', ''per-type stats panel''; typical import `import { ai } from "@warlock.js/ai"` + `import "@warlock.js/ai-panoptic"`. Skip: shipping traces to a backend (OTel / Langfuse / file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the queryable store API the dashboard reads — `@warlock.js/ai-panoptic/query-traces/SKILL.md`; wiring the subscriber by hand — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.'
586
+ ---
587
+
588
+ # The local Panoptic dashboard — zero-setup trace viewing
589
+
590
+ A loopback-only `node:http` server that serves one self-contained HTML page polling a read-only JSON API over a trace store. No external backend and no build step; it binds `127.0.0.1` with no auth by default, and gains optional bearer-token auth + a Host allowlist (`authToken` / `allowedHosts`, **required** off-loopback). The store is the live object the collector fills, so each poll reflects the latest completed traces.
591
+
592
+ ## The documented path — `ai.config({ panoptic: { dashboard } })`
593
+
594
+ You almost never call `dashboard(...)` directly. Set it declaratively and the side-effect import wires the store, the collector, and the server for you:
595
+
596
+ ```ts
597
+ import { ai } from "@warlock.js/ai";
598
+ import "@warlock.js/ai-panoptic"; // side-effect: subscribes panoptic to the config seam
599
+
600
+ ai.config({
601
+ panoptic: {
602
+ dashboard: true, // serve on http://127.0.0.1:4319/
603
+ observeAll: true, // route every workflow/supervisor/team run to the collector
604
+ },
605
+ });
606
+ ```
607
+
608
+ `dashboard: true` uses the defaults. Pass an object to override:
609
+
610
+ ```ts
611
+ ai.config({
612
+ panoptic: {
613
+ exporters: [createInMemoryTraceStore({ capacity: 5000 })],
614
+ dashboard: { port: 4319, open: true, title: "My App — AI traces" },
615
+ observeAll: true,
616
+ },
617
+ });
618
+ ```
619
+
620
+ **Where the dashboard reads its traces from.** It needs a queryable store (a `TraceStoreContract`). Panoptic resolves one in this order:
621
+
622
+ 1. The first **store-shaped exporter** in `config.exporters` — `createInMemoryTraceStore()` doubles as an `ExporterContract`, so a store passed in `exporters` is reused (captured AND served from the same object).
623
+ 2. If none was supplied but `dashboard` is set, panoptic creates a fresh in-memory store and registers it as an exporter so the collector fills it.
624
+
625
+ So `dashboard: true` with no exporters Just Works — you get a store for free. Supply your own when you want a capacity cap or want to `query` / `aggregate` it from app code too.
626
+
627
+ **Idempotent.** Applying the config is safe on every `ai.config(...)` merge: the collector is registered as a core `Observer` exactly once and the dashboard is started at most once. Repeat calls only refresh the `observeAll` flag (latest config wins). `panoptic` undefined in the config is a no-op.
628
+
629
+ ## What the UI shows
630
+
631
+ The page is one self-contained, dependency-free vanilla-JS file (no bundler, no framework, works offline). It polls the JSON API every 2s and renders:
632
+
633
+ - **Header** — the Warlock logo + your `title`, the `TraceAggregate` headline counts (traces / completed / failed / cancelled), tokens as **`↓input · ↑output · total`**, total cost, an **errors-only** toggle, a **search** box, and a **light / dark / system** theme switch (persisted to `localStorage`, so the choice survives a reload; `system` follows the OS `prefers-color-scheme`).
634
+ - **Trace list** — newest-first clickable master rows, each led by a **colour-coded Title-case type label** (`Supervisor` / `Team` / `Workflow` / `Agent` / `Tool` / …; `ai.team` reports its own `team` type rather than `supervisor`, so it groups, filters, and labels separately). An optional **group-by-session** view collapses runs under collapsible session headers; a **group-by-prompt** view does the same over the `name@version` prompt key (see *Prompt-version linkage* below); a **group-by-type** view buckets the list by root type under collapsible `Type (N)` headers (`Agent (12)`, `Team (3)`, …). The three groupings are mutually exclusive.
635
+ - **Two-pane drawer** (click a row) — a collapsible **nested call tree** on the left and the **selected node's detail** on the right: rich structured input/output (chat bubbles / key-value / Markdown), a per-node **token breakdown** (`↓in · ↑out · total`), the node's **rollup cost** (its own cost, else the sum of its children — so wrapper nodes that carry tokens but no cost don't double-count), and a **metadata panel** with humanized keys (`Supervisor Terminated By`, `Span ID` — not raw `supervisor.terminatedBy`), and **started** / **ended** wall-clock timestamps (`28 Jun 2026 03:16 PM` — named month, 12-hour, minute precision) alongside the elapsed duration. Zero-valued token/cost rows are hidden so the panel stays readable.
636
+ - **Cost heatmap** — every tree node and trace row carries a left accent tinted by its rollup cost relative to the trace's most expensive node, with a small legend, so the spend hot-spots jump out at a glance.
637
+ - **Timeline view** — the drawer's left pane toggles between the nested call **Tree** and a Gantt **Timeline** (each span drawn at its offset from the root start, width = duration, critical path highlighted).
638
+ - **Stats panel** — a header **Stats** toggle shows a per-type aggregate table above the trace list, one row per root type (`Agent` / `Team` / `Workflow` / …) with Count, Failed (rate), p50 / p95 latency, total Tokens, and total Cost, computed over the active filtered set. Distinct from the headline counts in the header.
639
+
640
+ All UI state (selected trace, selected span, collapsed nodes, theme, filters) lives in JS, **not** the DOM, so the 2s poll never disturbs an open drawer.
641
+
642
+ ### Filtering & grouping (all client-side over the polled list)
643
+
644
+ The list narrows entirely in the browser — no server round-trip per keystroke:
645
+
646
+ - **Search** — free-text, case-insensitive, matched against root span name + session id.
647
+ - **Status / type chips** — restrict to selected terminal statuses or executable types. Chip, badge, drawer, and group-header labels are **Title-cased** everywhere (`Completed`, `Workflow`, `Team`); the underlying filter keys stay lowercase (`completed`, `workflow`, `team`).
648
+ - **Errors-only** — header toggle that keeps just `failed` / `cancelled` runs.
649
+ - **Session chip** — exact `sessionId`; **prompt chip** — exact `name@version` key.
650
+ - **Group-by-type** — buckets the list by root type under `Type (N)` headers; mutually exclusive with group-by-session and group-by-prompt.
651
+
652
+ The pure matching/grouping rules are unit-tested in `trace-filter.ts` and mirrored 1:1 by the inlined dashboard JS, exported for reuse / your own list views:
653
+
654
+ ```ts
655
+ import {
656
+ filterTraces, matchesFilter, groupBySession, rollupCost, maxNodeCost, heatIntensity,
657
+ type TraceFilter, type SessionGroup,
658
+ } from "@warlock.js/ai-panoptic"; // dashboard helpers, re-exported
659
+
660
+ const failed = filterTraces(traces, { errorsOnly: true });
661
+ const bySession = groupBySession(failed); // SessionGroup[], sessionless → "(no session)"
662
+ ```
663
+
664
+ `TraceFilter` is `{ text?, statuses?, types?, sessionId?, promptKey?, errorsOnly? }` — every field optional and ANDed, so `{}` matches everything. `rollupCost(span)` / `maxNodeCost(root)` / `heatIntensity(nodeCost, maxCost)` are the pure heatmap folds (intensity in `[0,1]`, `0` for a free trace rather than dividing by zero).
665
+
666
+ ### Deep-links
667
+
668
+ The open trace + span are reflected in the URL hash (`#trace=<id>&span=<id>`) and re-opened from it on load — so a drawer view is **shareable / bookmarkable**. (A live socket tail is a noted follow-up; this pass stays on the 2s poll.)
669
+
670
+ ### Prompt-version linkage
671
+
672
+ When an agent ran against a *named* `ai.prompts` builder, the collector stamps `agent.promptName` / `agent.promptVersion` onto the root span. The dashboard derives a `name@version` key from them (`tracePromptKey(trace)`) and offers **filter-by-prompt** and **group-by-prompt** beside session — so a reviewer sees every run of `support@2` together and compares its cost / failure rate against `support@3`. Runs with no named prompt bucket under `"(no prompt)"`.
673
+
674
+ ```ts
675
+ import { tracePromptKey, groupByPrompt, type PromptGroup } from "@warlock.js/ai-panoptic";
676
+
677
+ const byPrompt = groupByPrompt(store.query()); // PromptGroup[] keyed by name@version
678
+ ```
679
+
680
+ ## Persistent traces — `cache` (survive a restart)
681
+
682
+ By default the dashboard reads a fresh **in-memory** store, so traces vanish on restart. Point `panoptic.cache` at a `@warlock.js/cache` `CacheDriver` (Redis, file, …) and panoptic serves the dashboard from a **cache-backed** store instead — traces persist and re-hydrate on the next boot:
683
+
684
+ ```ts
685
+ import { ai } from "@warlock.js/ai";
686
+ import "@warlock.js/ai-panoptic";
687
+ import { RedisCacheDriver } from "@warlock.js/cache";
688
+
689
+ ai.config({
690
+ panoptic: {
691
+ dashboard: true,
692
+ observeAll: true,
693
+ // A driver, or a (possibly async) factory resolved lazily on first use —
694
+ // the factory form defers the Redis connect until the first trace lands.
695
+ cache: async () => {
696
+ const driver = new RedisCacheDriver();
697
+ await driver.connect();
698
+ return driver;
699
+ },
700
+ },
701
+ });
702
+ ```
703
+
704
+ Panoptic awaits the store's `ready()` hydration **before** the server starts serving, so a restart surfaces previously-persisted traces immediately. Reads stay synchronous (served from an in-memory mirror); writes go through to the cache fire-and-forget, so a flaky cache never throws into the collector's hot path. A store-shaped exporter you pass via `exporters` still wins over `cache`. The store building block (`createCacheTraceStore`) and its durability contract live in [`query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md).
705
+
706
+ ## The low-level building block — `dashboard(store, options)`
707
+
708
+ Reach for this directly only when you manage the store yourself (e.g. a long-lived store you also query elsewhere, or a non-default lifecycle). It returns a `Promise<DashboardHandle>`:
709
+
710
+ ```ts
711
+ import { dashboard, createInMemoryTraceStore } from "@warlock.js/ai-panoptic";
712
+
713
+ const store = createInMemoryTraceStore();
714
+ const handle = await dashboard(store, { port: 4319, open: true });
715
+
716
+ console.log(handle.url); // http://127.0.0.1:4319/
717
+ console.log(handle.port); // 4319 (the REAL OS port when port: 0 was passed)
718
+
719
+ // ...on teardown:
720
+ await handle.close();
721
+ ```
722
+
723
+ The store is the same object the collector writes to — wire the collector to it (pass it as an exporter to `panoptic({ exporters: [store] })` or via `ai.config`) and the dashboard reflects new traces on its next poll.
724
+
725
+ ### `DashboardOptions`
726
+
727
+ Every field optional, every default safe:
728
+
729
+ | Option | Default | Notes |
730
+ |---|---|---|
731
+ | `port` | `4319` | Pass `0` for an ephemeral port — the resolved port comes back on `handle.port`. A busy port rejects with a clear `Error` (`port N in use; pass { port: 0 }`), not raw `EADDRINUSE`. |
732
+ | `host` | `"127.0.0.1"` | **Loopback only** by default, so prompt content is never exposed to the LAN. Binding a non-loopback interface is gated — it requires `authToken` (see below) or `dashboard()` rejects. |
733
+ | `basePath` | `"/"` | Mount-path prefix for both the page and every `/api/...` route, e.g. `"/panoptic"`. Normalized to leading + trailing slash. |
734
+ | `open` | `false` | Open the default browser at the resolved URL once listening. Best-effort and fully swallowed — failing to open a browser never rejects the start. |
735
+ | `title` | `"Panoptic"` | Header title baked into the served page. |
736
+ | `authToken` | _unset_ | Bearer token required on every request (`Authorization: Bearer <token>` or `?token=`); missing/wrong → `401`. **Required** when `host` is non-loopback. |
737
+ | `allowedHosts` | loopback names | `Host`-header allowlist (DNS-rebind guard); an unlisted `Host` → `403`. Defaults to `localhost` / `127.0.0.1` / `[::1]` for a loopback bind. |
738
+
739
+ ### `DashboardHandle`
740
+
741
+ ```ts
742
+ type DashboardHandle = {
743
+ readonly url: string; // http://host:port{basePath}
744
+ readonly port: number; // real OS port (when port: 0 was requested)
745
+ close(): Promise<void>; // stop the server, release the port
746
+ };
747
+ ```
748
+
749
+ ## The JSON API
750
+
751
+ All routes are **`GET`-only and read-only** (non-`GET` → `405`), mounted under `basePath`. The page polls them; you can curl them too. Store shapes are already JSON-safe, so responses are a plain `JSON.stringify`.
752
+
753
+ | Route | Returns |
754
+ |---|---|
755
+ | `GET /api/traces` | `store.query(...)` — the trace list, filtered by the query string (`id` / `sessionId` / `status` / time window, parsed by `parseQuery`). |
756
+ | `GET /api/traces/:id` | `store.get(id)` — one full trace, or `404 { error: "trace_not_found" }`. |
757
+ | `GET /api/aggregate` | `store.aggregate(...)` — the usage + cost + status rollup over the same query filter. |
758
+ | `GET /` (basePath) | the self-contained HTML page. |
759
+
760
+ Query-string filters map 1:1 onto `TraceQuery`:
761
+
762
+ ```sh
763
+ curl 'http://127.0.0.1:4319/api/traces?status=failed'
764
+ curl 'http://127.0.0.1:4319/api/aggregate?sessionId=sess_42'
765
+ ```
766
+
767
+ (The exact filter keys and the aggregate shape live in [`query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md).)
768
+
769
+ ## Advanced — build the handler yourself
770
+
771
+ `createRequestHandler(store, config)` is the bare `(req, res) => void` the server closes over — split out from the lifecycle so it unit-tests without binding a port, and so you can mount the same routes inside an existing server:
772
+
773
+ ```ts
774
+ import { createRequestHandler } from "@warlock.js/ai-panoptic";
775
+ import { createServer } from "node:http";
776
+
777
+ const handler = createRequestHandler(store, {
778
+ basePath: "/",
779
+ title: "Panoptic",
780
+ allowedHosts: ["127.0.0.1", "localhost"],
781
+ });
782
+ createServer(handler).listen(4319, "127.0.0.1");
783
+ ```
784
+
785
+ `dashboardHtml(basePath, title)` returns the served page as a string if you embed it elsewhere. `ServeConfig` is `{ basePath; title; allowedHosts; authToken? }`.
786
+
787
+ ## Security note
788
+
789
+ The dashboard surfaces whatever the store holds — including captured prompt/response content when content capture is on (`captureContent`). It binds **loopback-only** by default precisely so that content never leaves the machine. Binding a non-loopback host is gated — it requires an `authToken` (and checks a `Host` allowlist, sending `nosniff` / CSP / `X-Frame-Options: DENY` on every response) — but still prefer loopback for dev, and for production ship to a real backend via an exporter rather than exposing the dashboard.
790
+
791
+ ## See also
792
+
793
+ - [`@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`](@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md) — the `ai.config({ panoptic })` wiring and the underlying `panoptic(...)` subscriber that fills the store.
794
+ - [`@warlock.js/ai-panoptic/query-traces/SKILL.md`](@warlock.js/ai-panoptic/query-traces/SKILL.md) — the `TraceStoreContract` (`query` / `get` / `aggregate`) the dashboard reads, the `TraceQuery` filter, and capacity eviction.
795
+ - [`@warlock.js/ai-panoptic/export-traces/SKILL.md`](@warlock.js/ai-panoptic/export-traces/SKILL.md) — shipping traces to OTel / Langfuse / a file instead of (or alongside) the local dashboard.
796
+
797
+
package/llms.txt CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [export-traces](@warlock.js/ai-panoptic/export-traces/SKILL.md): Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `toGenAiAttributes`, `walkSpans`, `totalCostUsd`, `GEN_AI_ATTRIBUTES`, `WARLOCK_ATTRIBUTES`, `reportToTrace`, `reportToSpan`, `extractSpanAttributes`, `normalizeError`, `formatSpanLine`, `createPanopticMiddleware`; 'export AI traces to OpenTelemetry', 'send traces to Langfuse', 'log traces to console / a file', 'gen_ai semantic conventions', 'write a custom exporter'; typical import `import { otelExporter, langfuseExporter, consoleExporter, fileExporter } from "@warlock.js/ai-panoptic"`. Skip: wiring the subscriber/collector into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`; the core report shape — `@warlock.js/ai/run-ai-agent/SKILL.md`.
10
- - [observe-with-panoptic](@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md): Wire @warlock.js/ai-panoptic into an agent/workflow/supervisor/orchestrator with the one-call `panoptic({ exporters })` subscriber. Triggers: `panoptic`, `Panoptic`, `PanopticOptions`, `.attach`, `.middleware`, `.collect`, `.toTrace`, `observe.attach(agent)`, `panoptic().middleware()`, `completedEvents`, `middlewareName`; 'observe a warlock agent', 'trace an agent run', 'attach observability to a workflow/supervisor', 'add a tracing middleware', 'collect an orchestrator turn report', 'wire OTel/Langfuse into my agent'; typical import `import { panoptic } from "@warlock.js/ai-panoptic"`. Skip: writing/choosing an exporter (the sink end) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the core report shape / running the agent itself — `@warlock.js/ai/run-ai-agent/SKILL.md`.
11
- - [query-traces](@warlock.js/ai-panoptic/query-traces/SKILL.md): Retain @warlock.js/ai-panoptic traces in a queryable in-memory store and slice them after the fact get one run by id, list a session, filter failed runs in a window, roll up usage + cost. Triggers: `createInMemoryTraceStore`, `TraceStoreContract`, `TraceQuery`, `TraceAggregate`, `InMemoryTraceStoreOptions`, `store.query`, `store.aggregate`, `store.get`, `store.add`, `store.clear`, `store.size`, `capacity`, `sumUsage`, `emptyUsage`, `matchTrace`; 'query collected traces', 'aggregate AI cost per session', 'how much did this session spend', 'list failed runs', 'retain traces in memory', 'roll up token usage'; typical import `import { createInMemoryTraceStore } from "@warlock.js/ai-panoptic"`. Skip: sending traces to an external backend (OTel/Langfuse/console/file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; wiring the subscriber into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.
9
+ - [export-traces](@warlock.js/ai-panoptic/export-traces/SKILL.md): Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters, including capturing prompt/response content onto spans. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `ContentCaptureOptions`, `captureContent`, `redactContent`, `fullHistory`, `ContentRedactor`, `toGenAiAttributes`, `walkSpans`, `totalCostUsd`, `GEN_AI_ATTRIBUTES`, `WARLOCK_ATTRIBUTES`, `reportToTrace`, `reportToSpan`, `extractSpanAttributes`, `normalizeError`, `formatSpanLine`, `createPanopticMiddleware`; 'export AI traces to OpenTelemetry', 'send traces to Langfuse', 'log traces to console / a file', 'capture the full prompt/conversation onto a span', 'gen_ai semantic conventions', 'write a custom exporter'; typical import `import { otelExporter, langfuseExporter, consoleExporter, fileExporter } from "@warlock.js/ai-panoptic"`. Skip: wiring the subscriber/collector into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`; the core report shape — `@warlock.js/ai/run-ai-agent/SKILL.md`.
10
+ - [observe-with-panoptic](@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md): Wire @warlock.js/ai-panoptic into an agent/workflow/supervisor/orchestrator — declaratively via `ai.config({ panoptic })` (registers panoptic on core's Observer seam) or with the one-call `panoptic({ exporters })` subscriber. Triggers: `panoptic`, `Panoptic`, `PanopticOptions`, `PanopticConfig`, `ai.config({ panoptic })`, `observeAll`, per-flow `observe`, `FlowObserveOption`, `registerObserver`, `Observer`, `.attach`, `.middleware`, `.collect`, `.toTrace`, `observe.attach(agent)`, `panoptic().middleware()`, `completedEvents`, `middlewareName`; 'observe a warlock agent', 'trace an agent run', 'attach observability to a workflow/supervisor', 'observe every flow by default', 'add a tracing middleware', 'collect an orchestrator turn report', 'wire OTel/Langfuse into my agent'; typical import `import { ai } from "@warlock.js/ai"` + `import "@warlock.js/ai-panoptic"` (or `import { panoptic } from "@warlock.js/ai-panoptic"`). Skip: the zero-setup local dashboard — `@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md`; writing/choosing an exporter (the sink end) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the core report shape / running the agent itself — `@warlock.js/ai/run-ai-agent/SKILL.md`.
11
+ - [query-traces](@warlock.js/ai-panoptic/query-traces/SKILL.md): Retain @warlock.js/ai-panoptic traces in a queryable store — in-memory, or cache-backed so they survive a process restart — and slice them after the fact: get one run by id, list a session, filter failed runs in a window, roll up usage + cost. Triggers: `createInMemoryTraceStore`, `createCacheTraceStore`, `CacheTraceStoreHandle`, `CacheTraceStoreOptions`, `CacheDriverInput`, `store.ready`, `TraceStoreContract`, `TraceQuery`, `TraceAggregate`, `InMemoryTraceStoreOptions`, `store.query`, `store.aggregate`, `store.get`, `store.add`, `store.clear`, `store.size`, `capacity`, `prefix`, `onError`, `sumUsage`, `emptyUsage`, `matchTrace`; 'query collected traces', 'aggregate AI cost per session', 'how much did this session spend', 'list failed runs', 'retain traces in memory', 'persist traces across a restart', 'cache-backed trace store', 'Redis trace store', 'roll up token usage'; typical import `import { createInMemoryTraceStore, createCacheTraceStore } from "@warlock.js/ai-panoptic"`. Skip: sending traces to an external backend (OTel/Langfuse/console/file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; wiring the subscriber into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.
12
+ - [use-local-dashboard](@warlock.js/ai-panoptic/use-local-dashboard/SKILL.md): Run the zero-setup local Panoptic dashboard — a loopback `node:http` server over a trace store served from `ai.config({ panoptic: { dashboard } })`, with a two-pane call-tree + metadata UI (light/dark/system theme, search/filter, session + prompt-version grouping, cost heatmap, Gantt timeline, errors filter, hash deep-links) and an optional cache-backed persistent store. Triggers: `dashboard`, `DashboardOptions`, `DashboardHandle`, `ai.config({ panoptic })`, `panoptic.dashboard`, `panoptic.cache`, `port: 4319`, `basePath`, `open`, `/api/traces`, `/api/aggregate`, `/api/traces/:id`, `createRequestHandler`, `dashboardHtml`, `ServeConfig`, `TraceStoreContract`, `createCacheTraceStore`, theme, dark mode, call tree, metadata panel, cost heatmap, timeline / Gantt, errors only, deep-link, group by session, prompt version, group by type, type stats panel, started / ended timestamps, team type; 'open a local AI trace dashboard', 'view agent traces in the browser', 'self-hosted panoptic UI', 'inspect traces without Langfuse', 'spin up a dev observability server', 'dark mode dashboard', 'persist traces across restart', 'group traces by session / prompt version / type', 'cost heatmap', 'timeline / waterfall view', 'filter failed runs in the dashboard', 'per-type stats panel'; typical import `import { ai } from "@warlock.js/ai"` + `import "@warlock.js/ai-panoptic"`. Skip: shipping traces to a backend (OTel / Langfuse / file) — `@warlock.js/ai-panoptic/export-traces/SKILL.md`; the queryable store API the dashboard reads — `@warlock.js/ai-panoptic/query-traces/SKILL.md`; wiring the subscriber by hand — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`.
package/package.json CHANGED
@@ -15,13 +15,17 @@
15
15
  "type": "git",
16
16
  "url": "https://github.com/warlockjs/ai-panoptic"
17
17
  },
18
+ "dependencies": {
19
+ "langfuse": "^3.38.20"
20
+ },
18
21
  "peerDependencies": {
19
- "@warlock.js/ai": "4.4.0",
20
22
  "@opentelemetry/api": "*",
21
23
  "@opentelemetry/sdk-trace-base": "*",
24
+ "@warlock.js/ai": "4.6.0",
25
+ "@warlock.js/cache": "4.6.0",
22
26
  "langfuse": "*"
23
27
  },
24
- "version": "4.4.0",
28
+ "version": "4.6.0",
25
29
  "main": "./cjs/index.cjs",
26
30
  "module": "./esm/index.mjs",
27
31
  "types": "./esm/index.d.mts",
package/skills/README.md CHANGED
@@ -6,12 +6,16 @@ Per-task skills. All cross-references use the form `@warlock.js/<pkg>/<skill>/SK
6
6
 
7
7
  ### [`observe-with-panoptic/`](./observe-with-panoptic/SKILL.md)
8
8
 
9
- Wire Panoptic into a run with the one-call `panoptic({ exporters })` subscriber `attach()` to an agent/workflow/supervisor event stream (every run captured automatically), `middleware()` for the agent pipeline, or `collect()` a report directly (the orchestrator turn). Covers the three feed paths, the default `*.completed` event surface and `completedEvents` override, failure isolation, `flush()` / `shutdown()`, and bringing your own collector/store. Load when attaching observability to a primitive, adding a tracing middleware, or collecting an orchestrator turn report.
9
+ Wire Panoptic into a run — declaratively via `ai.config({ panoptic })` (registers panoptic on core's `Observer` seam; `observeAll` + per-flow `observe` decide which workflow/supervisor/team runs route in), or imperatively with the one-call `panoptic({ exporters })` subscriber (`attach()` to an event stream, `middleware()` for the agent pipeline, or `collect()` a report directly the orchestrator turn). Covers the side-effect import, the three imperative feed paths, the default `*.completed` event surface and `completedEvents` override, failure isolation, `flush()` / `shutdown()`, and bringing your own collector/store. Load when attaching observability to a primitive, observing every flow by default, adding a tracing middleware, or collecting an orchestrator turn report.
10
10
 
11
11
  ### [`export-traces/`](./export-traces/SKILL.md)
12
12
 
13
- Send Panoptic traces to an observability backend via pluggable exporters — `consoleExporter` / `fileExporter` (zero-dep) and `otelExporter` (OpenTelemetry GenAI semantic conventions) / `langfuseExporter` (both lazily import their optional-peer SDK). Covers picking an exporter, the `ExporterContract`, the `gen_ai.*` / `warlock.*` attribute mapping, the shared `walkSpans` / `toGenAiAttributes` / `totalCostUsd` utilities, and authoring a custom exporter. Load when wiring `collector.use(...)` with an exporter, exporting to OTel / Langfuse / console / a file, or writing a custom sink.
13
+ Send Panoptic traces to an observability backend via pluggable exporters — `consoleExporter` / `fileExporter` (zero-dep) and `otelExporter` (OpenTelemetry GenAI semantic conventions) / `langfuseExporter` (both lazily import their optional-peer SDK). Covers picking an exporter, the `ExporterContract`, the `gen_ai.*` / `warlock.*` attribute mapping, opt-in content capture (`captureContent` / `redactContent` / `fullHistory` to emit the whole `CapturedMessage[]`), the shared `walkSpans` / `toGenAiAttributes` / `totalCostUsd` utilities, and authoring a custom exporter. Load when wiring `collector.use(...)` with an exporter, exporting to OTel / Langfuse / console / a file, capturing prompt/response content onto spans, or writing a custom sink.
14
14
 
15
15
  ### [`query-traces/`](./query-traces/SKILL.md)
16
16
 
17
- Retain traces in the queryable in-memory store and slice them after the fact `createInMemoryTraceStore` (doubles as an `ExporterContract`), `get` / `query` / `aggregate` / `clear` / `size`, the `TraceQuery` filter (id / session / status / time window), the `TraceAggregate` usage + cost + status rollup, `capacity` FIFO eviction, and the pure `sumUsage` / `emptyUsage` / `matchTrace` helpers. Load when querying collected traces, aggregating AI cost/usage per session, listing failed runs, or retaining traces in memory.
17
+ Retain traces in a queryable store — in-memory (`createInMemoryTraceStore`) or **cache-backed and restart-durable** (`createCacheTraceStore` over any `@warlock.js/cache` `CacheDriver` / async factory, with a synchronous in-memory read mirror, write-through persistence, `ready()` hydration, `prefix` / `capacity` / `onError`) — and slice them after the fact: `get` / `query` / `aggregate` / `clear` / `size`, the `TraceQuery` filter (id / session / status / time window), the `TraceAggregate` usage + cost + status rollup, `capacity` FIFO eviction, and the pure `sumUsage` / `emptyUsage` / `matchTrace` helpers. Load when querying collected traces, aggregating AI cost/usage per session, listing failed runs, retaining traces in memory, or persisting them across a restart.
18
+
19
+ ### [`use-local-dashboard/`](./use-local-dashboard/SKILL.md)
20
+
21
+ Run the zero-setup local Panoptic dashboard — a loopback `node:http` server over a trace store served from `ai.config({ panoptic: { dashboard } })`. Covers the redesigned UI (light/dark/system theme, two-pane nested call tree + metadata panel, colour-coded Title-case type labels including the first-class `Team` type, started/ended wall-clock timestamps, arrow-coded `↓in · ↑out · total` tokens, per-node rollup cost, hide-zero rows, Warlock logo), the new dashboard features (client-side search/filter, group-by-session + group-by-prompt + group-by-type, per-type stats panel, cost heatmap, Gantt timeline, errors-only toggle, hash deep-links) and their pure `trace-filter.ts` helpers (`filterTraces` / `groupBySession` / `groupByPrompt` / `rollupCost` / `maxNodeCost` / `heatIntensity` / `tracePromptKey`), the persistent `cache` store wiring, the declarative path (which store it reads, idempotent wiring), the low-level `dashboard(store, options)` building block, `DashboardOptions` (port `4319`, loopback host, `basePath`, `open`, `title`), the `DashboardHandle`, the read-only `/api/traces` / `/api/aggregate` / `/api/traces/:id` JSON API, `createRequestHandler` / `dashboardHtml` for embedding, prompt-version linkage, and the loopback-only security posture. Load when opening a local AI trace viewer in the browser, theming/filtering it, inspecting traces without a backend, persisting them across a restart, or spinning up a dev observability server.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: export-traces
3
- description: 'Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `toGenAiAttributes`, `walkSpans`, `totalCostUsd`, `GEN_AI_ATTRIBUTES`, `WARLOCK_ATTRIBUTES`, `reportToTrace`, `reportToSpan`, `extractSpanAttributes`, `normalizeError`, `formatSpanLine`, `createPanopticMiddleware`; ''export AI traces to OpenTelemetry'', ''send traces to Langfuse'', ''log traces to console / a file'', ''gen_ai semantic conventions'', ''write a custom exporter''; typical import `import { otelExporter, langfuseExporter, consoleExporter, fileExporter } from "@warlock.js/ai-panoptic"`. Skip: wiring the subscriber/collector into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`; the core report shape — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
3
+ description: 'Send @warlock.js/ai-panoptic traces to an observability backend via pluggable exporters, including capturing prompt/response content onto spans. Triggers: `consoleExporter`, `fileExporter`, `otelExporter`, `langfuseExporter`, `ExporterContract`, `ContentCaptureOptions`, `captureContent`, `redactContent`, `fullHistory`, `ContentRedactor`, `toGenAiAttributes`, `walkSpans`, `totalCostUsd`, `GEN_AI_ATTRIBUTES`, `WARLOCK_ATTRIBUTES`, `reportToTrace`, `reportToSpan`, `extractSpanAttributes`, `normalizeError`, `formatSpanLine`, `createPanopticMiddleware`; ''export AI traces to OpenTelemetry'', ''send traces to Langfuse'', ''log traces to console / a file'', ''capture the full prompt/conversation onto a span'', ''gen_ai semantic conventions'', ''write a custom exporter''; typical import `import { otelExporter, langfuseExporter, consoleExporter, fileExporter } from "@warlock.js/ai-panoptic"`. Skip: wiring the subscriber/collector into a run — `@warlock.js/ai-panoptic/observe-with-panoptic/SKILL.md`; the core report shape — `@warlock.js/ai/run-ai-agent/SKILL.md`.'
4
4
  ---
5
5
 
6
6
  # Export traces — the sink end of Panoptic
@@ -94,6 +94,38 @@ collector.use(langfuseExporter({ client: myLangfuse }));
94
94
  - Children nest under their parent observation; timing, `version`, and status (failed → `ERROR` level) are mapped.
95
95
  - `flush()` / `shutdown()` delegate to the client's `flushAsync()` / `shutdownAsync()` — call `collector.shutdown()` on teardown so buffered observations are sent.
96
96
 
97
+ ## Content capture — what reaches the `input` / `output` fields
98
+
99
+ Exporters surface `span.input` / `span.output` (console `in:` / `out:`, file JSON, OTel `gen_ai.prompt` / `gen_ai.completion`, Langfuse native `input` / `output`) only when **content capture** is enabled on the collector or subscriber. It's `ContentCaptureOptions`, set on `createCollector(...)` or `panoptic(...)`:
100
+
101
+ ```ts
102
+ panoptic({
103
+ exporters: [langfuseExporter({ /* ... */ })],
104
+ captureContent: true, // copy prompts/responses + tool args/results onto spans
105
+ redactContent: (value, { field }) => // optional masker, per value
106
+ field === "input" ? "[redacted]" : value,
107
+ fullHistory: true, // emit the WHOLE conversation as span.input (see below)
108
+ });
109
+ ```
110
+
111
+ Off by default — payloads are large and frequently sensitive. With `captureContent`, an agent span's `input` is a `[system, user]` chat array (or the bare user string when there's no system prompt) and its `output` is the last non-empty trip's text; a tool span carries its call args + result.
112
+
113
+ ### `fullHistory` — emit the entire `CapturedMessage[]`
114
+
115
+ `fullHistory: true` replaces the `[system, user]` first-trip array on `span.input` with the agent's **complete assembled conversation** — every trip, every role (system / user / assistant / tool). The `output` stays the final response text.
116
+
117
+ ```ts
118
+ // upstream: the agent run MUST opt into capturing its messages
119
+ const result = await agent.execute(input, { captureMessages: true });
120
+ // ^ core @warlock.js/ai — makes report.messages present
121
+
122
+ panoptic({ exporters: [...], captureContent: true, fullHistory: true });
123
+ ```
124
+
125
+ **Requires the upstream `captureMessages`.** `fullHistory` reads `report.messages` (the captured `CapturedMessage[]`). When the run did NOT opt into `captureMessages`, `report.messages` is absent and the branch **degrades gracefully** to today's first-trip `[system, user]` capture — never an error, just less history. The whole array is passed through `ContentRedactor` as a single value, so masking still applies to captured history.
126
+
127
+ Reach for `fullHistory` when a backend (Langfuse) should show the full multi-turn / tool-loop transcript a run actually sent, not just its opening prompt.
128
+
97
129
  ## Shared utilities
98
130
 
99
131
  Reusable when authoring a **custom exporter**: