@warlock.js/ai-panoptic 4.3.0 → 4.4.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 (53) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/cjs/index.cjs +259 -52
  3. package/cjs/index.cjs.map +1 -1
  4. package/esm/collector/collector.d.mts +2 -1
  5. package/esm/collector/collector.d.mts.map +1 -1
  6. package/esm/collector/collector.mjs +10 -4
  7. package/esm/collector/collector.mjs.map +1 -1
  8. package/esm/collector/content-capture.type.d.mts +42 -0
  9. package/esm/collector/content-capture.type.d.mts.map +1 -0
  10. package/esm/collector/index.d.mts +1 -0
  11. package/esm/collector/report-to-span.d.mts +8 -1
  12. package/esm/collector/report-to-span.d.mts.map +1 -1
  13. package/esm/collector/report-to-span.mjs +66 -2
  14. package/esm/collector/report-to-span.mjs.map +1 -1
  15. package/esm/collector/report-to-trace.d.mts +2 -1
  16. package/esm/collector/report-to-trace.d.mts.map +1 -1
  17. package/esm/collector/report-to-trace.mjs +2 -2
  18. package/esm/collector/report-to-trace.mjs.map +1 -1
  19. package/esm/contracts/trace.type.d.mts +22 -3
  20. package/esm/contracts/trace.type.d.mts.map +1 -1
  21. package/esm/exporters/console/console-exporter.d.mts +11 -3
  22. package/esm/exporters/console/console-exporter.d.mts.map +1 -1
  23. package/esm/exporters/console/console-exporter.mjs +31 -13
  24. package/esm/exporters/console/console-exporter.mjs.map +1 -1
  25. package/esm/exporters/console/console-exporter.type.d.mts +15 -0
  26. package/esm/exporters/console/console-exporter.type.d.mts.map +1 -1
  27. package/esm/exporters/console/format-span-io.d.mts +27 -0
  28. package/esm/exporters/console/format-span-io.d.mts.map +1 -0
  29. package/esm/exporters/console/format-span-io.mjs +52 -0
  30. package/esm/exporters/console/format-span-io.mjs.map +1 -0
  31. package/esm/exporters/console/index.d.mts +2 -1
  32. package/esm/exporters/index.d.mts +1 -0
  33. package/esm/exporters/index.mjs +1 -0
  34. package/esm/exporters/langfuse/langfuse-exporter.d.mts +10 -6
  35. package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -1
  36. package/esm/exporters/langfuse/langfuse-exporter.mjs +77 -30
  37. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
  38. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +3 -1
  39. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -1
  40. package/esm/exporters/otel/otel-exporter.mjs +15 -0
  41. package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
  42. package/esm/exporters/utils/gen-ai-attributes.d.mts +3 -1
  43. package/esm/exporters/utils/gen-ai-attributes.d.mts.map +1 -1
  44. package/esm/exporters/utils/gen-ai-attributes.mjs +5 -1
  45. package/esm/exporters/utils/gen-ai-attributes.mjs.map +1 -1
  46. package/esm/index.d.mts +3 -1
  47. package/esm/index.mjs +2 -1
  48. package/esm/panoptic/panoptic.d.mts.map +1 -1
  49. package/esm/panoptic/panoptic.mjs +4 -1
  50. package/esm/panoptic/panoptic.mjs.map +1 -1
  51. package/esm/panoptic/panoptic.type.d.mts +17 -0
  52. package/esm/panoptic/panoptic.type.d.mts.map +1 -1
  53. package/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { toGenAiAttributes } from "../utils/gen-ai-attributes.mjs";
1
+ import { GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES, toGenAiAttributes } from "../utils/gen-ai-attributes.mjs";
2
2
 
3
3
  //#region ../@warlock.js/ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts
4
4
  const EXPORTER_NAME = "langfuse";
