@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.
- package/CHANGELOG.md +9 -1
- package/cjs/index.cjs +259 -52
- package/cjs/index.cjs.map +1 -1
- package/esm/collector/collector.d.mts +2 -1
- package/esm/collector/collector.d.mts.map +1 -1
- package/esm/collector/collector.mjs +10 -4
- package/esm/collector/collector.mjs.map +1 -1
- package/esm/collector/content-capture.type.d.mts +42 -0
- package/esm/collector/content-capture.type.d.mts.map +1 -0
- package/esm/collector/index.d.mts +1 -0
- package/esm/collector/report-to-span.d.mts +8 -1
- package/esm/collector/report-to-span.d.mts.map +1 -1
- package/esm/collector/report-to-span.mjs +66 -2
- package/esm/collector/report-to-span.mjs.map +1 -1
- package/esm/collector/report-to-trace.d.mts +2 -1
- package/esm/collector/report-to-trace.d.mts.map +1 -1
- package/esm/collector/report-to-trace.mjs +2 -2
- package/esm/collector/report-to-trace.mjs.map +1 -1
- package/esm/contracts/trace.type.d.mts +22 -3
- package/esm/contracts/trace.type.d.mts.map +1 -1
- package/esm/exporters/console/console-exporter.d.mts +11 -3
- package/esm/exporters/console/console-exporter.d.mts.map +1 -1
- package/esm/exporters/console/console-exporter.mjs +31 -13
- package/esm/exporters/console/console-exporter.mjs.map +1 -1
- package/esm/exporters/console/console-exporter.type.d.mts +15 -0
- package/esm/exporters/console/console-exporter.type.d.mts.map +1 -1
- package/esm/exporters/console/format-span-io.d.mts +27 -0
- package/esm/exporters/console/format-span-io.d.mts.map +1 -0
- package/esm/exporters/console/format-span-io.mjs +52 -0
- package/esm/exporters/console/format-span-io.mjs.map +1 -0
- package/esm/exporters/console/index.d.mts +2 -1
- package/esm/exporters/index.d.mts +1 -0
- package/esm/exporters/index.mjs +1 -0
- package/esm/exporters/langfuse/langfuse-exporter.d.mts +10 -6
- package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -1
- package/esm/exporters/langfuse/langfuse-exporter.mjs +77 -30
- package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -1
- package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +3 -1
- package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -1
- package/esm/exporters/otel/otel-exporter.mjs +15 -0
- package/esm/exporters/otel/otel-exporter.mjs.map +1 -1
- package/esm/exporters/utils/gen-ai-attributes.d.mts +3 -1
- package/esm/exporters/utils/gen-ai-attributes.d.mts.map +1 -1
- package/esm/exporters/utils/gen-ai-attributes.mjs +5 -1
- package/esm/exporters/utils/gen-ai-attributes.mjs.map +1 -1
- package/esm/index.d.mts +3 -1
- package/esm/index.mjs +2 -1
- package/esm/panoptic/panoptic.d.mts.map +1 -1
- package/esm/panoptic/panoptic.mjs +4 -1
- package/esm/panoptic/panoptic.mjs.map +1 -1
- package/esm/panoptic/panoptic.type.d.mts +17 -0
- package/esm/panoptic/panoptic.type.d.mts.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,15 @@ All notable changes to `@warlock.js/ai-panoptic` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 4.4.0 - 2026-06-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Opt-in content capture.** `createCollector` / `panoptic({ captureContent, redactContent })` copies the agent prompt/response (first-trip input, last non-empty trip output) and each tool's args/result onto `TraceSpan.input` / `output`. Surfaced by `consoleExporter({ io, ioMaxChars })` (new `in:` / `out:` lines), the file exporter's JSON, OTel `gen_ai.prompt` / `gen_ai.completion`, and Langfuse native `input` / `output`. Off by default — payloads are large and often sensitive; a `ContentRedactor` masks each value. New exports: `ContentCaptureOptions`, `ContentRedactor`, `formatSpanIO`.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **Langfuse token accounting.** Generations now meter their OWN usage (rolled-up minus children); composite nodes with no own tokens emit as plain spans; the root execution is now emitted as an observation (its own tokens were previously unmetered); and the rolled-up usage/cost is stripped from observation metadata (the `usage` block is authoritative). Together these stop the trace total from double-counting nested spans.
|
|
8
16
|
|
|
9
17
|
## 4.3.0 - 2026-06-21
|
|
10
18
|
|
package/cjs/index.cjs
CHANGED
|
@@ -159,6 +159,12 @@ function addToolAttributes(attributes, extensions) {
|
|
|
159
159
|
* identity, tool trip index) is routed into the optional `attributes`
|
|
160
160
|
* bag via {@link extractSpanAttributes}.
|
|
161
161
|
*
|
|
162
|
+
* When {@link ContentCaptureOptions.captureContent} is set, the raw
|
|
163
|
+
* prompt/response (agents) and args/result (tools) are additionally
|
|
164
|
+
* copied onto `span.input` / `span.output` — off by default because
|
|
165
|
+
* payloads are large and often sensitive. A {@link ContentRedactor} can
|
|
166
|
+
* mask each value first.
|
|
167
|
+
*
|
|
162
168
|
* No external lookup is needed — a `BaseReport` already carries
|
|
163
169
|
* everything a span requires, so a collector can flatten a tree without
|
|
164
170
|
* consulting any other source.
|
|
@@ -167,7 +173,7 @@ function addToolAttributes(attributes, extensions) {
|
|
|
167
173
|
* const root = reportToSpan(result.report);
|
|
168
174
|
* console.log(root.spanId, root.traceId, root.children.length);
|
|
169
175
|
*/
|
|
170
|
-
function reportToSpan(report) {
|
|
176
|
+
function reportToSpan(report, options) {
|
|
171
177
|
const span = {
|
|
172
178
|
spanId: report.runId,
|
|
173
179
|
traceId: report.rootRunId,
|
|
@@ -178,7 +184,7 @@ function reportToSpan(report) {
|
|
|
178
184
|
endedAt: report.endedAt,
|
|
179
185
|
duration: report.duration,
|
|
180
186
|
usage: report.usage,
|
|
181
|
-
children: report.children.map(reportToSpan)
|
|
187
|
+
children: report.children.map((child) => reportToSpan(child, options))
|
|
182
188
|
};
|
|
183
189
|
if (report.parentRunId !== void 0) span.parentSpanId = report.parentRunId;
|
|
184
190
|
if (report.sessionId !== void 0) span.sessionId = report.sessionId;
|
|
@@ -187,8 +193,66 @@ function reportToSpan(report) {
|
|
|
187
193
|
if (error !== void 0) span.error = error;
|
|
188
194
|
const attributes = extractSpanAttributes(report);
|
|
189
195
|
if (attributes !== void 0) span.attributes = attributes;
|
|
196
|
+
if (options?.captureContent) captureContent(span, report, options.redactContent);
|
|
190
197
|
return span;
|
|
191
198
|
}
|
|
199
|
+
/**
|
|
200
|
+
* Copy the node's raw content onto `span.input` / `span.output`.
|
|
201
|
+
*
|
|
202
|
+
* - Tools carry the call arguments + return value directly on the report
|
|
203
|
+
* (`ToolCall.input` / `ToolCall.output`).
|
|
204
|
+
* - Agents carry a `trips[]` history; the first trip's `input` is the
|
|
205
|
+
* real user prompt, and the last NON-EMPTY trip `output` is the final
|
|
206
|
+
* response text. (A failed / max-trips run can end on a trip whose
|
|
207
|
+
* `output` is `""` or tool-call-only, so we scan back for the last one
|
|
208
|
+
* that carried text rather than surfacing an empty string. Intermediate
|
|
209
|
+
* trips also store a `"[tool results]"` placeholder upstream.)
|
|
210
|
+
*
|
|
211
|
+
* Each value is passed through the optional {@link ContentRedactor};
|
|
212
|
+
* a redactor returning `undefined` drops the field.
|
|
213
|
+
*/
|
|
214
|
+
function captureContent(span, report, redact) {
|
|
215
|
+
const node = report;
|
|
216
|
+
let input;
|
|
217
|
+
let output;
|
|
218
|
+
if (report.type === "tool") {
|
|
219
|
+
input = node.input;
|
|
220
|
+
output = node.output;
|
|
221
|
+
} else if (Array.isArray(node.trips) && node.trips.length > 0) {
|
|
222
|
+
input = node.trips[0]?.input;
|
|
223
|
+
output = lastNonEmptyOutput(node.trips);
|
|
224
|
+
}
|
|
225
|
+
if (input !== void 0) {
|
|
226
|
+
const value = redact ? redact(input, {
|
|
227
|
+
name: span.name,
|
|
228
|
+
type: span.type,
|
|
229
|
+
field: "input"
|
|
230
|
+
}) : input;
|
|
231
|
+
if (value !== void 0) span.input = value;
|
|
232
|
+
}
|
|
233
|
+
if (output !== void 0) {
|
|
234
|
+
const value = redact ? redact(output, {
|
|
235
|
+
name: span.name,
|
|
236
|
+
type: span.type,
|
|
237
|
+
field: "output"
|
|
238
|
+
}) : output;
|
|
239
|
+
if (value !== void 0) span.output = value;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* The last trip output that actually carries text — the agent's final
|
|
244
|
+
* response on the happy path. A failed or max-trips run can end on a trip
|
|
245
|
+
* whose `output` is `""` or tool-call-only, so we scan backwards for the
|
|
246
|
+
* last trip that produced text rather than blindly taking the final trip
|
|
247
|
+
* (which would surface an empty string). Returns `undefined` when no trip
|
|
248
|
+
* produced any output.
|
|
249
|
+
*/
|
|
250
|
+
function lastNonEmptyOutput(trips) {
|
|
251
|
+
for (let i = trips.length - 1; i >= 0; i -= 1) {
|
|
252
|
+
const out = trips[i]?.output;
|
|
253
|
+
if (typeof out === "string" ? out.length > 0 : out !== void 0) return out;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
192
256
|
|
|
193
257
|
//#endregion
|
|
194
258
|
//#region ../@warlock.js/ai-panoptic/src/collector/report-to-trace.ts
|
|
@@ -219,8 +283,8 @@ function reportToSpan(report) {
|
|
|
219
283
|
* const trace = reportToTrace(result.report, result.error);
|
|
220
284
|
* console.log(trace.traceId, trace.usage.total, trace.duration);
|
|
221
285
|
*/
|
|
222
|
-
function reportToTrace(report, rootError) {
|
|
223
|
-
const root = reportToSpan(report);
|
|
286
|
+
function reportToTrace(report, rootError, options) {
|
|
287
|
+
const root = reportToSpan(report, options);
|
|
224
288
|
if (root.error === void 0) {
|
|
225
289
|
const error = normalizeError(rootError);
|
|
226
290
|
if (error !== void 0) root.error = error;
|
|
@@ -257,7 +321,13 @@ function reportToTrace(report, rootError) {
|
|
|
257
321
|
* agent loop nor stop sibling exporters from receiving the trace.
|
|
258
322
|
*/
|
|
259
323
|
var Collector = class {
|
|
260
|
-
|
|
324
|
+
/**
|
|
325
|
+
* Content-capture options threaded into every `toTrace` projection.
|
|
326
|
+
* When `captureContent` is set, projected spans carry `input` /
|
|
327
|
+
* `output`; otherwise they stay metadata-only.
|
|
328
|
+
*/
|
|
329
|
+
constructor(options = {}) {
|
|
330
|
+
this.options = options;
|
|
261
331
|
this.exporters = /* @__PURE__ */ new Map();
|
|
262
332
|
}
|
|
263
333
|
use(exporter) {
|
|
@@ -265,7 +335,7 @@ var Collector = class {
|
|
|
265
335
|
return this;
|
|
266
336
|
}
|
|
267
337
|
toTrace(report, rootError) {
|
|
268
|
-
return reportToTrace(report, rootError);
|
|
338
|
+
return reportToTrace(report, rootError, this.options);
|
|
269
339
|
}
|
|
270
340
|
async collect(report, rootError) {
|
|
271
341
|
const trace = this.toTrace(report, rootError);
|
|
@@ -318,8 +388,8 @@ var Collector = class {
|
|
|
318
388
|
* // on shutdown:
|
|
319
389
|
* await collector.shutdown();
|
|
320
390
|
*/
|
|
321
|
-
function createCollector() {
|
|
322
|
-
return new Collector();
|
|
391
|
+
function createCollector(options = {}) {
|
|
392
|
+
return new Collector(options);
|
|
323
393
|
}
|
|
324
394
|
|
|
325
395
|
//#endregion
|
|
@@ -592,7 +662,11 @@ const GEN_AI_ATTRIBUTES = {
|
|
|
592
662
|
responseModel: "gen_ai.response.model",
|
|
593
663
|
usageInputTokens: "gen_ai.usage.input_tokens",
|
|
594
664
|
usageOutputTokens: "gen_ai.usage.output_tokens",
|
|
595
|
-
conversationId: "gen_ai.conversation.id"
|
|
665
|
+
conversationId: "gen_ai.conversation.id",
|
|
666
|
+
/** Captured prompt/input (set only under content capture). */
|
|
667
|
+
prompt: "gen_ai.prompt",
|
|
668
|
+
/** Captured completion/output (set only under content capture). */
|
|
669
|
+
completion: "gen_ai.completion"
|
|
596
670
|
};
|
|
597
671
|
/**
|
|
598
672
|
* Panoptic-specific attribute keys that have no GenAI-convention
|
|
@@ -659,6 +733,56 @@ function mergeScalarAttributes(target, source) {
|
|
|
659
733
|
for (const [key, value] of Object.entries(source)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") target[key] = value;
|
|
660
734
|
}
|
|
661
735
|
|
|
736
|
+
//#endregion
|
|
737
|
+
//#region ../@warlock.js/ai-panoptic/src/exporters/console/format-span-io.ts
|
|
738
|
+
/** Default cap on how many characters of captured content a console line shows. */
|
|
739
|
+
const DEFAULT_IO_MAX_CHARS = 500;
|
|
740
|
+
/**
|
|
741
|
+
* Render a span's captured content ({@link TraceSpan.input} /
|
|
742
|
+
* {@link TraceSpan.output}) as extra indented console lines, one level
|
|
743
|
+
* below the span's own line:
|
|
744
|
+
*
|
|
745
|
+
* ```text
|
|
746
|
+
* ok agent "market-research" — 1794ms, 224 tok, $0.0008
|
|
747
|
+
* in: Research the market for Acme Coffee Roasters …
|
|
748
|
+
* out: Demand is steady; specialty buyers skew premium …
|
|
749
|
+
* ```
|
|
750
|
+
*
|
|
751
|
+
* Returns `[]` when the span carries no content — capture disabled, or a
|
|
752
|
+
* composite node with no own I/O. Each value is stringified (JSON for
|
|
753
|
+
* non-strings), whitespace-collapsed to stay scannable, and truncated to
|
|
754
|
+
* `maxChars` (default {@link DEFAULT_IO_MAX_CHARS}) with an ellipsis. Use
|
|
755
|
+
* the file exporter for the full, untruncated payload.
|
|
756
|
+
*
|
|
757
|
+
* @example
|
|
758
|
+
* formatSpanIO(toolSpan, 1);
|
|
759
|
+
* // [' in: {"query":"specialty coffee demand"}', ' out: {"results":[…]}']
|
|
760
|
+
*/
|
|
761
|
+
function formatSpanIO(span, depth = 0, maxChars = DEFAULT_IO_MAX_CHARS) {
|
|
762
|
+
const lines = [];
|
|
763
|
+
const indent = " ".repeat(depth + 1);
|
|
764
|
+
if (span.input !== void 0) lines.push(`${indent}in: ${preview(span.input, maxChars)}`);
|
|
765
|
+
if (span.output !== void 0) lines.push(`${indent}out: ${preview(span.output, maxChars)}`);
|
|
766
|
+
return lines;
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* One-line, length-capped preview of a captured value. Strings pass
|
|
770
|
+
* through; everything else is JSON-stringified (falling back to
|
|
771
|
+
* `String()` on a circular / unstringifiable value). Internal whitespace
|
|
772
|
+
* is collapsed so the preview never breaks the tree layout.
|
|
773
|
+
*/
|
|
774
|
+
function preview(value, maxChars) {
|
|
775
|
+
const collapsed = (typeof value === "string" ? value : stringify(value)).replace(/\s+/g, " ").trim();
|
|
776
|
+
return collapsed.length > maxChars ? `${collapsed.slice(0, maxChars)}…` : collapsed;
|
|
777
|
+
}
|
|
778
|
+
function stringify(value) {
|
|
779
|
+
try {
|
|
780
|
+
return JSON.stringify(value) ?? String(value);
|
|
781
|
+
} catch {
|
|
782
|
+
return String(value);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
662
786
|
//#endregion
|
|
663
787
|
//#region ../@warlock.js/ai-panoptic/src/exporters/console/format-span-line.ts
|
|
664
788
|
/**
|
|
@@ -705,9 +829,10 @@ const EXPORTER_NAME$3 = "console";
|
|
|
705
829
|
* as the reference implementation of the contract.
|
|
706
830
|
*
|
|
707
831
|
* By default it prints one summary line per completed trace. Set
|
|
708
|
-
* `tree: true` to print the full indented span tree,
|
|
709
|
-
*
|
|
710
|
-
*
|
|
832
|
+
* `tree: true` to print the full indented span tree, `io: true` to also
|
|
833
|
+
* print each span's captured `input` / `output` (needs the collector's
|
|
834
|
+
* `captureContent`), and `streaming: true` to print each span the moment
|
|
835
|
+
* it finalizes (via the optional `exportSpan` hook).
|
|
711
836
|
*
|
|
712
837
|
* @example
|
|
713
838
|
* collector.use(consoleExporter());
|
|
@@ -715,36 +840,52 @@ const EXPORTER_NAME$3 = "console";
|
|
|
715
840
|
*
|
|
716
841
|
* @example
|
|
717
842
|
* collector.use(consoleExporter({ tree: true }));
|
|
843
|
+
*
|
|
844
|
+
* @example
|
|
845
|
+
* // Full content trace — prompts, responses, and tool I/O:
|
|
846
|
+
* const observe = panoptic({
|
|
847
|
+
* captureContent: true,
|
|
848
|
+
* exporters: [consoleExporter({ tree: true, io: true })],
|
|
849
|
+
* });
|
|
718
850
|
*/
|
|
719
851
|
function consoleExporter(options = {}) {
|
|
720
852
|
const sink = options.console ?? console;
|
|
721
853
|
const tree = options.tree ?? false;
|
|
854
|
+
const io = options.io ?? false;
|
|
855
|
+
const ioMaxChars = options.ioMaxChars;
|
|
722
856
|
const exporter = {
|
|
723
857
|
name: EXPORTER_NAME$3,
|
|
724
858
|
export(trace) {
|
|
725
|
-
writeTrace(sink, trace, tree);
|
|
859
|
+
writeTrace(sink, trace, tree, io, ioMaxChars);
|
|
726
860
|
}
|
|
727
861
|
};
|
|
728
862
|
if (options.streaming) exporter.exportSpan = (span) => {
|
|
729
863
|
sink.log(formatSpanLine(span));
|
|
864
|
+
if (io) for (const line of formatSpanIO(span, 0, ioMaxChars)) sink.log(line);
|
|
730
865
|
};
|
|
731
866
|
return exporter;
|
|
732
867
|
}
|
|
733
868
|
/**
|
|
734
869
|
* Write a completed trace — either a single root summary line or the
|
|
735
|
-
* full indented tree
|
|
736
|
-
*
|
|
870
|
+
* full indented tree, each span optionally followed by its captured
|
|
871
|
+
* `input` / `output`. Failed / cancelled spans route to `console.error`
|
|
872
|
+
* so they surface at the right severity in log aggregators.
|
|
737
873
|
*/
|
|
738
|
-
function writeTrace(sink, trace, tree) {
|
|
874
|
+
function writeTrace(sink, trace, tree, io, ioMaxChars) {
|
|
739
875
|
if (!tree) {
|
|
740
|
-
|
|
741
|
-
writeAtSeverity(sink, trace.root.status, line);
|
|
876
|
+
writeSpan(sink, trace.root, 0, io, ioMaxChars);
|
|
742
877
|
return;
|
|
743
878
|
}
|
|
744
|
-
for (const span of walkSpans(trace.root))
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
879
|
+
for (const span of walkSpans(trace.root)) writeSpan(sink, span, spanDepth(trace.root, span.spanId), io, ioMaxChars);
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Write one span's line and — when `io` is on — its captured content,
|
|
883
|
+
* all routed at the span's own severity so a failed span keeps its
|
|
884
|
+
* content beside it in the error stream.
|
|
885
|
+
*/
|
|
886
|
+
function writeSpan(sink, span, depth, io, ioMaxChars) {
|
|
887
|
+
writeAtSeverity(sink, span.status, formatSpanLine(span, depth));
|
|
888
|
+
if (io) for (const line of formatSpanIO(span, depth, ioMaxChars)) writeAtSeverity(sink, span.status, line);
|
|
748
889
|
}
|
|
749
890
|
/**
|
|
750
891
|
* Route a line to `error` when the span failed/cancelled, otherwise to
|
|
@@ -901,12 +1042,16 @@ function loadLangfuse() {
|
|
|
901
1042
|
* installed, and a missing SDK surfaces as a curated "install this"
|
|
902
1043
|
* error when the exporter first needs to build a client.
|
|
903
1044
|
*
|
|
904
|
-
* The root {@link TraceSpan} becomes a Langfuse trace
|
|
905
|
-
*
|
|
906
|
-
*
|
|
907
|
-
*
|
|
908
|
-
*
|
|
909
|
-
*
|
|
1045
|
+
* The root {@link TraceSpan} becomes a Langfuse trace AND its top
|
|
1046
|
+
* observation (so the root execution's own tokens are metered, not just
|
|
1047
|
+
* its children's); every node is an observation — a `generation` when it
|
|
1048
|
+
* metered its OWN tokens (LLM-backed agents, supervisors), otherwise a
|
|
1049
|
+
* plain `span` (tools, callbacks, and composite nodes whose tokens came
|
|
1050
|
+
* only from children). Timing, status, and version map 1:1; each
|
|
1051
|
+
* generation reports its own token usage (rolled-up minus children) so
|
|
1052
|
+
* the trace total isn't double-counted; non-usage GenAI attributes ride
|
|
1053
|
+
* along as metadata, and captured content (under `captureContent`)
|
|
1054
|
+
* surfaces as native `input` / `output`.
|
|
910
1055
|
*
|
|
911
1056
|
* @example
|
|
912
1057
|
* collector.use(langfuseExporter({ publicKey: "pk-...", secretKey: "sk-..." }));
|
|
@@ -948,20 +1093,20 @@ function langfuseExporter(options) {
|
|
|
948
1093
|
*/
|
|
949
1094
|
function emitTrace(client, trace) {
|
|
950
1095
|
const root = trace.root;
|
|
951
|
-
|
|
1096
|
+
emitObservation(client.trace({
|
|
952
1097
|
id: root.traceId,
|
|
953
1098
|
name: root.name,
|
|
954
1099
|
sessionId: trace.sessionId,
|
|
955
1100
|
version: root.version,
|
|
956
1101
|
timestamp: new Date(root.startedAt),
|
|
957
|
-
metadata:
|
|
958
|
-
});
|
|
959
|
-
for (const child of root.children) emitObservation(langfuseTrace, child);
|
|
1102
|
+
metadata: langfuseMetadata(root)
|
|
1103
|
+
}), root);
|
|
960
1104
|
}
|
|
961
1105
|
/**
|
|
962
1106
|
* Map one {@link TraceSpan} onto a Langfuse observation under `parent`,
|
|
963
|
-
* then recurse its children.
|
|
964
|
-
* `generation`s; everything else
|
|
1107
|
+
* then recurse its children. Spans that metered their OWN tokens become
|
|
1108
|
+
* `generation`s; everything else (tools, callbacks, composite nodes)
|
|
1109
|
+
* becomes a plain `span`.
|
|
965
1110
|
*/
|
|
966
1111
|
function emitObservation(parent, span) {
|
|
967
1112
|
const body = {
|
|
@@ -972,14 +1117,17 @@ function emitObservation(parent, span) {
|
|
|
972
1117
|
level: toLevel(span),
|
|
973
1118
|
statusMessage: span.error?.message,
|
|
974
1119
|
version: span.version,
|
|
975
|
-
metadata:
|
|
1120
|
+
metadata: langfuseMetadata(span)
|
|
976
1121
|
};
|
|
1122
|
+
if (span.input !== void 0) body.input = span.input;
|
|
1123
|
+
if (span.output !== void 0) body.output = span.output;
|
|
977
1124
|
let observation;
|
|
978
|
-
|
|
1125
|
+
const own = ownUsage(span);
|
|
1126
|
+
if (own.total > 0) {
|
|
979
1127
|
body.usage = {
|
|
980
|
-
input:
|
|
981
|
-
output:
|
|
982
|
-
total:
|
|
1128
|
+
input: own.input,
|
|
1129
|
+
output: own.output,
|
|
1130
|
+
total: own.total,
|
|
983
1131
|
unit: "TOKENS"
|
|
984
1132
|
};
|
|
985
1133
|
observation = parent.generation(body);
|
|
@@ -988,20 +1136,60 @@ function emitObservation(parent, span) {
|
|
|
988
1136
|
observation.end({ endTime: body.endTime });
|
|
989
1137
|
}
|
|
990
1138
|
/**
|
|
991
|
-
*
|
|
992
|
-
*
|
|
1139
|
+
* Own token usage for a span — its rolled-up {@link TraceSpan.usage}
|
|
1140
|
+
* minus the rolled-up usage of its direct children. `TraceSpan.usage` is
|
|
1141
|
+
* the subtree total (this node plus every descendant), so subtracting the
|
|
1142
|
+
* children leaves the tokens THIS node alone metered, clamped at zero
|
|
1143
|
+
* defensively.
|
|
993
1144
|
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
998
|
-
*
|
|
999
|
-
*
|
|
1000
|
-
* it; the rolled-up `usage` block emitted on such a generation therefore
|
|
1001
|
-
* double-counts tokens already attributed to descendant observations.
|
|
1145
|
+
* Langfuse sums observation usage into the trace total, so reporting
|
|
1146
|
+
* own-usage on each generation (rather than the subtree rollup) is what
|
|
1147
|
+
* keeps the trace total correct instead of multiply-counting nested
|
|
1148
|
+
* spans. A composite node with no own tokens (e.g. a workflow whose
|
|
1149
|
+
* tokens all came from agent children) yields `total: 0` and is emitted
|
|
1150
|
+
* as a plain span, not a generation.
|
|
1002
1151
|
*/
|
|
1003
|
-
function
|
|
1004
|
-
|
|
1152
|
+
function ownUsage(span) {
|
|
1153
|
+
let childInput = 0;
|
|
1154
|
+
let childOutput = 0;
|
|
1155
|
+
let childTotal = 0;
|
|
1156
|
+
for (const child of span.children) {
|
|
1157
|
+
childInput += child.usage.input;
|
|
1158
|
+
childOutput += child.usage.output;
|
|
1159
|
+
childTotal += child.usage.total;
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
input: Math.max(0, span.usage.input - childInput),
|
|
1163
|
+
output: Math.max(0, span.usage.output - childOutput),
|
|
1164
|
+
total: Math.max(0, span.usage.total - childTotal)
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Token-usage + cost keys that the per-observation `usage` block already
|
|
1169
|
+
* carries authoritatively (as OWN usage). Omitting them from `metadata`
|
|
1170
|
+
* avoids a confusing contradiction — metadata would otherwise show the
|
|
1171
|
+
* rolled-up subtree totals next to an own-usage `usage` block. Cost is
|
|
1172
|
+
* omitted for the same reason; Langfuse prices the own tokens itself.
|
|
1173
|
+
*/
|
|
1174
|
+
const LANGFUSE_METADATA_OMIT = new Set([
|
|
1175
|
+
GEN_AI_ATTRIBUTES.usageInputTokens,
|
|
1176
|
+
GEN_AI_ATTRIBUTES.usageOutputTokens,
|
|
1177
|
+
WARLOCK_ATTRIBUTES.totalTokens,
|
|
1178
|
+
WARLOCK_ATTRIBUTES.cachedTokens,
|
|
1179
|
+
WARLOCK_ATTRIBUTES.reasoningTokens,
|
|
1180
|
+
WARLOCK_ATTRIBUTES.costUsd
|
|
1181
|
+
]);
|
|
1182
|
+
/**
|
|
1183
|
+
* Observation metadata — the GenAI attribute set minus the usage/cost
|
|
1184
|
+
* keys that live authoritatively on the `usage` block (see
|
|
1185
|
+
* {@link LANGFUSE_METADATA_OMIT}). Keeps model identity, report type,
|
|
1186
|
+
* version, session id, and any collector-set attributes.
|
|
1187
|
+
*/
|
|
1188
|
+
function langfuseMetadata(span) {
|
|
1189
|
+
const all = toGenAiAttributes(span);
|
|
1190
|
+
const metadata = {};
|
|
1191
|
+
for (const [key, value] of Object.entries(all)) if (!LANGFUSE_METADATA_OMIT.has(key)) metadata[key] = value;
|
|
1192
|
+
return metadata;
|
|
1005
1193
|
}
|
|
1006
1194
|
/**
|
|
1007
1195
|
* Map the Panoptic span status onto a Langfuse observation level —
|
|
@@ -1113,9 +1301,24 @@ function emitSpan(tracer, span, parentContext, options) {
|
|
|
1113
1301
|
function applyAttributes(otelSpan, span, options) {
|
|
1114
1302
|
const attributes = toGenAiAttributes(span);
|
|
1115
1303
|
if (options.system !== void 0 && attributes[GEN_AI_ATTRIBUTES.system] === void 0) attributes[GEN_AI_ATTRIBUTES.system] = options.system;
|
|
1304
|
+
if (span.input !== void 0) attributes[GEN_AI_ATTRIBUTES.prompt] = stringifyContent(span.input);
|
|
1305
|
+
if (span.output !== void 0) attributes[GEN_AI_ATTRIBUTES.completion] = stringifyContent(span.output);
|
|
1116
1306
|
otelSpan.setAttributes(attributes);
|
|
1117
1307
|
}
|
|
1118
1308
|
/**
|
|
1309
|
+
* Coerce a captured content value to a string OTel attribute. Strings
|
|
1310
|
+
* pass through; structured values are JSON-encoded (falling back to
|
|
1311
|
+
* `String()` if they can't be serialized).
|
|
1312
|
+
*/
|
|
1313
|
+
function stringifyContent(value) {
|
|
1314
|
+
if (typeof value === "string") return value;
|
|
1315
|
+
try {
|
|
1316
|
+
return JSON.stringify(value) ?? String(value);
|
|
1317
|
+
} catch {
|
|
1318
|
+
return String(value);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
/**
|
|
1119
1322
|
* Map the Panoptic span status onto the OTel span status, recording the
|
|
1120
1323
|
* normalized error as an exception event + ERROR status when present.
|
|
1121
1324
|
*/
|
|
@@ -1242,7 +1445,10 @@ const DEFAULT_COMPLETED_EVENTS = [
|
|
|
1242
1445
|
*/
|
|
1243
1446
|
var PanopticSubscriber = class {
|
|
1244
1447
|
constructor(options = {}) {
|
|
1245
|
-
this.collector = options.collector ?? createCollector(
|
|
1448
|
+
this.collector = options.collector ?? createCollector({
|
|
1449
|
+
captureContent: options.captureContent,
|
|
1450
|
+
redactContent: options.redactContent
|
|
1451
|
+
});
|
|
1246
1452
|
for (const exporter of options.exporters ?? []) this.collector.use(exporter);
|
|
1247
1453
|
this.completedEvents = options.completedEvents ?? [...DEFAULT_COMPLETED_EVENTS];
|
|
1248
1454
|
this.middlewareName = options.middlewareName ?? "panoptic";
|
|
@@ -1355,6 +1561,7 @@ exports.createPanopticMiddleware = createPanopticMiddleware;
|
|
|
1355
1561
|
exports.emptyUsage = emptyUsage;
|
|
1356
1562
|
exports.extractSpanAttributes = extractSpanAttributes;
|
|
1357
1563
|
exports.fileExporter = fileExporter;
|
|
1564
|
+
exports.formatSpanIO = formatSpanIO;
|
|
1358
1565
|
exports.formatSpanLine = formatSpanLine;
|
|
1359
1566
|
exports.langfuseExporter = langfuseExporter;
|
|
1360
1567
|
exports.matchTrace = matchTrace;
|