@@ -42,12 +42,16 @@ function loadLangfuse() {
42
42
  * installed, and a missing SDK surfaces as a curated "install this"
43
43
  * error when the exporter first needs to build a client.
44
44
  *
45
- * The root {@link TraceSpan} becomes a Langfuse trace; each descendant
46
- * becomes a nested observation a `generation` when it produced tokens
47
- * (LLM-backed agents, supervisors), otherwise a plain `span` (tools,
48
- * callbacks). Timing, status, version, and rolled-up `usage` are mapped
49
- * 1:1; the rest of the GenAI attributes ride along as observation
50
- * metadata.
45
+ * The root {@link TraceSpan} becomes a Langfuse trace AND its top
46
+ * observation (so the root execution's own tokens are metered, not just
47
+ * its children's); every node is an observation — a `generation` when it
48
+ * metered its OWN tokens (LLM-backed agents, supervisors), otherwise a
49
+ * plain `span` (tools, callbacks, and composite nodes whose tokens came
50
+ * only from children). Timing, status, and version map 1:1; each
51
+ * generation reports its own token usage (rolled-up minus children) so
52
+ * the trace total isn't double-counted; non-usage GenAI attributes ride
53
+ * along as metadata, and captured content (under `captureContent`)
54
+ * surfaces as native `input` / `output`.
51
55
  *
52
56
  * @example
53
57
  * collector.use(langfuseExporter({ publicKey: "pk-...", secretKey: "sk-..." }));
@@ -89,20 +93,20 @@ function langfuseExporter(options) {
89
93
  */
90
94
  function emitTrace(client, trace) {
91
95
  const root = trace.root;
92
- const langfuseTrace = client.trace({
96
+ emitObservation(client.trace({
93
97
  id: root.traceId,
94
98
  name: root.name,
95
99
  sessionId: trace.sessionId,
96
100
  version: root.version,
97
101
  timestamp: new Date(root.startedAt),
98
- metadata: toGenAiAttributes(root)
99
- });
100
- for (const child of root.children) emitObservation(langfuseTrace, child);
102
+ metadata: langfuseMetadata(root)
103
+ }), root);
101
104
  }
102
105
  /**
103
106
  * Map one {@link TraceSpan} onto a Langfuse observation under `parent`,
104
- * then recurse its children. Token-producing spans become
105
- * `generation`s; everything else becomes a plain `span`.
107
+ * then recurse its children. Spans that metered their OWN tokens become
108
+ * `generation`s; everything else (tools, callbacks, composite nodes)
109
+ * becomes a plain `span`.
106
110
  */
107
111
  function emitObservation(parent, span) {
108
112
  const body = {
@@ -113,14 +117,17 @@ function emitObservation(parent, span) {
113
117
  level: toLevel(span),
114
118
  statusMessage: span.error?.message,
115
119
  version: span.version,
116
- metadata: toGenAiAttributes(span)
120
+ metadata: langfuseMetadata(span)
117
121
  };
122
+ if (span.input !== void 0) body.input = span.input;
123
+ if (span.output !== void 0) body.output = span.output;
118
124
  let observation;
119
- if (producedTokens(span)) {
125
+ const own = ownUsage(span);
126
+ if (own.total > 0) {
120
127
  body.usage = {
121
- input: span.usage.input,
122
- output: span.usage.output,
123
- total: span.usage.total,
128
+ input: own.input,
129
+ output: own.output,
130
+ total: own.total,
124
131
  unit: "TOKENS"
125
132
  };
126
133
  observation = parent.generation(body);
@@ -129,20 +136,60 @@ function emitObservation(parent, span) {
129
136
  observation.end({ endTime: body.endTime });
130
137
  }
131
138
  /**
132
- * A span counts as an LLM `generation` when it metered any tokens. Pure
133
- * tools/callbacks contribute zero own-cost and map to plain spans.
139
+ * Own token usage for a span its rolled-up {@link TraceSpan.usage}
140
+ * minus the rolled-up usage of its direct children. `TraceSpan.usage` is
141
+ * the subtree total (this node plus every descendant), so subtracting the
142
+ * children leaves the tokens THIS node alone metered, clamped at zero
143
+ * defensively.
134
144
  *
135
- * **Known limitation.** `TraceSpan.usage` is the ROLLED-UP usage (this
136
- * node's own cost plus the sum of its children), not own-usage — the
137
- * core `BaseReport` exposes no separate own-usage field, so a composite
138
- * node (e.g. an agent that itself made no model call but whose tool
139
- * children did) is classified as a `generation` on its children's
140
- * tokens. We cannot classify on own-usage until the report shape carries
141
- * it; the rolled-up `usage` block emitted on such a generation therefore
142
- * double-counts tokens already attributed to descendant observations.
145
+ * Langfuse sums observation usage into the trace total, so reporting
146
+ * own-usage on each generation (rather than the subtree rollup) is what
147
+ * keeps the trace total correct instead of multiply-counting nested
148
+ * spans. A composite node with no own tokens (e.g. a workflow whose
149
+ * tokens all came from agent children) yields `total: 0` and is emitted
150
+ * as a plain span, not a generation.
143
151
  */
144
- function producedTokens(span) {
145
- return span.usage.total > 0;
152
+ function ownUsage(span) {
153
+ let childInput = 0;
154
+ let childOutput = 0;
155
+ let childTotal = 0;
156
+ for (const child of span.children) {
157
+ childInput += child.usage.input;
158
+ childOutput += child.usage.output;
159
+ childTotal += child.usage.total;
160
+ }
161
+ return {
162
+ input: Math.max(0, span.usage.input - childInput),
163
+ output: Math.max(0, span.usage.output - childOutput),
164
+ total: Math.max(0, span.usage.total - childTotal)
165
+ };
166
+ }
167
+ /**
168
+ * Token-usage + cost keys that the per-observation `usage` block already
169
+ * carries authoritatively (as OWN usage). Omitting them from `metadata`
170
+ * avoids a confusing contradiction — metadata would otherwise show the
171
+ * rolled-up subtree totals next to an own-usage `usage` block. Cost is
172
+ * omitted for the same reason; Langfuse prices the own tokens itself.
173
+ */
174
+ const LANGFUSE_METADATA_OMIT = new Set([
175
+ GEN_AI_ATTRIBUTES.usageInputTokens,
176
+ GEN_AI_ATTRIBUTES.usageOutputTokens,
177
+ WARLOCK_ATTRIBUTES.totalTokens,
178
+ WARLOCK_ATTRIBUTES.cachedTokens,
179
+ WARLOCK_ATTRIBUTES.reasoningTokens,
180
+ WARLOCK_ATTRIBUTES.costUsd
181
+ ]);
182
+ /**
183
+ * Observation metadata — the GenAI attribute set minus the usage/cost
184
+ * keys that live authoritatively on the `usage` block (see
185
+ * {@link LANGFUSE_METADATA_OMIT}). Keeps model identity, report type,
186
+ * version, session id, and any collector-set attributes.
187
+ */
188
+ function langfuseMetadata(span) {
189
+ const all = toGenAiAttributes(span);
190
+ const metadata = {};
191
+ for (const [key, value] of Object.entries(all)) if (!LANGFUSE_METADATA_OMIT.has(key)) metadata[key] = value;
192
+ return metadata;
146
193
  }
147
194
  /**
148
195
  * Map the Panoptic span status onto a Langfuse observation level —
@@ -1 +1 @@
1
- {"version":3,"file":"langfuse-exporter.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts"],"sourcesContent":["import type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport { toGenAiAttributes } from \"../utils\";\nimport type {\n LangfuseClientLike,\n LangfuseExporterOptions,\n LangfuseObservationBody,\n LangfuseObservationLevel,\n LangfuseObservationLike,\n LangfuseTraceLike,\n} from \"./langfuse-exporter.type\";\n\nconst EXPORTER_NAME = \"langfuse\";\n\n// ============================================================\n// Lazily-loaded langfuse SDK (OPTIONAL peer)\n// ============================================================\n\nlet LangfuseSdk: typeof import(\"langfuse\");\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst LANGFUSE_INSTALL_INSTRUCTIONS = `\nThe Panoptic Langfuse exporter requires the langfuse package.\nInstall it with:\n\n npm install langfuse\n\nOr with your preferred package manager:\n\n pnpm add langfuse\n yarn add langfuse\n`.trim();\n\n/**\n * Settle the lazy import of `langfuse` once, concurrency-safe. Only\n * needed when the caller did not pass a ready `client`. A bare `catch`\n * flips the flag to `false`; the curated install string surfaces at use\n * time, never a raw module-resolution stack trace.\n */\nfunction loadLangfuse(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n LangfuseSdk = await import(\"langfuse\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * {@link ExporterContract} that maps Panoptic traces onto Langfuse\n * traces and observations. Lazily imports `langfuse` so it stays an\n * OPTIONAL peer — importing this module never forces the SDK to be\n * installed, and a missing SDK surfaces as a curated \"install this\"\n * error when the exporter first needs to build a client.\n *\n * The root {@link TraceSpan} becomes a Langfuse trace; each descendant\n * becomes a nested observation — a `generation` when it produced tokens\n * (LLM-backed agents, supervisors), otherwise a plain `span` (tools,\n * callbacks). Timing, status, version, and rolled-up `usage` are mapped\n * 1:1; the rest of the GenAI attributes ride along as observation\n * metadata.\n *\n * @example\n * collector.use(langfuseExporter({ publicKey: \"pk-...\", secretKey: \"sk-...\" }));\n * // or reuse an existing client:\n * collector.use(langfuseExporter({ client: myLangfuse }));\n */\nexport function langfuseExporter(options: LangfuseExporterOptions): ExporterContract {\n let client: LangfuseClientLike | undefined = options.client;\n\n if (!client) {\n loadLangfuse();\n }\n\n const resolveClient = async (): Promise<LangfuseClientLike> => {\n if (client) {\n return client;\n }\n\n await loadLangfuse();\n\n if (!isModuleExists) {\n throw new Error(LANGFUSE_INSTALL_INSTRUCTIONS);\n }\n\n client = new LangfuseSdk.Langfuse({\n publicKey: options.publicKey,\n secretKey: options.secretKey,\n baseUrl: options.baseUrl,\n }) as unknown as LangfuseClientLike;\n\n return client;\n };\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n const activeClient = await resolveClient();\n emitTrace(activeClient, trace);\n },\n async flush(): Promise<void> {\n if (!client) {\n return;\n }\n\n await client.flushAsync();\n },\n async shutdown(): Promise<void> {\n if (!client) {\n return;\n }\n\n await client.shutdownAsync();\n },\n };\n}\n\n/**\n * Create the Langfuse trace from the root span, then recurse the\n * children into nested observations.\n */\nfunction emitTrace(client: LangfuseClientLike, trace: Trace): void {\n const root = trace.root;\n\n const langfuseTrace = client.trace({\n id: root.traceId,\n name: root.name,\n sessionId: trace.sessionId,\n version: root.version,\n timestamp: new Date(root.startedAt),\n metadata: toGenAiAttributes(root),\n });\n\n for (const child of root.children) {\n emitObservation(langfuseTrace, child);\n }\n}\n\n/**\n * Map one {@link TraceSpan} onto a Langfuse observation under `parent`,\n * then recurse its children. Token-producing spans become\n * `generation`s; everything else becomes a plain `span`.\n */\nfunction emitObservation(\n parent: LangfuseTraceLike | LangfuseObservationLike,\n span: TraceSpan,\n): void {\n const body: LangfuseObservationBody = {\n id: span.spanId,\n name: span.name,\n startTime: new Date(span.startedAt),\n endTime: new Date(span.endedAt),\n level: toLevel(span),\n statusMessage: span.error?.message,\n version: span.version,\n metadata: toGenAiAttributes(span),\n };\n\n let observation: LangfuseObservationLike;\n\n if (producedTokens(span)) {\n body.usage = {\n input: span.usage.input,\n output: span.usage.output,\n total: span.usage.total,\n unit: \"TOKENS\",\n };\n observation = parent.generation(body);\n } else {\n observation = parent.span(body);\n }\n\n for (const child of span.children) {\n emitObservation(observation, child);\n }\n\n // Explicitly end the observation. The body already carries `endTime`,\n // so this is idempotent — but the SDK only finalizes (and flushes) an\n // observation on `end()`, so without it long-lived clients can leave\n // observations open. Safe against the local `LangfuseObservationLike`\n // shape, which declares `end(body?)`.\n observation.end({ endTime: body.endTime });\n}\n\n/**\n * A span counts as an LLM `generation` when it metered any tokens. Pure\n * tools/callbacks contribute zero own-cost and map to plain spans.\n *\n * **Known limitation.** `TraceSpan.usage` is the ROLLED-UP usage (this\n * node's own cost plus the sum of its children), not own-usage — the\n * core `BaseReport` exposes no separate own-usage field, so a composite\n * node (e.g. an agent that itself made no model call but whose tool\n * children did) is classified as a `generation` on its children's\n * tokens. We cannot classify on own-usage until the report shape carries\n * it; the rolled-up `usage` block emitted on such a generation therefore\n * double-counts tokens already attributed to descendant observations.\n */\nfunction producedTokens(span: TraceSpan): boolean {\n return span.usage.total > 0;\n}\n\n/**\n * Map the Panoptic span status onto a Langfuse observation level —\n * failed/cancelled spans surface as `ERROR`, everything else `DEFAULT`.\n */\nfunction toLevel(span: TraceSpan): LangfuseObservationLevel {\n if (span.status === \"failed\" || span.status === \"cancelled\") {\n return \"ERROR\";\n }\n\n return \"DEFAULT\";\n}\n"],"mappings":";;;AAWA,MAAM,gBAAgB;AAMtB,IAAI;AACJ,IAAI,iBAAiC;AACrC,IAAI;AAEJ,MAAM,gCAAgC;;;;;;;;;;EAUpC,KAAK;;;;;;;AAQP,SAAS,eAA8B;CACrC,IAAI,mBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,cAAc,MAAM,OAAO;GAC3B,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;AAqBA,SAAgB,iBAAiB,SAAoD;CACnF,IAAI,SAAyC,QAAQ;CAErD,IAAI,CAAC,QACH,aAAa;CAGf,MAAM,gBAAgB,YAAyC;EAC7D,IAAI,QACF,OAAO;EAGT,MAAM,aAAa;EAEnB,IAAI,CAAC,gBACH,MAAM,IAAI,MAAM,6BAA6B;EAG/C,SAAS,IAAI,YAAY,SAAS;GAChC,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,SAAS,QAAQ;EACnB,CAAC;EAED,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EACN,MAAM,OAAO,OAA6B;GAExC,UAAU,MADiB,cAAc,GACjB,KAAK;EAC/B;EACA,MAAM,QAAuB;GAC3B,IAAI,CAAC,QACH;GAGF,MAAM,OAAO,WAAW;EAC1B;EACA,MAAM,WAA0B;GAC9B,IAAI,CAAC,QACH;GAGF,MAAM,OAAO,cAAc;EAC7B;CACF;AACF;;;;;AAMA,SAAS,UAAU,QAA4B,OAAoB;CACjE,MAAM,OAAO,MAAM;CAEnB,MAAM,gBAAgB,OAAO,MAAM;EACjC,IAAI,KAAK;EACT,MAAM,KAAK;EACX,WAAW,MAAM;EACjB,SAAS,KAAK;EACd,WAAW,IAAI,KAAK,KAAK,SAAS;EAClC,UAAU,kBAAkB,IAAI;CAClC,CAAC;CAED,KAAK,MAAM,SAAS,KAAK,UACvB,gBAAgB,eAAe,KAAK;AAExC;;;;;;AAOA,SAAS,gBACP,QACA,MACM;CACN,MAAM,OAAgC;EACpC,IAAI,KAAK;EACT,MAAM,KAAK;EACX,WAAW,IAAI,KAAK,KAAK,SAAS;EAClC,SAAS,IAAI,KAAK,KAAK,OAAO;EAC9B,OAAO,QAAQ,IAAI;EACnB,eAAe,KAAK,OAAO;EAC3B,SAAS,KAAK;EACd,UAAU,kBAAkB,IAAI;CAClC;CAEA,IAAI;CAEJ,IAAI,eAAe,IAAI,GAAG;EACxB,KAAK,QAAQ;GACX,OAAO,KAAK,MAAM;GAClB,QAAQ,KAAK,MAAM;GACnB,OAAO,KAAK,MAAM;GAClB,MAAM;EACR;EACA,cAAc,OAAO,WAAW,IAAI;CACtC,OACE,cAAc,OAAO,KAAK,IAAI;CAGhC,KAAK,MAAM,SAAS,KAAK,UACvB,gBAAgB,aAAa,KAAK;CAQpC,YAAY,IAAI,EAAE,SAAS,KAAK,QAAQ,CAAC;AAC3C;;;;;;;;;;;;;;AAeA,SAAS,eAAe,MAA0B;CAChD,OAAO,KAAK,MAAM,QAAQ;AAC5B;;;;;AAMA,SAAS,QAAQ,MAA2C;CAC1D,IAAI,KAAK,WAAW,YAAY,KAAK,WAAW,aAC9C,OAAO;CAGT,OAAO;AACT"}
1
+ {"version":3,"file":"langfuse-exporter.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts"],"sourcesContent":["import type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport type { AttributeValue } from \"../utils\";\nimport { GEN_AI_ATTRIBUTES, toGenAiAttributes, WARLOCK_ATTRIBUTES } from \"../utils\";\nimport type {\n LangfuseClientLike,\n LangfuseExporterOptions,\n LangfuseObservationBody,\n LangfuseObservationLevel,\n LangfuseObservationLike,\n LangfuseTraceLike,\n} from \"./langfuse-exporter.type\";\n\nconst EXPORTER_NAME = \"langfuse\";\n\n// ============================================================\n// Lazily-loaded langfuse SDK (OPTIONAL peer)\n// ============================================================\n\nlet LangfuseSdk: typeof import(\"langfuse\");\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst LANGFUSE_INSTALL_INSTRUCTIONS = `\nThe Panoptic Langfuse exporter requires the langfuse package.\nInstall it with:\n\n npm install langfuse\n\nOr with your preferred package manager:\n\n pnpm add langfuse\n yarn add langfuse\n`.trim();\n\n/**\n * Settle the lazy import of `langfuse` once, concurrency-safe. Only\n * needed when the caller did not pass a ready `client`. A bare `catch`\n * flips the flag to `false`; the curated install string surfaces at use\n * time, never a raw module-resolution stack trace.\n */\nfunction loadLangfuse(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n LangfuseSdk = await import(\"langfuse\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * {@link ExporterContract} that maps Panoptic traces onto Langfuse\n * traces and observations. Lazily imports `langfuse` so it stays an\n * OPTIONAL peer — importing this module never forces the SDK to be\n * installed, and a missing SDK surfaces as a curated \"install this\"\n * error when the exporter first needs to build a client.\n *\n * The root {@link TraceSpan} becomes a Langfuse trace AND its top\n * observation (so the root execution's own tokens are metered, not just\n * its children's); every node is an observation — a `generation` when it\n * metered its OWN tokens (LLM-backed agents, supervisors), otherwise a\n * plain `span` (tools, callbacks, and composite nodes whose tokens came\n * only from children). Timing, status, and version map 1:1; each\n * generation reports its own token usage (rolled-up minus children) so\n * the trace total isn't double-counted; non-usage GenAI attributes ride\n * along as metadata, and captured content (under `captureContent`)\n * surfaces as native `input` / `output`.\n *\n * @example\n * collector.use(langfuseExporter({ publicKey: \"pk-...\", secretKey: \"sk-...\" }));\n * // or reuse an existing client:\n * collector.use(langfuseExporter({ client: myLangfuse }));\n */\nexport function langfuseExporter(options: LangfuseExporterOptions): ExporterContract {\n let client: LangfuseClientLike | undefined = options.client;\n\n if (!client) {\n loadLangfuse();\n }\n\n const resolveClient = async (): Promise<LangfuseClientLike> => {\n if (client) {\n return client;\n }\n\n await loadLangfuse();\n\n if (!isModuleExists) {\n throw new Error(LANGFUSE_INSTALL_INSTRUCTIONS);\n }\n\n client = new LangfuseSdk.Langfuse({\n publicKey: options.publicKey,\n secretKey: options.secretKey,\n baseUrl: options.baseUrl,\n }) as unknown as LangfuseClientLike;\n\n return client;\n };\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n const activeClient = await resolveClient();\n emitTrace(activeClient, trace);\n },\n async flush(): Promise<void> {\n if (!client) {\n return;\n }\n\n await client.flushAsync();\n },\n async shutdown(): Promise<void> {\n if (!client) {\n return;\n }\n\n await client.shutdownAsync();\n },\n };\n}\n\n/**\n * Create the Langfuse trace from the root span, then recurse the\n * children into nested observations.\n */\nfunction emitTrace(client: LangfuseClientLike, trace: Trace): void {\n const root = trace.root;\n\n const langfuseTrace = client.trace({\n id: root.traceId,\n name: root.name,\n sessionId: trace.sessionId,\n version: root.version,\n timestamp: new Date(root.startedAt),\n metadata: langfuseMetadata(root),\n });\n\n // Emit the ROOT as an observation too — not just its children — so the\n // root execution's OWN tokens are metered. Langfuse derives the trace\n // total by summing observation usage; leaving the root (often the single\n // top-level agent) as trace-only would drop its own spend. Children nest\n // under the root observation, mirroring the execution tree, and own-usage\n // metering telescopes the per-node sums back to the true trace total.\n emitObservation(langfuseTrace, root);\n}\n\n/**\n * Map one {@link TraceSpan} onto a Langfuse observation under `parent`,\n * then recurse its children. Spans that metered their OWN tokens become\n * `generation`s; everything else (tools, callbacks, composite nodes)\n * becomes a plain `span`.\n */\nfunction emitObservation(\n parent: LangfuseTraceLike | LangfuseObservationLike,\n span: TraceSpan,\n): void {\n const body: LangfuseObservationBody = {\n id: span.spanId,\n name: span.name,\n startTime: new Date(span.startedAt),\n endTime: new Date(span.endedAt),\n level: toLevel(span),\n statusMessage: span.error?.message,\n version: span.version,\n metadata: langfuseMetadata(span),\n };\n\n // Captured content (only present under `captureContent`) maps onto\n // Langfuse's native observation input/output.\n if (span.input !== undefined) {\n body.input = span.input;\n }\n\n if (span.output !== undefined) {\n body.output = span.output;\n }\n\n let observation: LangfuseObservationLike;\n\n // Classify + meter on OWN tokens (this node's rolled-up usage minus its\n // children's), not the subtree rollup. A composite node whose tokens\n // came only from descendants has zero own-usage and becomes a plain\n // span — so its children's tokens aren't counted twice in the trace\n // total Langfuse sums across observations.\n const own = ownUsage(span);\n\n if (own.total > 0) {\n body.usage = {\n input: own.input,\n output: own.output,\n total: own.total,\n unit: \"TOKENS\",\n };\n observation = parent.generation(body);\n } else {\n observation = parent.span(body);\n }\n\n for (const child of span.children) {\n emitObservation(observation, child);\n }\n\n // Explicitly end the observation. The body already carries `endTime`,\n // so this is idempotent — but the SDK only finalizes (and flushes) an\n // observation on `end()`, so without it long-lived clients can leave\n // observations open. Safe against the local `LangfuseObservationLike`\n // shape, which declares `end(body?)`.\n observation.end({ endTime: body.endTime });\n}\n\n/**\n * Own token usage for a span — its rolled-up {@link TraceSpan.usage}\n * minus the rolled-up usage of its direct children. `TraceSpan.usage` is\n * the subtree total (this node plus every descendant), so subtracting the\n * children leaves the tokens THIS node alone metered, clamped at zero\n * defensively.\n *\n * Langfuse sums observation usage into the trace total, so reporting\n * own-usage on each generation (rather than the subtree rollup) is what\n * keeps the trace total correct instead of multiply-counting nested\n * spans. A composite node with no own tokens (e.g. a workflow whose\n * tokens all came from agent children) yields `total: 0` and is emitted\n * as a plain span, not a generation.\n */\nfunction ownUsage(span: TraceSpan): { input: number; output: number; total: number } {\n let childInput = 0;\n let childOutput = 0;\n let childTotal = 0;\n\n for (const child of span.children) {\n childInput += child.usage.input;\n childOutput += child.usage.output;\n childTotal += child.usage.total;\n }\n\n return {\n input: Math.max(0, span.usage.input - childInput),\n output: Math.max(0, span.usage.output - childOutput),\n total: Math.max(0, span.usage.total - childTotal),\n };\n}\n\n/**\n * Token-usage + cost keys that the per-observation `usage` block already\n * carries authoritatively (as OWN usage). Omitting them from `metadata`\n * avoids a confusing contradiction — metadata would otherwise show the\n * rolled-up subtree totals next to an own-usage `usage` block. Cost is\n * omitted for the same reason; Langfuse prices the own tokens itself.\n */\nconst LANGFUSE_METADATA_OMIT = new Set<string>([\n GEN_AI_ATTRIBUTES.usageInputTokens,\n GEN_AI_ATTRIBUTES.usageOutputTokens,\n WARLOCK_ATTRIBUTES.totalTokens,\n WARLOCK_ATTRIBUTES.cachedTokens,\n WARLOCK_ATTRIBUTES.reasoningTokens,\n WARLOCK_ATTRIBUTES.costUsd,\n]);\n\n/**\n * Observation metadata — the GenAI attribute set minus the usage/cost\n * keys that live authoritatively on the `usage` block (see\n * {@link LANGFUSE_METADATA_OMIT}). Keeps model identity, report type,\n * version, session id, and any collector-set attributes.\n */\nfunction langfuseMetadata(span: TraceSpan): Record<string, AttributeValue> {\n const all = toGenAiAttributes(span);\n const metadata: Record<string, AttributeValue> = {};\n\n for (const [key, value] of Object.entries(all)) {\n if (!LANGFUSE_METADATA_OMIT.has(key)) {\n metadata[key] = value;\n }\n }\n\n return metadata;\n}\n\n/**\n * Map the Panoptic span status onto a Langfuse observation level —\n * failed/cancelled spans surface as `ERROR`, everything else `DEFAULT`.\n */\nfunction toLevel(span: TraceSpan): LangfuseObservationLevel {\n if (span.status === \"failed\" || span.status === \"cancelled\") {\n return \"ERROR\";\n }\n\n return \"DEFAULT\";\n}\n"],"mappings":";;;AAYA,MAAM,gBAAgB;AAMtB,IAAI;AACJ,IAAI,iBAAiC;AACrC,IAAI;AAEJ,MAAM,gCAAgC;;;;;;;;;;EAUpC,KAAK;;;;;;;AAQP,SAAS,eAA8B;CACrC,IAAI,mBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,cAAc,MAAM,OAAO;GAC3B,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,iBAAiB,SAAoD;CACnF,IAAI,SAAyC,QAAQ;CAErD,IAAI,CAAC,QACH,aAAa;CAGf,MAAM,gBAAgB,YAAyC;EAC7D,IAAI,QACF,OAAO;EAGT,MAAM,aAAa;EAEnB,IAAI,CAAC,gBACH,MAAM,IAAI,MAAM,6BAA6B;EAG/C,SAAS,IAAI,YAAY,SAAS;GAChC,WAAW,QAAQ;GACnB,WAAW,QAAQ;GACnB,SAAS,QAAQ;EACnB,CAAC;EAED,OAAO;CACT;CAEA,OAAO;EACL,MAAM;EACN,MAAM,OAAO,OAA6B;GAExC,UAAU,MADiB,cAAc,GACjB,KAAK;EAC/B;EACA,MAAM,QAAuB;GAC3B,IAAI,CAAC,QACH;GAGF,MAAM,OAAO,WAAW;EAC1B;EACA,MAAM,WAA0B;GAC9B,IAAI,CAAC,QACH;GAGF,MAAM,OAAO,cAAc;EAC7B;CACF;AACF;;;;;AAMA,SAAS,UAAU,QAA4B,OAAoB;CACjE,MAAM,OAAO,MAAM;CAiBnB,gBAfsB,OAAO,MAAM;EACjC,IAAI,KAAK;EACT,MAAM,KAAK;EACX,WAAW,MAAM;EACjB,SAAS,KAAK;EACd,WAAW,IAAI,KAAK,KAAK,SAAS;EAClC,UAAU,iBAAiB,IAAI;CACjC,CAQ4B,GAAG,IAAI;AACrC;;;;;;;AAQA,SAAS,gBACP,QACA,MACM;CACN,MAAM,OAAgC;EACpC,IAAI,KAAK;EACT,MAAM,KAAK;EACX,WAAW,IAAI,KAAK,KAAK,SAAS;EAClC,SAAS,IAAI,KAAK,KAAK,OAAO;EAC9B,OAAO,QAAQ,IAAI;EACnB,eAAe,KAAK,OAAO;EAC3B,SAAS,KAAK;EACd,UAAU,iBAAiB,IAAI;CACjC;CAIA,IAAI,KAAK,UAAU,QACjB,KAAK,QAAQ,KAAK;CAGpB,IAAI,KAAK,WAAW,QAClB,KAAK,SAAS,KAAK;CAGrB,IAAI;CAOJ,MAAM,MAAM,SAAS,IAAI;CAEzB,IAAI,IAAI,QAAQ,GAAG;EACjB,KAAK,QAAQ;GACX,OAAO,IAAI;GACX,QAAQ,IAAI;GACZ,OAAO,IAAI;GACX,MAAM;EACR;EACA,cAAc,OAAO,WAAW,IAAI;CACtC,OACE,cAAc,OAAO,KAAK,IAAI;CAGhC,KAAK,MAAM,SAAS,KAAK,UACvB,gBAAgB,aAAa,KAAK;CAQpC,YAAY,IAAI,EAAE,SAAS,KAAK,QAAQ,CAAC;AAC3C;;;;;;;;;;;;;;;AAgBA,SAAS,SAAS,MAAmE;CACnF,IAAI,aAAa;CACjB,IAAI,cAAc;CAClB,IAAI,aAAa;CAEjB,KAAK,MAAM,SAAS,KAAK,UAAU;EACjC,cAAc,MAAM,MAAM;EAC1B,eAAe,MAAM,MAAM;EAC3B,cAAc,MAAM,MAAM;CAC5B;CAEA,OAAO;EACL,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,UAAU;EAChD,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,SAAS,WAAW;EACnD,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,UAAU;CAClD;AACF;;;;;;;;AASA,MAAM,yBAAyB,IAAI,IAAY;CAC7C,kBAAkB;CAClB,kBAAkB;CAClB,mBAAmB;CACnB,mBAAmB;CACnB,mBAAmB;CACnB,mBAAmB;AACrB,CAAC;;;;;;;AAQD,SAAS,iBAAiB,MAAiD;CACzE,MAAM,MAAM,kBAAkB,IAAI;CAClC,MAAM,WAA2C,CAAC;CAElD,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,GAAG,GAC3C,IAAI,CAAC,uBAAuB,IAAI,GAAG,GACjC,SAAS,OAAO;CAIpB,OAAO;AACT;;;;;AAMA,SAAS,QAAQ,MAA2C;CAC1D,IAAI,KAAK,WAAW,YAAY,KAAK,WAAW,aAC9C,OAAO;CAGT,OAAO;AACT"}
@@ -49,7 +49,9 @@ type LangfuseObservationBody = {
49
49
  statusMessage?: string;
50
50
  version?: string;
51
51
  usage?: LangfuseUsageBody;
52
- metadata?: Record<string, unknown>;
52
+ metadata?: Record<string, unknown>; /** Captured input (prompt / tool args), set only under content capture. */
53
+ input?: unknown; /** Captured output (completion / tool result), set only under content capture. */
54
+ output?: unknown;
53
55
  };
54
56
  /** Body accepted by `observation.end()`. */
55
57
  type LangfuseObservationEndBody = {
@@ -1 +1 @@
1
- {"version":3,"file":"langfuse-exporter.type.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/langfuse/langfuse-exporter.type.ts"],"mappings":";;AASA;;;;;;;;KAAY,kBAAA;EACV,KAAA,CAAM,IAAA,EAAM,iBAAA,GAAoB,iBAAA;EAChC,UAAA,IAAc,OAAA;EACd,aAAA,IAAiB,OAAA;AAAA;;;;;KAOP,iBAAA;EACV,IAAA,CAAK,IAAA,EAAM,uBAAA,GAA0B,uBAAA;EACrC,UAAA,CAAW,IAAA,EAAM,uBAAA,GAA0B,uBAAA;AAAA;;;;;KAOjC,uBAAA;EACV,IAAA,CAAK,IAAA,EAAM,uBAAA,GAA0B,uBAAA;EACrC,UAAA,CAAW,IAAA,EAAM,uBAAA,GAA0B,uBAAA;EAC3C,GAAA,CAAI,IAAA,GAAO,0BAAA;AAAA;;KAID,iBAAA;EACV,EAAA;EACA,IAAA;EACA,SAAA;EACA,OAAA;EACA,SAAA,GAAY,IAAA;EACZ,QAAA,GAAW,MAAM;AAAA;;KAIP,uBAAA;EACV,EAAA;EACA,IAAA;EACA,SAAA,GAAY,IAAA;EACZ,OAAA,GAAU,IAAA;EACV,KAAA,GAAQ,wBAAA;EACR,aAAA;EACA,OAAA;EACA,KAAA,GAAQ,iBAAA;EACR,QAAA,GAAW,MAAA;AAAA;;KAID,0BAAA;EACV,OAAA,GAAU,IAAA;EACV,KAAA,GAAQ,wBAAwB;EAChC,aAAA;AAAA;;KAIU,iBAAA;EACV,KAAA;EACA,MAAA;EACA,KAAA;EACA,IAAA;AAAA;;KAIU,wBAAA;;;;;;;;;KAUA,uBAAA;EAtCA;;;;;EA4CV,MAAA,GAAS,kBAAkB,EApCnB;EAsCR,SAAA,WArCiB;EAuCjB,SAAA,WA/CA;EAiDA,OAAA;AAAA"}
1
+ {"version":3,"file":"langfuse-exporter.type.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/langfuse/langfuse-exporter.type.ts"],"mappings":";;AASA;;;;;;;;KAAY,kBAAA;EACV,KAAA,CAAM,IAAA,EAAM,iBAAA,GAAoB,iBAAA;EAChC,UAAA,IAAc,OAAA;EACd,aAAA,IAAiB,OAAA;AAAA;;;;;KAOP,iBAAA;EACV,IAAA,CAAK,IAAA,EAAM,uBAAA,GAA0B,uBAAA;EACrC,UAAA,CAAW,IAAA,EAAM,uBAAA,GAA0B,uBAAA;AAAA;;;;;KAOjC,uBAAA;EACV,IAAA,CAAK,IAAA,EAAM,uBAAA,GAA0B,uBAAA;EACrC,UAAA,CAAW,IAAA,EAAM,uBAAA,GAA0B,uBAAA;EAC3C,GAAA,CAAI,IAAA,GAAO,0BAAA;AAAA;;KAID,iBAAA;EACV,EAAA;EACA,IAAA;EACA,SAAA;EACA,OAAA;EACA,SAAA,GAAY,IAAA;EACZ,QAAA,GAAW,MAAM;AAAA;;KAIP,uBAAA;EACV,EAAA;EACA,IAAA;EACA,SAAA,GAAY,IAAA;EACZ,OAAA,GAAU,IAAA;EACV,KAAA,GAAQ,wBAAA;EACR,aAAA;EACA,OAAA;EACA,KAAA,GAAQ,iBAAA;EACR,QAAA,GAAW,MAAA,mBAzBN;EA2BL,KAAA,YA1BA;EA4BA,MAAA;AAAA;;KAIU,0BAAA;EACV,OAAA,GAAU,IAAA;EACV,KAAA,GAAQ,wBAAwB;EAChC,aAAA;AAAA;AA9BF;AAAA,KAkCY,iBAAA;EACV,KAAA;EACA,MAAA;EACA,KAAA;EACA,IAAA;AAAA;;KAIU,wBAAA;;;;;AApCO;AAInB;;;KA0CY,uBAAA;EAtCA;;;;;EA4CV,MAAA,GAAS,kBAAkB,EA/C3B;EAiDA,SAAA,WA/CA;EAiDA,SAAA,WAhDA;EAkDA,OAAA;AAAA"}
@@ -100,9 +100,24 @@ function emitSpan(tracer, span, parentContext, options) {
100
100
  function applyAttributes(otelSpan, span, options) {
101
101
  const attributes = toGenAiAttributes(span);
102
102
  if (options.system !== void 0 && attributes[GEN_AI_ATTRIBUTES.system] === void 0) attributes[GEN_AI_ATTRIBUTES.system] = options.system;
103
+ if (span.input !== void 0) attributes[GEN_AI_ATTRIBUTES.prompt] = stringifyContent(span.input);
104
+ if (span.output !== void 0) attributes[GEN_AI_ATTRIBUTES.completion] = stringifyContent(span.output);
103
105
  otelSpan.setAttributes(attributes);
104
106
  }
105
107
  /**
108
+ * Coerce a captured content value to a string OTel attribute. Strings
109
+ * pass through; structured values are JSON-encoded (falling back to
110
+ * `String()` if they can't be serialized).
111
+ */
112
+ function stringifyContent(value) {
113
+ if (typeof value === "string") return value;
114
+ try {
115
+ return JSON.stringify(value) ?? String(value);
116
+ } catch {
117
+ return String(value);
118
+ }
119
+ }
120
+ /**
106
121
  * Map the Panoptic span status onto the OTel span status, recording the
107
122
  * normalized error as an exception event + ERROR status when present.
108
123
  */
@@ -1 +1 @@
1
- {"version":3,"file":"otel-exporter.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/otel/otel-exporter.ts"],"sourcesContent":["import type {\n Context,\n Span,\n SpanStatusCode as SpanStatusCodeEnum,\n Tracer,\n} from \"@opentelemetry/api\";\nimport type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport { toGenAiAttributes, GEN_AI_ATTRIBUTES } from \"../utils\";\nimport type { OtelExporterOptions } from \"./otel-exporter.type\";\n\nconst EXPORTER_NAME = \"otel\";\nconst DEFAULT_TRACER_NAME = \"@warlock.js/ai-panoptic\";\n\n// ============================================================\n// Lazily-loaded @opentelemetry/api (OPTIONAL peer)\n// ============================================================\n\nlet OtelApi: typeof import(\"@opentelemetry/api\");\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst OTEL_INSTALL_INSTRUCTIONS = `\nThe Panoptic OpenTelemetry exporter requires the @opentelemetry/api package.\nInstall it with:\n\n npm install @opentelemetry/api\n\nOr with your preferred package manager:\n\n pnpm add @opentelemetry/api\n yarn add @opentelemetry/api\n`.trim();\n\n/**\n * Settle the lazy import of `@opentelemetry/api` once, concurrency-safe.\n * A bare `catch` flips the flag to `false`; the curated install string\n * surfaces at use time so a missing SDK never throws a raw module\n * resolution error.\n */\nfunction loadOtel(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n OtelApi = await import(\"@opentelemetry/api\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * {@link ExporterContract} that maps Panoptic traces onto OpenTelemetry\n * spans following the GenAI semantic conventions (`gen_ai.*`\n * attributes). Lazily imports `@opentelemetry/api` so it stays an\n * OPTIONAL peer — importing this module never forces the SDK to be\n * installed, and a missing SDK surfaces as a curated \"install this\"\n * error on first `export`, not a boot-time stack trace.\n *\n * The exporter emits onto a `Tracer` you supply (or fetches one from the\n * globally registered provider). It never configures the SDK — wiring a\n * `TracerProvider`, processors, and span exporters is the host app's\n * job, exactly as with any other OTel instrumentation.\n *\n * Each {@link TraceSpan} becomes one OTel span with the source span's\n * start/end times and parent relationship reconstructed, so the emitted\n * tree matches the original execution tree.\n *\n * @example\n * // app already set up @opentelemetry/sdk-trace-base + a provider\n * collector.use(otelExporter({ tracerName: \"my-app\", system: \"openai\" }));\n */\nexport function otelExporter(options: OtelExporterOptions = {}): ExporterContract {\n loadOtel();\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n await loadOtel();\n\n if (!isModuleExists) {\n throw new Error(OTEL_INSTALL_INSTRUCTIONS);\n }\n\n const tracer = resolveTracer(options);\n emitSpan(tracer, trace.root, undefined, options);\n },\n };\n}\n\n/**\n * Resolve the `Tracer` spans are emitted on — the caller-supplied one,\n * or one fetched from the globally registered provider by name.\n */\nfunction resolveTracer(options: OtelExporterOptions): Tracer {\n if (options.tracer) {\n return options.tracer;\n }\n\n return OtelApi.trace.getTracer(\n options.tracerName ?? DEFAULT_TRACER_NAME,\n options.tracerVersion,\n );\n}\n\n/**\n * Recreate one {@link TraceSpan} (and its subtree) as OTel spans. The\n * span is started with the source `startedAt`, parented under\n * `parentContext` so the tree is preserved, annotated with GenAI\n * attributes, given the mapped status, and ended at `endedAt`. Children\n * recurse under this span's context.\n */\nfunction emitSpan(\n tracer: Tracer,\n span: TraceSpan,\n parentContext: Context | undefined,\n options: OtelExporterOptions,\n): void {\n const startTime = toEpochMillis(span.startedAt);\n const baseContext = parentContext ?? OtelApi.context.active();\n\n const otelSpan = tracer.startSpan(span.name, { startTime }, baseContext);\n\n applyAttributes(otelSpan, span, options);\n applyStatus(otelSpan, span);\n\n const childContext = OtelApi.trace.setSpan(baseContext, otelSpan);\n\n for (const child of span.children) {\n emitSpan(tracer, child, childContext, options);\n }\n\n otelSpan.end(toEpochMillis(span.endedAt));\n}\n\n/**\n * Set the GenAI + Warlock attributes on the OTel span, defaulting\n * `gen_ai.system` from the exporter options when the span carried none.\n */\nfunction applyAttributes(\n otelSpan: Span,\n span: TraceSpan,\n options: OtelExporterOptions,\n): void {\n const attributes = toGenAiAttributes(span);\n\n if (options.system !== undefined && attributes[GEN_AI_ATTRIBUTES.system] === undefined) {\n attributes[GEN_AI_ATTRIBUTES.system] = options.system;\n }\n\n otelSpan.setAttributes(attributes);\n}\n\n/**\n * Map the Panoptic span status onto the OTel span status, recording the\n * normalized error as an exception event + ERROR status when present.\n */\nfunction applyStatus(otelSpan: Span, span: TraceSpan): void {\n const codes: typeof SpanStatusCodeEnum = OtelApi.SpanStatusCode;\n\n if (span.error) {\n otelSpan.recordException({\n name: span.error.type,\n message: span.error.message,\n stack: span.error.stack,\n });\n }\n\n if (span.status === \"failed\" || span.status === \"cancelled\") {\n otelSpan.setStatus({\n code: codes.ERROR,\n message: span.error?.message,\n });\n return;\n }\n\n // A capped / paused run is neither a failure nor a clean success.\n // Mapping it to OK would let a hit iteration cap read as a healthy\n // run; leave the OTel status UNSET with a descriptive message so the\n // outcome is visible without being miscounted as an error.\n if (span.status === \"max-iterations\" || span.status === \"awaiting-input\") {\n otelSpan.setStatus({\n code: codes.UNSET,\n message:\n span.status === \"max-iterations\"\n ? \"Run hit the iteration cap without an explicit end\"\n : \"Run is awaiting the next input turn\",\n });\n return;\n }\n\n otelSpan.setStatus({ code: codes.OK });\n}\n\n/**\n * Convert an ISO-8601 timestamp to epoch milliseconds — the `TimeInput`\n * form OTel's `startSpan` / `Span.end` accept directly.\n */\nfunction toEpochMillis(isoTimestamp: string): number {\n return new Date(isoTimestamp).getTime();\n}\n"],"mappings":";;;AAUA,MAAM,gBAAgB;AACtB,MAAM,sBAAsB;AAM5B,IAAI;AACJ,IAAI,iBAAiC;AACrC,IAAI;AAEJ,MAAM,4BAA4B;;;;;;;;;;EAUhC,KAAK;;;;;;;AAQP,SAAS,WAA0B;CACjC,IAAI,mBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,UAAU,MAAM,OAAO;GACvB,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,aAAa,UAA+B,CAAC,GAAqB;CAChF,SAAS;CAET,OAAO;EACL,MAAM;EACN,MAAM,OAAO,OAA6B;GACxC,MAAM,SAAS;GAEf,IAAI,CAAC,gBACH,MAAM,IAAI,MAAM,yBAAyB;GAI3C,SADe,cAAc,OACf,GAAG,MAAM,MAAM,QAAW,OAAO;EACjD;CACF;AACF;;;;;AAMA,SAAS,cAAc,SAAsC;CAC3D,IAAI,QAAQ,QACV,OAAO,QAAQ;CAGjB,OAAO,QAAQ,MAAM,UACnB,QAAQ,cAAc,qBACtB,QAAQ,aACV;AACF;;;;;;;;AASA,SAAS,SACP,QACA,MACA,eACA,SACM;CACN,MAAM,YAAY,cAAc,KAAK,SAAS;CAC9C,MAAM,cAAc,iBAAiB,QAAQ,QAAQ,OAAO;CAE5D,MAAM,WAAW,OAAO,UAAU,KAAK,MAAM,EAAE,UAAU,GAAG,WAAW;CAEvE,gBAAgB,UAAU,MAAM,OAAO;CACvC,YAAY,UAAU,IAAI;CAE1B,MAAM,eAAe,QAAQ,MAAM,QAAQ,aAAa,QAAQ;CAEhE,KAAK,MAAM,SAAS,KAAK,UACvB,SAAS,QAAQ,OAAO,cAAc,OAAO;CAG/C,SAAS,IAAI,cAAc,KAAK,OAAO,CAAC;AAC1C;;;;;AAMA,SAAS,gBACP,UACA,MACA,SACM;CACN,MAAM,aAAa,kBAAkB,IAAI;CAEzC,IAAI,QAAQ,WAAW,UAAa,WAAW,kBAAkB,YAAY,QAC3E,WAAW,kBAAkB,UAAU,QAAQ;CAGjD,SAAS,cAAc,UAAU;AACnC;;;;;AAMA,SAAS,YAAY,UAAgB,MAAuB;CAC1D,MAAM,QAAmC,QAAQ;CAEjD,IAAI,KAAK,OACP,SAAS,gBAAgB;EACvB,MAAM,KAAK,MAAM;EACjB,SAAS,KAAK,MAAM;EACpB,OAAO,KAAK,MAAM;CACpB,CAAC;CAGH,IAAI,KAAK,WAAW,YAAY,KAAK,WAAW,aAAa;EAC3D,SAAS,UAAU;GACjB,MAAM,MAAM;GACZ,SAAS,KAAK,OAAO;EACvB,CAAC;EACD;CACF;CAMA,IAAI,KAAK,WAAW,oBAAoB,KAAK,WAAW,kBAAkB;EACxE,SAAS,UAAU;GACjB,MAAM,MAAM;GACZ,SACE,KAAK,WAAW,mBACZ,sDACA;EACR,CAAC;EACD;CACF;CAEA,SAAS,UAAU,EAAE,MAAM,MAAM,GAAG,CAAC;AACvC;;;;;AAMA,SAAS,cAAc,cAA8B;CACnD,OAAO,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC"}
1
+ {"version":3,"file":"otel-exporter.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/otel/otel-exporter.ts"],"sourcesContent":["import type {\n Context,\n Span,\n SpanStatusCode as SpanStatusCodeEnum,\n Tracer,\n} from \"@opentelemetry/api\";\nimport type { ExporterContract, Trace, TraceSpan } from \"../../contracts\";\nimport { toGenAiAttributes, GEN_AI_ATTRIBUTES } from \"../utils\";\nimport type { OtelExporterOptions } from \"./otel-exporter.type\";\n\nconst EXPORTER_NAME = \"otel\";\nconst DEFAULT_TRACER_NAME = \"@warlock.js/ai-panoptic\";\n\n// ============================================================\n// Lazily-loaded @opentelemetry/api (OPTIONAL peer)\n// ============================================================\n\nlet OtelApi: typeof import(\"@opentelemetry/api\");\nlet isModuleExists: boolean | null = null;\nlet loadingPromise: Promise<void> | undefined;\n\nconst OTEL_INSTALL_INSTRUCTIONS = `\nThe Panoptic OpenTelemetry exporter requires the @opentelemetry/api package.\nInstall it with:\n\n npm install @opentelemetry/api\n\nOr with your preferred package manager:\n\n pnpm add @opentelemetry/api\n yarn add @opentelemetry/api\n`.trim();\n\n/**\n * Settle the lazy import of `@opentelemetry/api` once, concurrency-safe.\n * A bare `catch` flips the flag to `false`; the curated install string\n * surfaces at use time so a missing SDK never throws a raw module\n * resolution error.\n */\nfunction loadOtel(): Promise<void> {\n if (isModuleExists !== null) {\n return Promise.resolve();\n }\n\n if (loadingPromise) {\n return loadingPromise;\n }\n\n loadingPromise = (async () => {\n try {\n OtelApi = await import(\"@opentelemetry/api\");\n isModuleExists = true;\n } catch {\n isModuleExists = false;\n }\n })();\n\n return loadingPromise;\n}\n\n/**\n * {@link ExporterContract} that maps Panoptic traces onto OpenTelemetry\n * spans following the GenAI semantic conventions (`gen_ai.*`\n * attributes). Lazily imports `@opentelemetry/api` so it stays an\n * OPTIONAL peer — importing this module never forces the SDK to be\n * installed, and a missing SDK surfaces as a curated \"install this\"\n * error on first `export`, not a boot-time stack trace.\n *\n * The exporter emits onto a `Tracer` you supply (or fetches one from the\n * globally registered provider). It never configures the SDK — wiring a\n * `TracerProvider`, processors, and span exporters is the host app's\n * job, exactly as with any other OTel instrumentation.\n *\n * Each {@link TraceSpan} becomes one OTel span with the source span's\n * start/end times and parent relationship reconstructed, so the emitted\n * tree matches the original execution tree.\n *\n * @example\n * // app already set up @opentelemetry/sdk-trace-base + a provider\n * collector.use(otelExporter({ tracerName: \"my-app\", system: \"openai\" }));\n */\nexport function otelExporter(options: OtelExporterOptions = {}): ExporterContract {\n loadOtel();\n\n return {\n name: EXPORTER_NAME,\n async export(trace: Trace): Promise<void> {\n await loadOtel();\n\n if (!isModuleExists) {\n throw new Error(OTEL_INSTALL_INSTRUCTIONS);\n }\n\n const tracer = resolveTracer(options);\n emitSpan(tracer, trace.root, undefined, options);\n },\n };\n}\n\n/**\n * Resolve the `Tracer` spans are emitted on — the caller-supplied one,\n * or one fetched from the globally registered provider by name.\n */\nfunction resolveTracer(options: OtelExporterOptions): Tracer {\n if (options.tracer) {\n return options.tracer;\n }\n\n return OtelApi.trace.getTracer(\n options.tracerName ?? DEFAULT_TRACER_NAME,\n options.tracerVersion,\n );\n}\n\n/**\n * Recreate one {@link TraceSpan} (and its subtree) as OTel spans. The\n * span is started with the source `startedAt`, parented under\n * `parentContext` so the tree is preserved, annotated with GenAI\n * attributes, given the mapped status, and ended at `endedAt`. Children\n * recurse under this span's context.\n */\nfunction emitSpan(\n tracer: Tracer,\n span: TraceSpan,\n parentContext: Context | undefined,\n options: OtelExporterOptions,\n): void {\n const startTime = toEpochMillis(span.startedAt);\n const baseContext = parentContext ?? OtelApi.context.active();\n\n const otelSpan = tracer.startSpan(span.name, { startTime }, baseContext);\n\n applyAttributes(otelSpan, span, options);\n applyStatus(otelSpan, span);\n\n const childContext = OtelApi.trace.setSpan(baseContext, otelSpan);\n\n for (const child of span.children) {\n emitSpan(tracer, child, childContext, options);\n }\n\n otelSpan.end(toEpochMillis(span.endedAt));\n}\n\n/**\n * Set the GenAI + Warlock attributes on the OTel span, defaulting\n * `gen_ai.system` from the exporter options when the span carried none.\n */\nfunction applyAttributes(\n otelSpan: Span,\n span: TraceSpan,\n options: OtelExporterOptions,\n): void {\n const attributes = toGenAiAttributes(span);\n\n if (options.system !== undefined && attributes[GEN_AI_ATTRIBUTES.system] === undefined) {\n attributes[GEN_AI_ATTRIBUTES.system] = options.system;\n }\n\n // Captured content (only present under `captureContent`) maps onto the\n // GenAI prompt/completion attributes, stringified since OTel attribute\n // values must be primitives.\n if (span.input !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.prompt] = stringifyContent(span.input);\n }\n\n if (span.output !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.completion] = stringifyContent(span.output);\n }\n\n otelSpan.setAttributes(attributes);\n}\n\n/**\n * Coerce a captured content value to a string OTel attribute. Strings\n * pass through; structured values are JSON-encoded (falling back to\n * `String()` if they can't be serialized).\n */\nfunction stringifyContent(value: unknown): string {\n if (typeof value === \"string\") {\n return value;\n }\n\n try {\n return JSON.stringify(value) ?? String(value);\n } catch {\n return String(value);\n }\n}\n\n/**\n * Map the Panoptic span status onto the OTel span status, recording the\n * normalized error as an exception event + ERROR status when present.\n */\nfunction applyStatus(otelSpan: Span, span: TraceSpan): void {\n const codes: typeof SpanStatusCodeEnum = OtelApi.SpanStatusCode;\n\n if (span.error) {\n otelSpan.recordException({\n name: span.error.type,\n message: span.error.message,\n stack: span.error.stack,\n });\n }\n\n if (span.status === \"failed\" || span.status === \"cancelled\") {\n otelSpan.setStatus({\n code: codes.ERROR,\n message: span.error?.message,\n });\n return;\n }\n\n // A capped / paused run is neither a failure nor a clean success.\n // Mapping it to OK would let a hit iteration cap read as a healthy\n // run; leave the OTel status UNSET with a descriptive message so the\n // outcome is visible without being miscounted as an error.\n if (span.status === \"max-iterations\" || span.status === \"awaiting-input\") {\n otelSpan.setStatus({\n code: codes.UNSET,\n message:\n span.status === \"max-iterations\"\n ? \"Run hit the iteration cap without an explicit end\"\n : \"Run is awaiting the next input turn\",\n });\n return;\n }\n\n otelSpan.setStatus({ code: codes.OK });\n}\n\n/**\n * Convert an ISO-8601 timestamp to epoch milliseconds — the `TimeInput`\n * form OTel's `startSpan` / `Span.end` accept directly.\n */\nfunction toEpochMillis(isoTimestamp: string): number {\n return new Date(isoTimestamp).getTime();\n}\n"],"mappings":";;;AAUA,MAAM,gBAAgB;AACtB,MAAM,sBAAsB;AAM5B,IAAI;AACJ,IAAI,iBAAiC;AACrC,IAAI;AAEJ,MAAM,4BAA4B;;;;;;;;;;EAUhC,KAAK;;;;;;;AAQP,SAAS,WAA0B;CACjC,IAAI,mBAAmB,MACrB,OAAO,QAAQ,QAAQ;CAGzB,IAAI,gBACF,OAAO;CAGT,kBAAkB,YAAY;EAC5B,IAAI;GACF,UAAU,MAAM,OAAO;GACvB,iBAAiB;EACnB,QAAQ;GACN,iBAAiB;EACnB;CACF,EAAC,CAAE;CAEH,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;AAuBA,SAAgB,aAAa,UAA+B,CAAC,GAAqB;CAChF,SAAS;CAET,OAAO;EACL,MAAM;EACN,MAAM,OAAO,OAA6B;GACxC,MAAM,SAAS;GAEf,IAAI,CAAC,gBACH,MAAM,IAAI,MAAM,yBAAyB;GAI3C,SADe,cAAc,OACf,GAAG,MAAM,MAAM,QAAW,OAAO;EACjD;CACF;AACF;;;;;AAMA,SAAS,cAAc,SAAsC;CAC3D,IAAI,QAAQ,QACV,OAAO,QAAQ;CAGjB,OAAO,QAAQ,MAAM,UACnB,QAAQ,cAAc,qBACtB,QAAQ,aACV;AACF;;;;;;;;AASA,SAAS,SACP,QACA,MACA,eACA,SACM;CACN,MAAM,YAAY,cAAc,KAAK,SAAS;CAC9C,MAAM,cAAc,iBAAiB,QAAQ,QAAQ,OAAO;CAE5D,MAAM,WAAW,OAAO,UAAU,KAAK,MAAM,EAAE,UAAU,GAAG,WAAW;CAEvE,gBAAgB,UAAU,MAAM,OAAO;CACvC,YAAY,UAAU,IAAI;CAE1B,MAAM,eAAe,QAAQ,MAAM,QAAQ,aAAa,QAAQ;CAEhE,KAAK,MAAM,SAAS,KAAK,UACvB,SAAS,QAAQ,OAAO,cAAc,OAAO;CAG/C,SAAS,IAAI,cAAc,KAAK,OAAO,CAAC;AAC1C;;;;;AAMA,SAAS,gBACP,UACA,MACA,SACM;CACN,MAAM,aAAa,kBAAkB,IAAI;CAEzC,IAAI,QAAQ,WAAW,UAAa,WAAW,kBAAkB,YAAY,QAC3E,WAAW,kBAAkB,UAAU,QAAQ;CAMjD,IAAI,KAAK,UAAU,QACjB,WAAW,kBAAkB,UAAU,iBAAiB,KAAK,KAAK;CAGpE,IAAI,KAAK,WAAW,QAClB,WAAW,kBAAkB,cAAc,iBAAiB,KAAK,MAAM;CAGzE,SAAS,cAAc,UAAU;AACnC;;;;;;AAOA,SAAS,iBAAiB,OAAwB;CAChD,IAAI,OAAO,UAAU,UACnB,OAAO;CAGT,IAAI;EACF,OAAO,KAAK,UAAU,KAAK,KAAK,OAAO,KAAK;CAC9C,QAAQ;EACN,OAAO,OAAO,KAAK;CACrB;AACF;;;;;AAMA,SAAS,YAAY,UAAgB,MAAuB;CAC1D,MAAM,QAAmC,QAAQ;CAEjD,IAAI,KAAK,OACP,SAAS,gBAAgB;EACvB,MAAM,KAAK,MAAM;EACjB,SAAS,KAAK,MAAM;EACpB,OAAO,KAAK,MAAM;CACpB,CAAC;CAGH,IAAI,KAAK,WAAW,YAAY,KAAK,WAAW,aAAa;EAC3D,SAAS,UAAU;GACjB,MAAM,MAAM;GACZ,SAAS,KAAK,OAAO;EACvB,CAAC;EACD;CACF;CAMA,IAAI,KAAK,WAAW,oBAAoB,KAAK,WAAW,kBAAkB;EACxE,SAAS,UAAU;GACjB,MAAM,MAAM;GACZ,SACE,KAAK,WAAW,mBACZ,sDACA;EACR,CAAC;EACD;CACF;CAEA,SAAS,UAAU,EAAE,MAAM,MAAM,GAAG,CAAC;AACvC;;;;;AAMA,SAAS,cAAc,cAA8B;CACnD,OAAO,IAAI,KAAK,YAAY,CAAC,CAAC,QAAQ;AACxC"}
@@ -15,7 +15,9 @@ declare const GEN_AI_ATTRIBUTES: {
15
15
  readonly responseModel: "gen_ai.response.model";
16
16
  readonly usageInputTokens: "gen_ai.usage.input_tokens";
17
17
  readonly usageOutputTokens: "gen_ai.usage.output_tokens";
18
- readonly conversationId: "gen_ai.conversation.id";
18
+ readonly conversationId: "gen_ai.conversation.id"; /** Captured prompt/input (set only under content capture). */
19
+ readonly prompt: "gen_ai.prompt"; /** Captured completion/output (set only under content capture). */
20
+ readonly completion: "gen_ai.completion";
19
21
  };
20
22
  /**
21
23
  * Panoptic-specific attribute keys that have no GenAI-convention
@@ -1 +1 @@
1
- {"version":3,"file":"gen-ai-attributes.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/utils/gen-ai-attributes.ts"],"mappings":";;;;;AAWA;;;;;cAAa,iBAAA;EAAA;;;;;;;;;;;;;cAeA,kBAAA;EAAA;;;;;;;;;;AAea;AAwB1B;;KAxBY,cAAA;;;;;;;;;;;AAwBqE;;;;;;;;;;;;iBAAjE,iBAAA,CAAkB,IAAA,EAAM,SAAA,GAAY,MAAA,SAAe,cAAA"}
1
+ {"version":3,"file":"gen-ai-attributes.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/utils/gen-ai-attributes.ts"],"mappings":";;;;;AAWA;;;;;cAAa,iBAAA;EAAA;;;;;;;oCAmBA;EAAA;;;;;;;cAAA,kBAAA;EAAA;;;;;;;;AAea;AAwB1B;;;;AAxB0B,KAAd,cAAA;;;;;;;;;AAwBqE;;;;;;;;;;;;;;iBAAjE,iBAAA,CAAkB,IAAA,EAAM,SAAA,GAAY,MAAA,SAAe,cAAA"}
@@ -16,7 +16,11 @@ const GEN_AI_ATTRIBUTES = {
16
16
  responseModel: "gen_ai.response.model",
17
17
  usageInputTokens: "gen_ai.usage.input_tokens",
18
18
  usageOutputTokens: "gen_ai.usage.output_tokens",
19
- conversationId: "gen_ai.conversation.id"
19
+ conversationId: "gen_ai.conversation.id",
20
+ /** Captured prompt/input (set only under content capture). */
21
+ prompt: "gen_ai.prompt",
22
+ /** Captured completion/output (set only under content capture). */
23
+ completion: "gen_ai.completion"
20
24
  };
21
25
  /**
22
26
  * Panoptic-specific attribute keys that have no GenAI-convention
@@ -1 +1 @@
1
- {"version":3,"file":"gen-ai-attributes.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/utils/gen-ai-attributes.ts"],"sourcesContent":["import type { TraceSpan } from \"../../contracts\";\nimport { totalCostUsd } from \"./total-cost\";\n\n/**\n * Subset of the OpenTelemetry GenAI semantic-convention attribute keys\n * Panoptic emits. Kept as a named constant map (not inline string\n * literals scattered through the mapper) so the convention names live in\n * one place and a convention bump is a single edit.\n *\n * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/\n */\nexport const GEN_AI_ATTRIBUTES = {\n operationName: \"gen_ai.operation.name\",\n system: \"gen_ai.system\",\n requestModel: \"gen_ai.request.model\",\n responseModel: \"gen_ai.response.model\",\n usageInputTokens: \"gen_ai.usage.input_tokens\",\n usageOutputTokens: \"gen_ai.usage.output_tokens\",\n conversationId: \"gen_ai.conversation.id\",\n} as const;\n\n/**\n * Panoptic-specific attribute keys that have no GenAI-convention\n * equivalent. Namespaced under `warlock.*` so they never collide with a\n * future `gen_ai.*` key the spec might add.\n */\nexport const WARLOCK_ATTRIBUTES = {\n reportType: \"warlock.report.type\",\n version: \"warlock.version\",\n durationMs: \"warlock.duration_ms\",\n totalTokens: \"gen_ai.usage.total_tokens\",\n cachedTokens: \"gen_ai.usage.cached_tokens\",\n reasoningTokens: \"gen_ai.usage.reasoning_tokens\",\n costUsd: \"warlock.cost.usd\",\n} as const;\n\n/**\n * Span attribute values an OpenTelemetry / Langfuse backend accepts.\n * GenAI attributes are scalars; the framework's free-form\n * `TraceSpan.attributes` may also carry these.\n */\nexport type AttributeValue = string | number | boolean;\n\n/**\n * Project a {@link TraceSpan} onto the OpenTelemetry GenAI\n * semantic-convention attribute set.\n *\n * The vendor-neutral {@link TraceSpan} carries identity, timing, status,\n * and rolled-up `usage` as first-class fields; model identity and other\n * provider detail live in the free-form `attributes` bag the collector\n * populated. This mapper folds both into a flat `gen_ai.*` /\n * `warlock.*` attribute map ready to set on an OTel span or hand to a\n * Langfuse generation.\n *\n * - `gen_ai.operation.name` / `gen_ai.system` / `gen_ai.request.model`\n * are forwarded from the span's `attributes` when the collector set\n * them; never invented here.\n * - Token counts come from the span's typed `usage` rollup.\n * - The free-form `attributes` are merged last so an explicit collector\n * value wins over a derived one.\n *\n * @example\n * const attributes = toGenAiAttributes(span);\n * // { \"gen_ai.usage.input_tokens\": 150, \"gen_ai.usage.output_tokens\": 320, ... }\n */\nexport function toGenAiAttributes(span: TraceSpan): Record<string, AttributeValue> {\n const attributes: Record<string, AttributeValue> = {\n [WARLOCK_ATTRIBUTES.reportType]: span.type,\n [WARLOCK_ATTRIBUTES.durationMs]: span.duration,\n [WARLOCK_ATTRIBUTES.totalTokens]: span.usage.total,\n [GEN_AI_ATTRIBUTES.usageInputTokens]: span.usage.input,\n [GEN_AI_ATTRIBUTES.usageOutputTokens]: span.usage.output,\n };\n\n if (span.version !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.version] = span.version;\n }\n\n if (span.sessionId !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.conversationId] = span.sessionId;\n }\n\n if (span.usage.cachedTokens !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.cachedTokens] = span.usage.cachedTokens;\n }\n\n if (span.usage.reasoningTokens !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.reasoningTokens] = span.usage.reasoningTokens;\n }\n\n const cost = totalCostUsd(span.usage);\n\n if (cost !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.costUsd] = cost;\n }\n\n mergeScalarAttributes(attributes, span.attributes);\n\n return attributes;\n}\n\n/**\n * Copy the scalar entries of a free-form attribute bag onto the target\n * map. Non-scalar values (objects, arrays, functions) are skipped — OTel\n * and Langfuse attribute values must be primitives, and the collector's\n * bag may legitimately hold nested digests that don't belong on a span\n * attribute. Explicit collector values overwrite derived ones.\n */\nfunction mergeScalarAttributes(\n target: Record<string, AttributeValue>,\n source: Record<string, unknown> | undefined,\n): void {\n if (!source) {\n return;\n }\n\n for (const [key, value] of Object.entries(source)) {\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n target[key] = value;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,oBAAoB;CAC/B,eAAe;CACf,QAAQ;CACR,cAAc;CACd,eAAe;CACf,kBAAkB;CAClB,mBAAmB;CACnB,gBAAgB;AAClB;;;;;;AAOA,MAAa,qBAAqB;CAChC,YAAY;CACZ,SAAS;CACT,YAAY;CACZ,aAAa;CACb,cAAc;CACd,iBAAiB;CACjB,SAAS;AACX;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,kBAAkB,MAAiD;CACjF,MAAM,aAA6C;GAChD,mBAAmB,aAAa,KAAK;GACrC,mBAAmB,aAAa,KAAK;GACrC,mBAAmB,cAAc,KAAK,MAAM;GAC5C,kBAAkB,mBAAmB,KAAK,MAAM;GAChD,kBAAkB,oBAAoB,KAAK,MAAM;CACpD;CAEA,IAAI,KAAK,YAAY,QACnB,WAAW,mBAAmB,WAAW,KAAK;CAGhD,IAAI,KAAK,cAAc,QACrB,WAAW,kBAAkB,kBAAkB,KAAK;CAGtD,IAAI,KAAK,MAAM,iBAAiB,QAC9B,WAAW,mBAAmB,gBAAgB,KAAK,MAAM;CAG3D,IAAI,KAAK,MAAM,oBAAoB,QACjC,WAAW,mBAAmB,mBAAmB,KAAK,MAAM;CAG9D,MAAM,OAAO,aAAa,KAAK,KAAK;CAEpC,IAAI,SAAS,QACX,WAAW,mBAAmB,WAAW;CAG3C,sBAAsB,YAAY,KAAK,UAAU;CAEjD,OAAO;AACT;;;;;;;;AASA,SAAS,sBACP,QACA,QACM;CACN,IAAI,CAAC,QACH;CAGF,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO,OAAO;AAGpB"}
1
+ {"version":3,"file":"gen-ai-attributes.mjs","names":[],"sources":["../../../../../../../../@warlock.js/ai-panoptic/src/exporters/utils/gen-ai-attributes.ts"],"sourcesContent":["import type { TraceSpan } from \"../../contracts\";\nimport { totalCostUsd } from \"./total-cost\";\n\n/**\n * Subset of the OpenTelemetry GenAI semantic-convention attribute keys\n * Panoptic emits. Kept as a named constant map (not inline string\n * literals scattered through the mapper) so the convention names live in\n * one place and a convention bump is a single edit.\n *\n * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/\n */\nexport const GEN_AI_ATTRIBUTES = {\n operationName: \"gen_ai.operation.name\",\n system: \"gen_ai.system\",\n requestModel: \"gen_ai.request.model\",\n responseModel: \"gen_ai.response.model\",\n usageInputTokens: \"gen_ai.usage.input_tokens\",\n usageOutputTokens: \"gen_ai.usage.output_tokens\",\n conversationId: \"gen_ai.conversation.id\",\n /** Captured prompt/input (set only under content capture). */\n prompt: \"gen_ai.prompt\",\n /** Captured completion/output (set only under content capture). */\n completion: \"gen_ai.completion\",\n} as const;\n\n/**\n * Panoptic-specific attribute keys that have no GenAI-convention\n * equivalent. Namespaced under `warlock.*` so they never collide with a\n * future `gen_ai.*` key the spec might add.\n */\nexport const WARLOCK_ATTRIBUTES = {\n reportType: \"warlock.report.type\",\n version: \"warlock.version\",\n durationMs: \"warlock.duration_ms\",\n totalTokens: \"gen_ai.usage.total_tokens\",\n cachedTokens: \"gen_ai.usage.cached_tokens\",\n reasoningTokens: \"gen_ai.usage.reasoning_tokens\",\n costUsd: \"warlock.cost.usd\",\n} as const;\n\n/**\n * Span attribute values an OpenTelemetry / Langfuse backend accepts.\n * GenAI attributes are scalars; the framework's free-form\n * `TraceSpan.attributes` may also carry these.\n */\nexport type AttributeValue = string | number | boolean;\n\n/**\n * Project a {@link TraceSpan} onto the OpenTelemetry GenAI\n * semantic-convention attribute set.\n *\n * The vendor-neutral {@link TraceSpan} carries identity, timing, status,\n * and rolled-up `usage` as first-class fields; model identity and other\n * provider detail live in the free-form `attributes` bag the collector\n * populated. This mapper folds both into a flat `gen_ai.*` /\n * `warlock.*` attribute map ready to set on an OTel span or hand to a\n * Langfuse generation.\n *\n * - `gen_ai.operation.name` / `gen_ai.system` / `gen_ai.request.model`\n * are forwarded from the span's `attributes` when the collector set\n * them; never invented here.\n * - Token counts come from the span's typed `usage` rollup.\n * - The free-form `attributes` are merged last so an explicit collector\n * value wins over a derived one.\n *\n * @example\n * const attributes = toGenAiAttributes(span);\n * // { \"gen_ai.usage.input_tokens\": 150, \"gen_ai.usage.output_tokens\": 320, ... }\n */\nexport function toGenAiAttributes(span: TraceSpan): Record<string, AttributeValue> {\n const attributes: Record<string, AttributeValue> = {\n [WARLOCK_ATTRIBUTES.reportType]: span.type,\n [WARLOCK_ATTRIBUTES.durationMs]: span.duration,\n [WARLOCK_ATTRIBUTES.totalTokens]: span.usage.total,\n [GEN_AI_ATTRIBUTES.usageInputTokens]: span.usage.input,\n [GEN_AI_ATTRIBUTES.usageOutputTokens]: span.usage.output,\n };\n\n if (span.version !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.version] = span.version;\n }\n\n if (span.sessionId !== undefined) {\n attributes[GEN_AI_ATTRIBUTES.conversationId] = span.sessionId;\n }\n\n if (span.usage.cachedTokens !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.cachedTokens] = span.usage.cachedTokens;\n }\n\n if (span.usage.reasoningTokens !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.reasoningTokens] = span.usage.reasoningTokens;\n }\n\n const cost = totalCostUsd(span.usage);\n\n if (cost !== undefined) {\n attributes[WARLOCK_ATTRIBUTES.costUsd] = cost;\n }\n\n mergeScalarAttributes(attributes, span.attributes);\n\n return attributes;\n}\n\n/**\n * Copy the scalar entries of a free-form attribute bag onto the target\n * map. Non-scalar values (objects, arrays, functions) are skipped — OTel\n * and Langfuse attribute values must be primitives, and the collector's\n * bag may legitimately hold nested digests that don't belong on a span\n * attribute. Explicit collector values overwrite derived ones.\n */\nfunction mergeScalarAttributes(\n target: Record<string, AttributeValue>,\n source: Record<string, unknown> | undefined,\n): void {\n if (!source) {\n return;\n }\n\n for (const [key, value] of Object.entries(source)) {\n if (typeof value === \"string\" || typeof value === \"number\" || typeof value === \"boolean\") {\n target[key] = value;\n }\n }\n}\n"],"mappings":";;;;;;;;;;;AAWA,MAAa,oBAAoB;CAC/B,eAAe;CACf,QAAQ;CACR,cAAc;CACd,eAAe;CACf,kBAAkB;CAClB,mBAAmB;CACnB,gBAAgB;;CAEhB,QAAQ;;CAER,YAAY;AACd;;;;;;AAOA,MAAa,qBAAqB;CAChC,YAAY;CACZ,SAAS;CACT,YAAY;CACZ,aAAa;CACb,cAAc;CACd,iBAAiB;CACjB,SAAS;AACX;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAAgB,kBAAkB,MAAiD;CACjF,MAAM,aAA6C;GAChD,mBAAmB,aAAa,KAAK;GACrC,mBAAmB,aAAa,KAAK;GACrC,mBAAmB,cAAc,KAAK,MAAM;GAC5C,kBAAkB,mBAAmB,KAAK,MAAM;GAChD,kBAAkB,oBAAoB,KAAK,MAAM;CACpD;CAEA,IAAI,KAAK,YAAY,QACnB,WAAW,mBAAmB,WAAW,KAAK;CAGhD,IAAI,KAAK,cAAc,QACrB,WAAW,kBAAkB,kBAAkB,KAAK;CAGtD,IAAI,KAAK,MAAM,iBAAiB,QAC9B,WAAW,mBAAmB,gBAAgB,KAAK,MAAM;CAG3D,IAAI,KAAK,MAAM,oBAAoB,QACjC,WAAW,mBAAmB,mBAAmB,KAAK,MAAM;CAG9D,MAAM,OAAO,aAAa,KAAK,KAAK;CAEpC,IAAI,SAAS,QACX,WAAW,mBAAmB,WAAW;CAG3C,sBAAsB,YAAY,KAAK,UAAU;CAEjD,OAAO;AACT;;;;;;;;AASA,SAAS,sBACP,QACA,QACM;CACN,IAAI,CAAC,QACH;CAGF,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAC9C,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAC7E,OAAO,OAAO;AAGpB"}
package/esm/index.d.mts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Trace, TraceSpan, TraceSpanError } from "./contracts/trace.type.mjs";
2
2
  import { ExporterContract } from "./contracts/exporter.contract.mjs";
3
3
  import { CollectorContract } from "./contracts/collector.contract.mjs";
4
+ import { ContentCaptureOptions, ContentRedactor } from "./collector/content-capture.type.mjs";
4
5
  import { createCollector } from "./collector/collector.mjs";
5
6
  import { reportToTrace } from "./collector/report-to-trace.mjs";
6
7
  import { reportToSpan } from "./collector/report-to-span.mjs";
@@ -15,6 +16,7 @@ import { emptyUsage, sumUsage } from "./store/sum-usage.mjs";
15
16
  import { ConsoleExporterOptions, ConsoleLike } from "./exporters/console/console-exporter.type.mjs";
16
17
  import { consoleExporter } from "./exporters/console/console-exporter.mjs";
17
18
  import { formatSpanLine } from "./exporters/console/format-span-line.mjs";
19
+ import { formatSpanIO } from "./exporters/console/format-span-io.mjs";
18
20
  import { FileExporterOptions, TraceRecord } from "./exporters/file/file-exporter.type.mjs";
19
21
  import { fileExporter } from "./exporters/file/file-exporter.mjs";
20
22
  import { LangfuseClientLike, LangfuseExporterOptions, LangfuseObservationBody, LangfuseObservationEndBody, LangfuseObservationLevel, LangfuseObservationLike, LangfuseTraceBody, LangfuseTraceLike, LangfuseUsageBody } from "./exporters/langfuse/langfuse-exporter.type.mjs";
@@ -28,4 +30,4 @@ import { CompletedEventPayload, PanopticTarget } from "./panoptic/panoptic-targe
28
30
  import { Panoptic, PanopticOptions } from "./panoptic/panoptic.type.mjs";
29
31
  import { panoptic } from "./panoptic/panoptic.mjs";
30
32
  import { createPanopticMiddleware } from "./panoptic/panoptic-middleware.mjs";
31
- export { type AttributeValue, type CollectorContract, type CompletedEventPayload, type ConsoleExporterOptions, type ConsoleLike, type ExporterContract, 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, type OtelExporterOptions, type Panoptic, type PanopticOptions, type PanopticTarget, type Trace, type TraceAggregate, type TraceQuery, type TraceRecord, type TraceSpan, type TraceSpanError, type TraceStoreContract, WARLOCK_ATTRIBUTES, consoleExporter, createCollector, createInMemoryTraceStore, createPanopticMiddleware, emptyUsage, extractSpanAttributes, fileExporter, formatSpanLine, langfuseExporter, matchTrace, normalizeError, otelExporter, panoptic, reportToSpan, reportToTrace, sumUsage, toGenAiAttributes, totalCostUsd, walkSpans };
33
+ export { type AttributeValue, type CollectorContract, type CompletedEventPayload, type ConsoleExporterOptions, type ConsoleLike, type ContentCaptureOptions, type ContentRedactor, type ExporterContract, 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, type OtelExporterOptions, type Panoptic, type PanopticOptions, type PanopticTarget, type Trace, type TraceAggregate, type TraceQuery, type TraceRecord, type TraceSpan, type TraceSpanError, type TraceStoreContract, WARLOCK_ATTRIBUTES, consoleExporter, createCollector, createInMemoryTraceStore, createPanopticMiddleware, emptyUsage, extractSpanAttributes, fileExporter, formatSpanIO, formatSpanLine, langfuseExporter, matchTrace, normalizeError, otelExporter, panoptic, reportToSpan, reportToTrace, sumUsage, toGenAiAttributes, totalCostUsd, walkSpans };
package/esm/index.mjs CHANGED
@@ -10,6 +10,7 @@ import { createInMemoryTraceStore } from "./store/in-memory-trace-store.mjs";
10
10
  import "./store/index.mjs";
11
11
  import { totalCostUsd } from "./exporters/utils/total-cost.mjs";
12
12
  import { GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES, toGenAiAttributes } from "./exporters/utils/gen-ai-attributes.mjs";
13
+ import { formatSpanIO } from "./exporters/console/format-span-io.mjs";
13
14
  import { formatSpanLine } from "./exporters/console/format-span-line.mjs";
14
15
  import { consoleExporter } from "./exporters/console/console-exporter.mjs";
15
16
  import { fileExporter } from "./exporters/file/file-exporter.mjs";
@@ -19,4 +20,4 @@ import "./exporters/index.mjs";
19
20
  import { createPanopticMiddleware } from "./panoptic/panoptic-middleware.mjs";
20
21
  import { panoptic } from "./panoptic/panoptic.mjs";
21
22
 
22
- export { GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES, consoleExporter, createCollector, createInMemoryTraceStore, createPanopticMiddleware, emptyUsage, extractSpanAttributes, fileExporter, formatSpanLine, langfuseExporter, matchTrace, normalizeError, otelExporter, panoptic, reportToSpan, reportToTrace, sumUsage, toGenAiAttributes, totalCostUsd, walkSpans };
23
+ export { GEN_AI_ATTRIBUTES, WARLOCK_ATTRIBUTES, consoleExporter, createCollector, createInMemoryTraceStore, createPanopticMiddleware, emptyUsage, extractSpanAttributes, fileExporter, formatSpanIO, formatSpanLine, langfuseExporter, matchTrace, normalizeError, otelExporter, panoptic, reportToSpan, reportToTrace, sumUsage, toGenAiAttributes, totalCostUsd, walkSpans };
@@ -1 +1 @@
1
- {"version":3,"file":"panoptic.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/panoptic/panoptic.ts"],"mappings":";;;;;AAwLA;;;;;;;;AAAiE;;;;;;;;;;;;;;;;;;;;iBAAjD,QAAA,CAAS,OAAA,GAAS,eAAA,GAAuB,QAAQ"}
1
+ {"version":3,"file":"panoptic.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/panoptic/panoptic.ts"],"mappings":";;;;;AA6LA;;;;;;;;AAAiE;;;;;;;;;;;;;;;;;;;;iBAAjD,QAAA,CAAS,OAAA,GAAS,eAAA,GAAuB,QAAQ"}
@@ -26,7 +26,10 @@ const DEFAULT_COMPLETED_EVENTS = [
26
26
  */
27
27
  var PanopticSubscriber = class {
28
28
  constructor(options = {}) {
29
- this.collector = options.collector ?? createCollector();
29
+ this.collector = options.collector ?? createCollector({
30
+ captureContent: options.captureContent,
31
+ redactContent: options.redactContent
32
+ });
30
33
  for (const exporter of options.exporters ?? []) this.collector.use(exporter);
31
34
  this.completedEvents = options.completedEvents ?? [...DEFAULT_COMPLETED_EVENTS];
32
35
  this.middlewareName = options.middlewareName ?? "panoptic";
@@ -1 +1 @@
1
- {"version":3,"file":"panoptic.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/panoptic/panoptic.ts"],"sourcesContent":["import type { AgentMiddleware, BaseReport } from \"@warlock.js/ai\";\nimport { createCollector } from \"../collector/collector\";\nimport type { CollectorContract } from \"../contracts/collector.contract\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { createPanopticMiddleware } from \"./panoptic-middleware\";\nimport type { CompletedEventPayload, PanopticTarget } from \"./panoptic-target.type\";\nimport type { Panoptic, PanopticOptions } from \"./panoptic.type\";\n\n/**\n * Terminal `*.completed` events of every core primitive that carries the\n * finalized `result` (and therefore the `report` tree). These fire once\n * per run regardless of outcome — the matching `*.error` event fires\n * first on failure, then `*.completed` still fires — so subscribing here\n * captures completed, failed, and cancelled runs alike.\n *\n * The orchestrator is intentionally absent: its `orchestrator.turn.*`\n * events carry only session identity, not a result. Collect an\n * orchestrator turn via {@link Panoptic.collect} with\n * `result.report` instead.\n */\nconst DEFAULT_COMPLETED_EVENTS = [\n \"agent.completed\",\n \"workflow.completed\",\n \"supervisor.completed\",\n] as const;\n\n/**\n * The Panoptic subscriber — binds a collector + its exporters to the\n * three feed paths (events, middleware, direct). Instantiated via\n * {@link panoptic}; callers never see `new`.\n */\nclass PanopticSubscriber implements Panoptic {\n public readonly collector: CollectorContract;\n\n private readonly completedEvents: string[];\n\n private readonly middlewareName: string;\n\n public constructor(options: PanopticOptions = {}) {\n this.collector = options.collector ?? createCollector();\n\n for (const exporter of options.exporters ?? []) {\n this.collector.use(exporter);\n }\n\n this.completedEvents =\n options.completedEvents ?? [...DEFAULT_COMPLETED_EVENTS];\n this.middlewareName = options.middlewareName ?? \"panoptic\";\n }\n\n public use(exporter: ExporterContract): Panoptic {\n this.collector.use(exporter);\n\n return this;\n }\n\n public attach(target: PanopticTarget): () => void {\n const unsubscribes: Array<() => void> = [];\n\n for (const event of this.completedEvents) {\n const unsubscribe = target.on(event, (payload) => {\n this.handleCompleted(payload);\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe();\n }\n };\n }\n\n public middleware(): AgentMiddleware {\n return createPanopticMiddleware(this.collector, this.middlewareName);\n }\n\n public async collect(report: BaseReport): Promise<void> {\n await this.collector.collect(report);\n }\n\n public toTrace(report: BaseReport): Trace {\n return this.collector.toTrace(report);\n }\n\n public async flush(): Promise<void> {\n await this.collector.flush();\n }\n\n public async shutdown(): Promise<void> {\n await this.collector.shutdown();\n }\n\n /**\n * Project one terminal `*.completed` payload's report into the\n * collector. The fan-out is fire-and-forget relative to the emitting\n * run: the core swallows handler errors, the collector isolates\n * exporter failures, and we additionally guard the rejection here so an\n * observability fault never escapes the event handler.\n */\n private handleCompleted(payload: unknown): void {\n const report = readReport(payload);\n\n if (!report) {\n return;\n }\n\n // The failing run's typed error lives on the result envelope\n // (`BaseResult.error`), never on the report tree — thread it so a\n // failed root span carries its error type/message.\n const rootError = readResultError(payload);\n\n void this.collector.collect(report, rootError).catch(() => {\n // Swallow — see the JSDoc above. Never surface on the run.\n });\n }\n}\n\n/**\n * Read the envelope error off a primitive's completed-event payload\n * (`{ result: { error } }`). The error rides on the result envelope, not\n * the report tree, so the collector needs it separately to populate a\n * failed root span. Returns `undefined` when the run succeeded.\n */\nfunction readResultError(payload: unknown): unknown {\n const result = (payload as Partial<CompletedEventPayload>)?.result;\n\n return (result as { error?: unknown })?.error;\n}\n\n/**\n * Read the `report` tree off a primitive's completed-event payload.\n * Structural (no concrete-type import) so it accepts every primitive's\n * result subtype; returns `undefined` when the payload isn't the\n * expected `{ result: { report } }` shape.\n */\nfunction readReport(payload: unknown): BaseReport | undefined {\n const result = (payload as Partial<CompletedEventPayload>)?.result;\n const report = (result as { report?: unknown })?.report;\n\n if (\n typeof report === \"object\" &&\n report !== null &&\n typeof (report as { runId?: unknown }).runId === \"string\" &&\n typeof (report as { rootRunId?: unknown }).rootRunId === \"string\"\n ) {\n return report as BaseReport;\n }\n\n return undefined;\n}\n\n/**\n * Create a Panoptic subscriber — the one-call entry point that wires the\n * observability pipeline. Pass the exporters you want and Panoptic\n * builds a collector, registers them, and hands back a subscriber you can\n * `attach()` to any agent/workflow/supervisor, install as agent\n * `middleware()`, or feed reports to directly with `collect()`.\n *\n * @example\n * // Attach to a primitive's event stream (captures every run):\n * const observe = panoptic({\n * exporters: [consoleExporter(), otelExporter({ tracerName: \"app\" })],\n * });\n *\n * const agent = ai.agent({ model });\n * const detach = observe.attach(agent);\n *\n * await agent.execute(\"Summarize this\");\n * // ...later, on shutdown:\n * await observe.shutdown();\n *\n * @example\n * // Or wire it through the agent middleware pipeline:\n * const observe = panoptic({ exporters: [langfuseExporter({ ... })] });\n * const agent = ai.agent({ model, middleware: [observe.middleware()] });\n *\n * @example\n * // Orchestrator turns carry no result-bearing event — collect directly:\n * const result = await orchestrator.execute(input, { sessionId });\n * await observe.collect(result.report);\n */\nexport function panoptic(options: PanopticOptions = {}): Panoptic {\n return new PanopticSubscriber(options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqBA,MAAM,2BAA2B;CAC/B;CACA;CACA;AACF;;;;;;AAOA,IAAM,qBAAN,MAA6C;CAO3C,AAAO,YAAY,UAA2B,CAAC,GAAG;EAChD,KAAK,YAAY,QAAQ,aAAa,gBAAgB;EAEtD,KAAK,MAAM,YAAY,QAAQ,aAAa,CAAC,GAC3C,KAAK,UAAU,IAAI,QAAQ;EAG7B,KAAK,kBACH,QAAQ,mBAAmB,CAAC,GAAG,wBAAwB;EACzD,KAAK,iBAAiB,QAAQ,kBAAkB;CAClD;CAEA,AAAO,IAAI,UAAsC;EAC/C,KAAK,UAAU,IAAI,QAAQ;EAE3B,OAAO;CACT;CAEA,AAAO,OAAO,QAAoC;EAChD,MAAM,eAAkC,CAAC;EAEzC,KAAK,MAAM,SAAS,KAAK,iBAAiB;GACxC,MAAM,cAAc,OAAO,GAAG,QAAQ,YAAY;IAChD,KAAK,gBAAgB,OAAO;GAC9B,CAAC;GAED,aAAa,KAAK,WAAW;EAC/B;EAEA,aAAa;GACX,KAAK,MAAM,eAAe,cACxB,YAAY;EAEhB;CACF;CAEA,AAAO,aAA8B;EACnC,OAAO,yBAAyB,KAAK,WAAW,KAAK,cAAc;CACrE;CAEA,MAAa,QAAQ,QAAmC;EACtD,MAAM,KAAK,UAAU,QAAQ,MAAM;CACrC;CAEA,AAAO,QAAQ,QAA2B;EACxC,OAAO,KAAK,UAAU,QAAQ,MAAM;CACtC;CAEA,MAAa,QAAuB;EAClC,MAAM,KAAK,UAAU,MAAM;CAC7B;CAEA,MAAa,WAA0B;EACrC,MAAM,KAAK,UAAU,SAAS;CAChC;;;;;;;;CASA,AAAQ,gBAAgB,SAAwB;EAC9C,MAAM,SAAS,WAAW,OAAO;EAEjC,IAAI,CAAC,QACH;EAMF,MAAM,YAAY,gBAAgB,OAAO;EAEzC,AAAK,KAAK,UAAU,QAAQ,QAAQ,SAAS,CAAC,CAAC,YAAY,CAE3D,CAAC;CACH;AACF;;;;;;;AAQA,SAAS,gBAAgB,SAA2B;CAGlD,QAFgB,SAA4C,OAE9C,EAA0B;AAC1C;;;;;;;AAQA,SAAS,WAAW,SAA0C;CAE5D,MAAM,UADU,SAA4C,OACtC,EAA2B;CAEjD,IACE,OAAO,WAAW,YAClB,WAAW,QACX,OAAQ,OAA+B,UAAU,YACjD,OAAQ,OAAmC,cAAc,UAEzD,OAAO;AAIX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,SAAS,UAA2B,CAAC,GAAa;CAChE,OAAO,IAAI,mBAAmB,OAAO;AACvC"}
1
+ {"version":3,"file":"panoptic.mjs","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/panoptic/panoptic.ts"],"sourcesContent":["import type { AgentMiddleware, BaseReport } from \"@warlock.js/ai\";\nimport { createCollector } from \"../collector/collector\";\nimport type { CollectorContract } from \"../contracts/collector.contract\";\nimport type { ExporterContract } from \"../contracts/exporter.contract\";\nimport type { Trace } from \"../contracts/trace.type\";\nimport { createPanopticMiddleware } from \"./panoptic-middleware\";\nimport type { CompletedEventPayload, PanopticTarget } from \"./panoptic-target.type\";\nimport type { Panoptic, PanopticOptions } from \"./panoptic.type\";\n\n/**\n * Terminal `*.completed` events of every core primitive that carries the\n * finalized `result` (and therefore the `report` tree). These fire once\n * per run regardless of outcome — the matching `*.error` event fires\n * first on failure, then `*.completed` still fires — so subscribing here\n * captures completed, failed, and cancelled runs alike.\n *\n * The orchestrator is intentionally absent: its `orchestrator.turn.*`\n * events carry only session identity, not a result. Collect an\n * orchestrator turn via {@link Panoptic.collect} with\n * `result.report` instead.\n */\nconst DEFAULT_COMPLETED_EVENTS = [\n \"agent.completed\",\n \"workflow.completed\",\n \"supervisor.completed\",\n] as const;\n\n/**\n * The Panoptic subscriber — binds a collector + its exporters to the\n * three feed paths (events, middleware, direct). Instantiated via\n * {@link panoptic}; callers never see `new`.\n */\nclass PanopticSubscriber implements Panoptic {\n public readonly collector: CollectorContract;\n\n private readonly completedEvents: string[];\n\n private readonly middlewareName: string;\n\n public constructor(options: PanopticOptions = {}) {\n this.collector =\n options.collector ??\n createCollector({\n captureContent: options.captureContent,\n redactContent: options.redactContent,\n });\n\n for (const exporter of options.exporters ?? []) {\n this.collector.use(exporter);\n }\n\n this.completedEvents =\n options.completedEvents ?? [...DEFAULT_COMPLETED_EVENTS];\n this.middlewareName = options.middlewareName ?? \"panoptic\";\n }\n\n public use(exporter: ExporterContract): Panoptic {\n this.collector.use(exporter);\n\n return this;\n }\n\n public attach(target: PanopticTarget): () => void {\n const unsubscribes: Array<() => void> = [];\n\n for (const event of this.completedEvents) {\n const unsubscribe = target.on(event, (payload) => {\n this.handleCompleted(payload);\n });\n\n unsubscribes.push(unsubscribe);\n }\n\n return () => {\n for (const unsubscribe of unsubscribes) {\n unsubscribe();\n }\n };\n }\n\n public middleware(): AgentMiddleware {\n return createPanopticMiddleware(this.collector, this.middlewareName);\n }\n\n public async collect(report: BaseReport): Promise<void> {\n await this.collector.collect(report);\n }\n\n public toTrace(report: BaseReport): Trace {\n return this.collector.toTrace(report);\n }\n\n public async flush(): Promise<void> {\n await this.collector.flush();\n }\n\n public async shutdown(): Promise<void> {\n await this.collector.shutdown();\n }\n\n /**\n * Project one terminal `*.completed` payload's report into the\n * collector. The fan-out is fire-and-forget relative to the emitting\n * run: the core swallows handler errors, the collector isolates\n * exporter failures, and we additionally guard the rejection here so an\n * observability fault never escapes the event handler.\n */\n private handleCompleted(payload: unknown): void {\n const report = readReport(payload);\n\n if (!report) {\n return;\n }\n\n // The failing run's typed error lives on the result envelope\n // (`BaseResult.error`), never on the report tree — thread it so a\n // failed root span carries its error type/message.\n const rootError = readResultError(payload);\n\n void this.collector.collect(report, rootError).catch(() => {\n // Swallow — see the JSDoc above. Never surface on the run.\n });\n }\n}\n\n/**\n * Read the envelope error off a primitive's completed-event payload\n * (`{ result: { error } }`). The error rides on the result envelope, not\n * the report tree, so the collector needs it separately to populate a\n * failed root span. Returns `undefined` when the run succeeded.\n */\nfunction readResultError(payload: unknown): unknown {\n const result = (payload as Partial<CompletedEventPayload>)?.result;\n\n return (result as { error?: unknown })?.error;\n}\n\n/**\n * Read the `report` tree off a primitive's completed-event payload.\n * Structural (no concrete-type import) so it accepts every primitive's\n * result subtype; returns `undefined` when the payload isn't the\n * expected `{ result: { report } }` shape.\n */\nfunction readReport(payload: unknown): BaseReport | undefined {\n const result = (payload as Partial<CompletedEventPayload>)?.result;\n const report = (result as { report?: unknown })?.report;\n\n if (\n typeof report === \"object\" &&\n report !== null &&\n typeof (report as { runId?: unknown }).runId === \"string\" &&\n typeof (report as { rootRunId?: unknown }).rootRunId === \"string\"\n ) {\n return report as BaseReport;\n }\n\n return undefined;\n}\n\n/**\n * Create a Panoptic subscriber — the one-call entry point that wires the\n * observability pipeline. Pass the exporters you want and Panoptic\n * builds a collector, registers them, and hands back a subscriber you can\n * `attach()` to any agent/workflow/supervisor, install as agent\n * `middleware()`, or feed reports to directly with `collect()`.\n *\n * @example\n * // Attach to a primitive's event stream (captures every run):\n * const observe = panoptic({\n * exporters: [consoleExporter(), otelExporter({ tracerName: \"app\" })],\n * });\n *\n * const agent = ai.agent({ model });\n * const detach = observe.attach(agent);\n *\n * await agent.execute(\"Summarize this\");\n * // ...later, on shutdown:\n * await observe.shutdown();\n *\n * @example\n * // Or wire it through the agent middleware pipeline:\n * const observe = panoptic({ exporters: [langfuseExporter({ ... })] });\n * const agent = ai.agent({ model, middleware: [observe.middleware()] });\n *\n * @example\n * // Orchestrator turns carry no result-bearing event — collect directly:\n * const result = await orchestrator.execute(input, { sessionId });\n * await observe.collect(result.report);\n */\nexport function panoptic(options: PanopticOptions = {}): Panoptic {\n return new PanopticSubscriber(options);\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAqBA,MAAM,2BAA2B;CAC/B;CACA;CACA;AACF;;;;;;AAOA,IAAM,qBAAN,MAA6C;CAO3C,AAAO,YAAY,UAA2B,CAAC,GAAG;EAChD,KAAK,YACH,QAAQ,aACR,gBAAgB;GACd,gBAAgB,QAAQ;GACxB,eAAe,QAAQ;EACzB,CAAC;EAEH,KAAK,MAAM,YAAY,QAAQ,aAAa,CAAC,GAC3C,KAAK,UAAU,IAAI,QAAQ;EAG7B,KAAK,kBACH,QAAQ,mBAAmB,CAAC,GAAG,wBAAwB;EACzD,KAAK,iBAAiB,QAAQ,kBAAkB;CAClD;CAEA,AAAO,IAAI,UAAsC;EAC/C,KAAK,UAAU,IAAI,QAAQ;EAE3B,OAAO;CACT;CAEA,AAAO,OAAO,QAAoC;EAChD,MAAM,eAAkC,CAAC;EAEzC,KAAK,MAAM,SAAS,KAAK,iBAAiB;GACxC,MAAM,cAAc,OAAO,GAAG,QAAQ,YAAY;IAChD,KAAK,gBAAgB,OAAO;GAC9B,CAAC;GAED,aAAa,KAAK,WAAW;EAC/B;EAEA,aAAa;GACX,KAAK,MAAM,eAAe,cACxB,YAAY;EAEhB;CACF;CAEA,AAAO,aAA8B;EACnC,OAAO,yBAAyB,KAAK,WAAW,KAAK,cAAc;CACrE;CAEA,MAAa,QAAQ,QAAmC;EACtD,MAAM,KAAK,UAAU,QAAQ,MAAM;CACrC;CAEA,AAAO,QAAQ,QAA2B;EACxC,OAAO,KAAK,UAAU,QAAQ,MAAM;CACtC;CAEA,MAAa,QAAuB;EAClC,MAAM,KAAK,UAAU,MAAM;CAC7B;CAEA,MAAa,WAA0B;EACrC,MAAM,KAAK,UAAU,SAAS;CAChC;;;;;;;;CASA,AAAQ,gBAAgB,SAAwB;EAC9C,MAAM,SAAS,WAAW,OAAO;EAEjC,IAAI,CAAC,QACH;EAMF,MAAM,YAAY,gBAAgB,OAAO;EAEzC,AAAK,KAAK,UAAU,QAAQ,QAAQ,SAAS,CAAC,CAAC,YAAY,CAE3D,CAAC;CACH;AACF;;;;;;;AAQA,SAAS,gBAAgB,SAA2B;CAGlD,QAFgB,SAA4C,OAE9C,EAA0B;AAC1C;;;;;;;AAQA,SAAS,WAAW,SAA0C;CAE5D,MAAM,UADU,SAA4C,OACtC,EAA2B;CAEjD,IACE,OAAO,WAAW,YAClB,WAAW,QACX,OAAQ,OAA+B,UAAU,YACjD,OAAQ,OAAmC,cAAc,UAEzD,OAAO;AAIX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,SAAS,UAA2B,CAAC,GAAa;CAChE,OAAO,IAAI,mBAAmB,OAAO;AACvC"}
@@ -1,6 +1,7 @@
1
1
  import { Trace } from "../contracts/trace.type.mjs";
2
2
  import { ExporterContract } from "../contracts/exporter.contract.mjs";
3
3
  import { CollectorContract } from "../contracts/collector.contract.mjs";
4
+ import { ContentRedactor } from "../collector/content-capture.type.mjs";
4
5
  import { PanopticTarget } from "./panoptic-target.type.mjs";
5
6
  import { AgentMiddleware, BaseReport } from "@warlock.js/ai";
6
7
 
@@ -42,6 +43,22 @@ type PanopticOptions = {
42
43
  * registering more than one Panoptic middleware on the same agent.
43
44
  */
44
45
  middlewareName?: string;
46
+ /**
47
+ * Capture raw prompts / responses / tool I/O onto each span's
48
+ * `input` / `output` so exporters can surface them — e.g.
49
+ * `consoleExporter({ io: true })`, the file exporter's JSON, OTel
50
+ * `gen_ai.prompt` / `gen_ai.completion`, Langfuse `input` / `output`.
51
+ * Off by default: payloads are large and often sensitive. Applies only
52
+ * when Panoptic builds the collector — ignored when you inject your own
53
+ * `collector` (configure capture on it via `createCollector`).
54
+ */
55
+ captureContent?: boolean;
56
+ /**
57
+ * Optional masker applied to every captured content value before it is
58
+ * stored — strip secrets / PII when capturing in a sensitive
59
+ * environment. See {@link ContentRedactor}.
60
+ */
61
+ redactContent?: ContentRedactor;
45
62
  };
46
63
  /**
47
64
  * The subscriber returned by {@link panoptic}. Wraps a collector +
@@ -1 +1 @@
1
- {"version":3,"file":"panoptic.type.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/panoptic/panoptic.type.ts"],"mappings":";;;;;;;;;AAcA;;;;;;KAAY,eAAA;EAaE;;;;AAeE;EAtBd,SAAA,GAAY,gBAAA;EA0CM;;;;;;EAnClB,SAAA,GAAY,iBAAiB;EA8Db;;;;;;;;EArDhB,eAAA;EA4BoB;;;;;EAtBpB,cAAA;AAAA;;;;;;;;;;;;;;;;;AAwDmB;KApCT,QAAA;mFAED,SAAA,EAAW,iBAAA;;;;;EAKpB,GAAA,CAAI,QAAA,EAAU,gBAAA,GAAmB,QAAA;;;;;;;EAOjC,MAAA,CAAO,MAAA,EAAQ,cAAA;;;;;;;EAOf,UAAA,IAAc,eAAA;;;;;;EAMd,OAAA,CAAQ,MAAA,EAAQ,UAAA,GAAa,OAAA;;;;;EAK7B,OAAA,CAAQ,MAAA,EAAQ,UAAA,GAAa,KAAA;EAE7B,KAAA,IAAS,OAAA;EAET,QAAA,IAAY,OAAA;AAAA"}
1
+ {"version":3,"file":"panoptic.type.d.mts","names":[],"sources":["../../../../../../../@warlock.js/ai-panoptic/src/panoptic/panoptic.type.ts"],"mappings":";;;;;;;;;;AAeA;;;;;;KAAY,eAAA;EA4CqB;;;;;EAtC/B,SAAA,GAAY,gBAAA;EAsBZ;;;;;AAgB+B;EA/B/B,SAAA,GAAY,iBAAA;EAmDM;;;;;;;;EA1ClB,eAAA;EA0EgB;;;;;EApEhB,cAAA;EAsCS;;;;;;;;;EA5BT,cAAA;EA+Cc;;;;;EAzCd,aAAA,GAAgB,eAAA;AAAA;;;;;;;;AAwDG;;;;;;;;;;KApCT,QAAA;mFAED,SAAA,EAAW,iBAAA;;;;;EAKpB,GAAA,CAAI,QAAA,EAAU,gBAAA,GAAmB,QAAA;;;;;;;EAOjC,MAAA,CAAO,MAAA,EAAQ,cAAA;;;;;;;EAOf,UAAA,IAAc,eAAA;;;;;;EAMd,OAAA,CAAQ,MAAA,EAAQ,UAAA,GAAa,OAAA;;;;;EAK7B,OAAA,CAAQ,MAAA,EAAQ,UAAA,GAAa,KAAA;EAE7B,KAAA,IAAS,OAAA;EAET,QAAA,IAAY,OAAA;AAAA"}
package/package.json CHANGED
@@ -16,12 +16,12 @@
16
16
  "url": "https://github.com/warlockjs/ai-panoptic"
17
17
  },
18
18
  "peerDependencies": {
19
- "@warlock.js/ai": "4.3.0",
19
+ "@warlock.js/ai": "4.4.0",
20
20
  "@opentelemetry/api": "*",
21
21
  "@opentelemetry/sdk-trace-base": "*",
22
22
  "langfuse": "*"
23
23
  },
24
- "version": "4.3.0",
24
+ "version": "4.4.0",
25
25
  "main": "./cjs/index.cjs",
26
26
  "module": "./esm/index.mjs",
27
27
  "types": "./esm/index.d.mts",