@warlock.js/ai-panoptic 4.3.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 (124) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/LICENSE +21 -0
  3. package/README.md +111 -0
  4. package/cjs/index.cjs +1370 -0
  5. package/cjs/index.cjs.map +1 -0
  6. package/esm/collector/collector.d.mts +19 -0
  7. package/esm/collector/collector.d.mts.map +1 -0
  8. package/esm/collector/collector.mjs +89 -0
  9. package/esm/collector/collector.mjs.map +1 -0
  10. package/esm/collector/extract-span-attributes.d.mts +29 -0
  11. package/esm/collector/extract-span-attributes.d.mts.map +1 -0
  12. package/esm/collector/extract-span-attributes.mjs +76 -0
  13. package/esm/collector/extract-span-attributes.mjs.map +1 -0
  14. package/esm/collector/index.d.mts +5 -0
  15. package/esm/collector/normalize-error.d.mts +27 -0
  16. package/esm/collector/normalize-error.d.mts.map +1 -0
  17. package/esm/collector/normalize-error.mjs +49 -0
  18. package/esm/collector/normalize-error.mjs.map +1 -0
  19. package/esm/collector/report-to-span.d.mts +31 -0
  20. package/esm/collector/report-to-span.d.mts.map +1 -0
  21. package/esm/collector/report-to-span.mjs +53 -0
  22. package/esm/collector/report-to-span.mjs.map +1 -0
  23. package/esm/collector/report-to-trace.d.mts +35 -0
  24. package/esm/collector/report-to-trace.d.mts.map +1 -0
  25. package/esm/collector/report-to-trace.mjs +53 -0
  26. package/esm/collector/report-to-trace.mjs.map +1 -0
  27. package/esm/contracts/collector.contract.d.mts +63 -0
  28. package/esm/contracts/collector.contract.d.mts.map +1 -0
  29. package/esm/contracts/exporter.contract.d.mts +76 -0
  30. package/esm/contracts/exporter.contract.d.mts.map +1 -0
  31. package/esm/contracts/index.d.mts +3 -0
  32. package/esm/contracts/trace.type.d.mts +122 -0
  33. package/esm/contracts/trace.type.d.mts.map +1 -0
  34. package/esm/exporters/console/console-exporter.d.mts +25 -0
  35. package/esm/exporters/console/console-exporter.d.mts.map +1 -0
  36. package/esm/exporters/console/console-exporter.mjs +79 -0
  37. package/esm/exporters/console/console-exporter.mjs.map +1 -0
  38. package/esm/exporters/console/console-exporter.type.d.mts +36 -0
  39. package/esm/exporters/console/console-exporter.type.d.mts.map +1 -0
  40. package/esm/exporters/console/format-span-line.d.mts +17 -0
  41. package/esm/exporters/console/format-span-line.d.mts.map +1 -0
  42. package/esm/exporters/console/format-span-line.mjs +41 -0
  43. package/esm/exporters/console/format-span-line.mjs.map +1 -0
  44. package/esm/exporters/console/index.d.mts +3 -0
  45. package/esm/exporters/file/file-exporter.d.mts +23 -0
  46. package/esm/exporters/file/file-exporter.d.mts.map +1 -0
  47. package/esm/exporters/file/file-exporter.mjs +93 -0
  48. package/esm/exporters/file/file-exporter.mjs.map +1 -0
  49. package/esm/exporters/file/file-exporter.type.d.mts +39 -0
  50. package/esm/exporters/file/file-exporter.type.d.mts.map +1 -0
  51. package/esm/exporters/file/index.d.mts +2 -0
  52. package/esm/exporters/index.d.mts +12 -0
  53. package/esm/exporters/index.mjs +12 -0
  54. package/esm/exporters/langfuse/index.d.mts +2 -0
  55. package/esm/exporters/langfuse/index.mjs +3 -0
  56. package/esm/exporters/langfuse/langfuse-exporter.d.mts +27 -0
  57. package/esm/exporters/langfuse/langfuse-exporter.d.mts.map +1 -0
  58. package/esm/exporters/langfuse/langfuse-exporter.mjs +158 -0
  59. package/esm/exporters/langfuse/langfuse-exporter.mjs.map +1 -0
  60. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts +90 -0
  61. package/esm/exporters/langfuse/langfuse-exporter.type.d.mts.map +1 -0
  62. package/esm/exporters/otel/index.d.mts +2 -0
  63. package/esm/exporters/otel/index.mjs +3 -0
  64. package/esm/exporters/otel/otel-exporter.d.mts +29 -0
  65. package/esm/exporters/otel/otel-exporter.d.mts.map +1 -0
  66. package/esm/exporters/otel/otel-exporter.mjs +142 -0
  67. package/esm/exporters/otel/otel-exporter.mjs.map +1 -0
  68. package/esm/exporters/otel/otel-exporter.type.d.mts +39 -0
  69. package/esm/exporters/otel/otel-exporter.type.d.mts.map +1 -0
  70. package/esm/exporters/utils/gen-ai-attributes.d.mts +65 -0
  71. package/esm/exporters/utils/gen-ai-attributes.d.mts.map +1 -0
  72. package/esm/exporters/utils/gen-ai-attributes.mjs +88 -0
  73. package/esm/exporters/utils/gen-ai-attributes.mjs.map +1 -0
  74. package/esm/exporters/utils/index.d.mts +3 -0
  75. package/esm/exporters/utils/total-cost.d.mts +20 -0
  76. package/esm/exporters/utils/total-cost.d.mts.map +1 -0
  77. package/esm/exporters/utils/total-cost.mjs +23 -0
  78. package/esm/exporters/utils/total-cost.mjs.map +1 -0
  79. package/esm/exporters/utils/walk-spans.d.mts +18 -0
  80. package/esm/exporters/utils/walk-spans.d.mts.map +1 -0
  81. package/esm/exporters/utils/walk-spans.mjs +21 -0
  82. package/esm/exporters/utils/walk-spans.mjs.map +1 -0
  83. package/esm/index.d.mts +31 -0
  84. package/esm/index.mjs +22 -0
  85. package/esm/panoptic/index.d.mts +4 -0
  86. package/esm/panoptic/panoptic-middleware.d.mts +38 -0
  87. package/esm/panoptic/panoptic-middleware.d.mts.map +1 -0
  88. package/esm/panoptic/panoptic-middleware.mjs +68 -0
  89. package/esm/panoptic/panoptic-middleware.mjs.map +1 -0
  90. package/esm/panoptic/panoptic-target.type.d.mts +53 -0
  91. package/esm/panoptic/panoptic-target.type.d.mts.map +1 -0
  92. package/esm/panoptic/panoptic.d.mts +37 -0
  93. package/esm/panoptic/panoptic.d.mts.map +1 -0
  94. package/esm/panoptic/panoptic.mjs +134 -0
  95. package/esm/panoptic/panoptic.mjs.map +1 -0
  96. package/esm/panoptic/panoptic.type.d.mts +100 -0
  97. package/esm/panoptic/panoptic.type.d.mts.map +1 -0
  98. package/esm/store/in-memory-trace-store.d.mts +32 -0
  99. package/esm/store/in-memory-trace-store.d.mts.map +1 -0
  100. package/esm/store/in-memory-trace-store.mjs +134 -0
  101. package/esm/store/in-memory-trace-store.mjs.map +1 -0
  102. package/esm/store/index.d.mts +6 -0
  103. package/esm/store/index.mjs +5 -0
  104. package/esm/store/match-trace.d.mts +21 -0
  105. package/esm/store/match-trace.d.mts.map +1 -0
  106. package/esm/store/match-trace.mjs +44 -0
  107. package/esm/store/match-trace.mjs.map +1 -0
  108. package/esm/store/sum-usage.d.mts +34 -0
  109. package/esm/store/sum-usage.d.mts.map +1 -0
  110. package/esm/store/sum-usage.mjs +65 -0
  111. package/esm/store/sum-usage.mjs.map +1 -0
  112. package/esm/store/trace-aggregate.type.d.mts +51 -0
  113. package/esm/store/trace-aggregate.type.d.mts.map +1 -0
  114. package/esm/store/trace-query.type.d.mts +45 -0
  115. package/esm/store/trace-query.type.d.mts.map +1 -0
  116. package/esm/store/trace-store.contract.d.mts +55 -0
  117. package/esm/store/trace-store.contract.d.mts.map +1 -0
  118. package/llms-full.txt +450 -0
  119. package/llms.txt +11 -0
  120. package/package.json +40 -0
  121. package/skills/README.md +17 -0
  122. package/skills/export-traces/SKILL.md +152 -0
  123. package/skills/observe-with-panoptic/SKILL.md +128 -0
  124. package/skills/query-traces/SKILL.md +152 -0
package/cjs/index.cjs ADDED
@@ -0,0 +1,1370 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+ let _warlock_js_ai = require("@warlock.js/ai");
3
+ let node_fs_promises = require("node:fs/promises");
4
+ let node_path = require("node:path");
5
+
6
+ //#region ../@warlock.js/ai-panoptic/src/exporters/utils/walk-spans.ts
7
+ /**
8
+ * Depth-first pre-order traversal of a {@link TraceSpan} tree, yielding
9
+ * the root first and then each descendant in `children` (invocation)
10
+ * order. Exporters that emit a flat span stream — OpenTelemetry, the
11
+ * console table — walk the tree once with this instead of re-writing the
12
+ * recursion in every exporter.
13
+ *
14
+ * @example
15
+ * for (const span of walkSpans(trace.root)) {
16
+ * emit(span);
17
+ * }
18
+ */
19
+ function* walkSpans(root) {
20
+ yield root;
21
+ for (const child of root.children) yield* walkSpans(child);
22
+ }
23
+
24
+ //#endregion
25
+ //#region ../@warlock.js/ai-panoptic/src/collector/normalize-error.ts
26
+ /**
27
+ * Project a captured execution error onto the structural
28
+ * {@link TraceSpanError} shape used on a failed / cancelled span.
29
+ *
30
+ * Source errors are typically `AIError` instances (every error surfaced
31
+ * by `@warlock.js/ai` is one), but the collector never depends on the
32
+ * concrete class — it reads only the structural surface (`name` / `code`
33
+ * / `message` / `stack`) so a plain `Error`, an `AIError`, or any
34
+ * thrown value all normalize identically. The result is a JSON-safe
35
+ * plain object so it survives serialization to a backend collector
36
+ * unchanged.
37
+ *
38
+ * The error `type` prefers the stable `code` (e.g. `"RATE_LIMIT"`) over
39
+ * the class `name`, falling back to `name` and finally to the generic
40
+ * `"Error"` so the field is always populated.
41
+ *
42
+ * @example
43
+ * const spanError = normalizeError(report.error);
44
+ * // { type: "RATE_LIMIT", message: "429 Too Many Requests", stack: "..." }
45
+ */
46
+ function normalizeError(error) {
47
+ if (error === void 0 || error === null) return;
48
+ if (typeof error !== "object") return {
49
+ type: "Error",
50
+ message: String(error)
51
+ };
52
+ const candidate = error;
53
+ const type = pickString(candidate.code) ?? pickString(candidate.name) ?? "Error";
54
+ const message = pickString(candidate.message) ?? "";
55
+ const stack = pickString(candidate.stack);
56
+ const normalized = {
57
+ type,
58
+ message
59
+ };
60
+ if (stack !== void 0) normalized.stack = stack;
61
+ return normalized;
62
+ }
63
+ /**
64
+ * Return the value when it is a non-empty string, otherwise `undefined`.
65
+ * Keeps `normalizeError` from promoting empty / non-string fields.
66
+ */
67
+ function pickString(value) {
68
+ if (typeof value === "string" && value.length > 0) return value;
69
+ }
70
+
71
+ //#endregion
72
+ //#region ../@warlock.js/ai-panoptic/src/collector/extract-span-attributes.ts
73
+ /**
74
+ * Build the free-form `TraceSpan.attributes` bag for one report node.
75
+ *
76
+ * The collector keeps the first-class span fields (identity, timing,
77
+ * status, usage, error) on the span itself and routes everything
78
+ * primitive-specific here — trip/step/iteration counts, the model
79
+ * identity an agent ran against, the tool's originating trip index, a
80
+ * supervisor's termination reason. Exporters forward this verbatim as
81
+ * backend span attributes (OTel attributes, Langfuse metadata).
82
+ *
83
+ * Only populated keys are emitted; the function returns `undefined`
84
+ * when the node carried no extra detail, so the optional
85
+ * `TraceSpan.attributes` field stays absent rather than holding an
86
+ * empty object (matches the contract's "absent when empty" note).
87
+ *
88
+ * Retry count is surfaced from the shared `BaseReport.attempts` for
89
+ * every primitive so cost dashboards see the real call count.
90
+ *
91
+ * @example
92
+ * const attributes = extractSpanAttributes(agentReport);
93
+ * // { "agent.trips": 3, "agent.model.name": "gpt-4o", "agent.model.provider": "openai" }
94
+ */
95
+ function extractSpanAttributes(report) {
96
+ const extensions = report;
97
+ const attributes = {};
98
+ if (report.attempts !== void 0 && report.attempts.length > 0) attributes["retries"] = report.attempts.length;
99
+ switch (report.type) {
100
+ case "agent":
101
+ addAgentAttributes(attributes, extensions);
102
+ break;
103
+ case "workflow":
104
+ addWorkflowAttributes(attributes, extensions);
105
+ break;
106
+ case "supervisor":
107
+ addSupervisorAttributes(attributes, extensions);
108
+ break;
109
+ case "orchestrator":
110
+ addOrchestratorAttributes(attributes, extensions);
111
+ break;
112
+ case "tool":
113
+ addToolAttributes(attributes, extensions);
114
+ break;
115
+ default: break;
116
+ }
117
+ if (Object.keys(attributes).length === 0) return;
118
+ return attributes;
119
+ }
120
+ function addAgentAttributes(attributes, extensions) {
121
+ if (Array.isArray(extensions.trips)) attributes["agent.trips"] = extensions.trips.length;
122
+ if (extensions.model?.name !== void 0) attributes["agent.model.name"] = extensions.model.name;
123
+ if (extensions.model?.provider !== void 0) attributes["agent.model.provider"] = extensions.model.provider;
124
+ }
125
+ function addWorkflowAttributes(attributes, extensions) {
126
+ if (extensions.workflowName !== void 0) attributes["workflow.name"] = extensions.workflowName;
127
+ if (extensions.signature !== void 0) attributes["workflow.signature"] = extensions.signature;
128
+ if (extensions.steps !== void 0) attributes["workflow.steps"] = Object.keys(extensions.steps).length;
129
+ }
130
+ function addSupervisorAttributes(attributes, extensions) {
131
+ if (extensions.supervisorName !== void 0) attributes["supervisor.name"] = extensions.supervisorName;
132
+ if (extensions.terminatedBy !== void 0) attributes["supervisor.terminatedBy"] = extensions.terminatedBy;
133
+ if (extensions.iterations !== void 0) attributes["supervisor.iterations"] = extensions.iterations;
134
+ }
135
+ function addOrchestratorAttributes(attributes, extensions) {
136
+ if (extensions.turnIndex !== void 0) attributes["orchestrator.turnIndex"] = extensions.turnIndex;
137
+ if (extensions.signature !== void 0) attributes["orchestrator.signature"] = extensions.signature;
138
+ if (Array.isArray(extensions.turns)) attributes["orchestrator.turns"] = extensions.turns.length;
139
+ }
140
+ function addToolAttributes(attributes, extensions) {
141
+ if (extensions.tripIndex !== void 0) attributes["tool.tripIndex"] = extensions.tripIndex;
142
+ if (extensions.recoveredFrom !== void 0) attributes["tool.recoveredFrom"] = extensions.recoveredFrom;
143
+ }
144
+
145
+ //#endregion
146
+ //#region ../@warlock.js/ai-panoptic/src/collector/report-to-span.ts
147
+ /**
148
+ * Project one {@link BaseReport} node — and its entire subtree — into a
149
+ * {@link TraceSpan}. Pure and recursive: identity, timing, status, and
150
+ * the rolled-up `usage` map across 1:1 from the report; lineage maps
151
+ * `runId → spanId`, `parentRunId → parentSpanId`, `rootRunId → traceId`;
152
+ * children recurse in invocation order so the span tree mirrors the
153
+ * report tree exactly.
154
+ *
155
+ * The error is normalized to the JSON-safe {@link
156
+ * import("../contracts/trace.type").TraceSpanError} shape only when the
157
+ * node carried one (failed / cancelled). Primitive-specific detail that
158
+ * has no first-class span field (trip / step / iteration counts, model
159
+ * identity, tool trip index) is routed into the optional `attributes`
160
+ * bag via {@link extractSpanAttributes}.
161
+ *
162
+ * No external lookup is needed — a `BaseReport` already carries
163
+ * everything a span requires, so a collector can flatten a tree without
164
+ * consulting any other source.
165
+ *
166
+ * @example
167
+ * const root = reportToSpan(result.report);
168
+ * console.log(root.spanId, root.traceId, root.children.length);
169
+ */
170
+ function reportToSpan(report) {
171
+ const span = {
172
+ spanId: report.runId,
173
+ traceId: report.rootRunId,
174
+ name: report.name,
175
+ type: report.type,
176
+ status: report.status,
177
+ startedAt: report.startedAt,
178
+ endedAt: report.endedAt,
179
+ duration: report.duration,
180
+ usage: report.usage,
181
+ children: report.children.map(reportToSpan)
182
+ };
183
+ if (report.parentRunId !== void 0) span.parentSpanId = report.parentRunId;
184
+ if (report.sessionId !== void 0) span.sessionId = report.sessionId;
185
+ if (report.version !== void 0) span.version = report.version;
186
+ const error = normalizeError(report.error);
187
+ if (error !== void 0) span.error = error;
188
+ const attributes = extractSpanAttributes(report);
189
+ if (attributes !== void 0) span.attributes = attributes;
190
+ return span;
191
+ }
192
+
193
+ //#endregion
194
+ //#region ../@warlock.js/ai-panoptic/src/collector/report-to-trace.ts
195
+ /**
196
+ * Project an outermost {@link BaseReport} (one whole `.execute()` /
197
+ * `.invoke()` run) into a {@link Trace} — the root {@link
198
+ * import("../contracts/trace.type").TraceSpan} plus the trace-wide
199
+ * rollups exporters need without re-walking the tree.
200
+ *
201
+ * The trace-level identity and rollups all read off the root span the
202
+ * projection already built (`traceId`, `usage`, timing), so the trace
203
+ * envelope never disagrees with its own root. `reportSchemaVersion` is
204
+ * mirrored from the root report when present (it is stamped only on
205
+ * root nodes upstream) so exporters can branch on the source shape.
206
+ *
207
+ * Pure — the same input always yields the same trace. The collector
208
+ * exposes this as `toTrace` so callers can inspect the normalized shape
209
+ * without dispatching to exporters.
210
+ *
211
+ * The optional `rootError` threads the failing run's envelope error
212
+ * (`BaseResult.error`) onto the root span. The error lives on the result
213
+ * envelope, never on the `BaseReport` tree, so without this a failed root
214
+ * would export `status` with no error type/message. Applied only when the
215
+ * root span carries none of its own; the subtree projection stays pure
216
+ * (each child surfaces its own report-level error, if any).
217
+ *
218
+ * @example
219
+ * const trace = reportToTrace(result.report, result.error);
220
+ * console.log(trace.traceId, trace.usage.total, trace.duration);
221
+ */
222
+ function reportToTrace(report, rootError) {
223
+ const root = reportToSpan(report);
224
+ if (root.error === void 0) {
225
+ const error = normalizeError(rootError);
226
+ if (error !== void 0) root.error = error;
227
+ }
228
+ const trace = {
229
+ traceId: root.traceId,
230
+ root,
231
+ startedAt: root.startedAt,
232
+ endedAt: root.endedAt,
233
+ duration: root.duration,
234
+ usage: root.usage
235
+ };
236
+ if (root.sessionId !== void 0) trace.sessionId = root.sessionId;
237
+ if (report.reportSchemaVersion !== void 0) trace.reportSchemaVersion = report.reportSchemaVersion;
238
+ return trace;
239
+ }
240
+
241
+ //#endregion
242
+ //#region ../@warlock.js/ai-panoptic/src/collector/collector.ts
243
+ /**
244
+ * Drive the source end of the Panoptic pipeline: ingest core
245
+ * `@warlock.js/ai` {@link BaseReport} trees, project them into {@link
246
+ * Trace}s, and fan each trace out to every registered exporter.
247
+ *
248
+ * Owns exporter registration (deduped by `ExporterContract.name`), the
249
+ * report→trace projection, and graceful shutdown so exporters drain
250
+ * before exit. Instantiated fresh per collector via {@link
251
+ * createCollector}; callers never see `new`.
252
+ *
253
+ * **Failure isolation.** An exporter that throws never propagates back
254
+ * into the originating run — `collect`, `flush`, and `shutdown` settle
255
+ * every exporter independently (mirrors how the core event hooks
256
+ * swallow consumer errors). One broken exporter can neither crash the
257
+ * agent loop nor stop sibling exporters from receiving the trace.
258
+ */
259
+ var Collector = class {
260
+ constructor() {
261
+ this.exporters = /* @__PURE__ */ new Map();
262
+ }
263
+ use(exporter) {
264
+ if (!this.exporters.has(exporter.name)) this.exporters.set(exporter.name, exporter);
265
+ return this;
266
+ }
267
+ toTrace(report, rootError) {
268
+ return reportToTrace(report, rootError);
269
+ }
270
+ async collect(report, rootError) {
271
+ const trace = this.toTrace(report, rootError);
272
+ await this.dispatch(trace);
273
+ }
274
+ async flush() {
275
+ await this.settleAll((exporter) => exporter.flush?.());
276
+ }
277
+ async shutdown() {
278
+ await this.flush();
279
+ await this.settleAll((exporter) => exporter.shutdown?.());
280
+ this.exporters.clear();
281
+ }
282
+ /**
283
+ * Fan one trace out to every exporter and, when an exporter advertises
284
+ * the per-span hook, deliver every span in the finalized tree to it as
285
+ * well. `exportSpan` is a post-completion per-span hook (not a live /
286
+ * streaming feed — the trace is already finalized): we walk the tree in
287
+ * pre-order with {@link walkSpans} so the exporter sees the root and
288
+ * every descendant exactly once. Every invocation is isolated so a
289
+ * throwing exporter can't abort the dispatch to its siblings or escape
290
+ * into the originating run.
291
+ */
292
+ async dispatch(trace) {
293
+ await this.settleAll(async (exporter) => {
294
+ await exporter.export(trace);
295
+ if (exporter.exportSpan !== void 0) for (const span of walkSpans(trace.root)) await exporter.exportSpan(span);
296
+ });
297
+ }
298
+ /**
299
+ * Run `task` against every registered exporter and wait for all of
300
+ * them to settle, swallowing individual rejections. `Promise.allSettled`
301
+ * guarantees one failure neither rejects the batch nor blocks the
302
+ * others — the contract's failure-isolation requirement.
303
+ */
304
+ async settleAll(task) {
305
+ const runs = [...this.exporters.values()].map((exporter) => Promise.resolve().then(() => task(exporter)));
306
+ await Promise.allSettled(runs);
307
+ }
308
+ };
309
+ /**
310
+ * Create a Panoptic collector — the single integration point an app
311
+ * wires into its agents/workflows (typically via the `onComplete`
312
+ * report hook). Register exporters with `use`, then feed finalized root
313
+ * reports to `collect`.
314
+ *
315
+ * @example
316
+ * const collector = createCollector().use(otelExporter).use(langfuseExporter);
317
+ * agent.on("onComplete", ({ result }) => collector.collect(result.report));
318
+ * // on shutdown:
319
+ * await collector.shutdown();
320
+ */
321
+ function createCollector() {
322
+ return new Collector();
323
+ }
324
+
325
+ //#endregion
326
+ //#region ../@warlock.js/ai-panoptic/src/store/match-trace.ts
327
+ /**
328
+ * Test whether one trace satisfies a {@link TraceQuery}. Every declared
329
+ * filter field must match (logical AND); absent fields are ignored, so
330
+ * an empty / undefined filter matches every trace.
331
+ *
332
+ * Time bounds compare against the trace's root `startedAt`, parsed to
333
+ * an epoch once per call, inclusive on both ends. Status accepts a
334
+ * single value or an array (membership test). Identity fields are exact
335
+ * string equality.
336
+ *
337
+ * Pure — used by the store's `query` and `aggregate` so both share one
338
+ * matching definition.
339
+ */
340
+ function matchTrace(trace, filter) {
341
+ if (!filter) return true;
342
+ if (filter.traceId !== void 0 && trace.traceId !== filter.traceId) return false;
343
+ if (filter.sessionId !== void 0 && trace.sessionId !== filter.sessionId) return false;
344
+ if (filter.status !== void 0 && !statusMatches(trace.root.status, filter.status)) return false;
345
+ const startedAt = Date.parse(trace.startedAt);
346
+ if (filter.startedAfter !== void 0 && startedAt < toEpoch(filter.startedAfter)) return false;
347
+ if (filter.startedBefore !== void 0 && startedAt > toEpoch(filter.startedBefore)) return false;
348
+ return true;
349
+ }
350
+ /**
351
+ * Membership test for the status filter — true when `status` equals the
352
+ * single wanted value, or is one of the wanted array.
353
+ */
354
+ function statusMatches(status, wanted) {
355
+ if (Array.isArray(wanted)) return wanted.includes(status);
356
+ return status === wanted;
357
+ }
358
+ /**
359
+ * Normalize a time bound (ISO string or `Date`) to epoch milliseconds
360
+ * for comparison against a parsed `startedAt`.
361
+ */
362
+ function toEpoch(bound) {
363
+ if (bound instanceof Date) return bound.getTime();
364
+ return Date.parse(bound);
365
+ }
366
+
367
+ //#endregion
368
+ //#region ../@warlock.js/ai-panoptic/src/store/sum-usage.ts
369
+ /**
370
+ * Fold a child {@link Usage} into a running accumulator. Token channels
371
+ * (`input` / `output` / `total`) always sum; the optional cache /
372
+ * reasoning channels (`cachedTokens` / `cacheWriteTokens` /
373
+ * `reasoningTokens`) sum only when at least one side reported them, so
374
+ * a provider that never meters a channel doesn't fabricate a `0` for
375
+ * it. The `cost` breakdown is merged with the core framework's
376
+ * {@link accumulateCost}, keeping cost-rollup semantics identical to a
377
+ * native report tree — an unpriced contributor never erases a priced
378
+ * one.
379
+ *
380
+ * Pure: returns a fresh `Usage`, never mutates either argument. Seed an
381
+ * aggregation with {@link emptyUsage}.
382
+ *
383
+ * @example
384
+ * let total = emptyUsage();
385
+ * for (const trace of traces) {
386
+ * total = sumUsage(total, trace.usage);
387
+ * }
388
+ */
389
+ function sumUsage(accumulator, next) {
390
+ const merged = {
391
+ input: accumulator.input + next.input,
392
+ output: accumulator.output + next.output,
393
+ total: accumulator.total + next.total
394
+ };
395
+ const cachedTokens = sumOptional(accumulator.cachedTokens, next.cachedTokens);
396
+ if (cachedTokens !== void 0) merged.cachedTokens = cachedTokens;
397
+ const cacheWriteTokens = sumOptional(accumulator.cacheWriteTokens, next.cacheWriteTokens);
398
+ if (cacheWriteTokens !== void 0) merged.cacheWriteTokens = cacheWriteTokens;
399
+ const reasoningTokens = sumOptional(accumulator.reasoningTokens, next.reasoningTokens);
400
+ if (reasoningTokens !== void 0) merged.reasoningTokens = reasoningTokens;
401
+ const cost = (0, _warlock_js_ai.accumulateCost)(accumulator.cost, next.cost);
402
+ if (cost !== void 0) merged.cost = cost;
403
+ return merged;
404
+ }
405
+ /**
406
+ * A zero-valued {@link Usage} to seed an aggregation. Only the required
407
+ * token channels are set; optional channels stay absent until a
408
+ * contributor reports them, preserving the "never reported" vs
409
+ * "reported as 0" distinction.
410
+ */
411
+ function emptyUsage() {
412
+ return {
413
+ input: 0,
414
+ output: 0,
415
+ total: 0
416
+ };
417
+ }
418
+ /**
419
+ * Add two optional token counts, treating either side's `undefined` as
420
+ * zero — but return `undefined` when both are absent, so an unreported
421
+ * channel stays unreported rather than collapsing to `0`.
422
+ */
423
+ function sumOptional(accumulator, next) {
424
+ if (accumulator === void 0 && next === void 0) return;
425
+ return (accumulator ?? 0) + (next ?? 0);
426
+ }
427
+
428
+ //#endregion
429
+ //#region ../@warlock.js/ai-panoptic/src/store/in-memory-trace-store.ts
430
+ /**
431
+ * In-memory {@link TraceStoreContract} that doubles as an
432
+ * {@link ExporterContract} — register it on a collector
433
+ * (`collector.use(store)`) and it fills as traces complete, then query
434
+ * or aggregate it after the fact.
435
+ *
436
+ * Backed by an insertion-ordered `Map` keyed by `traceId`, giving O(1)
437
+ * `get` / `add` / overwrite and O(n) scans for `query` / `aggregate`
438
+ * (the price of an in-memory store with no secondary indexes — fine for
439
+ * the dev/test and modest-volume runtime use this targets). When a
440
+ * `capacity` is configured, ingesting past the cap evicts the oldest
441
+ * trace.
442
+ *
443
+ * Instantiated fresh per store via {@link createInMemoryTraceStore};
444
+ * callers never see `new`.
445
+ */
446
+ var InMemoryTraceStore = class {
447
+ constructor(options) {
448
+ this.name = "in-memory-trace-store";
449
+ this.traces = /* @__PURE__ */ new Map();
450
+ this.capacity = options?.capacity ?? 0;
451
+ }
452
+ get size() {
453
+ return this.traces.size;
454
+ }
455
+ add(trace) {
456
+ this.traces.delete(trace.traceId);
457
+ this.traces.set(trace.traceId, trace);
458
+ this.evictOverflow();
459
+ }
460
+ /**
461
+ * `ExporterContract.export` — a collector dispatches a completed
462
+ * trace here, which is exactly an `add`. Lets the store be wired into
463
+ * a collector as a sink without an adapter.
464
+ */
465
+ export(trace) {
466
+ this.add(trace);
467
+ }
468
+ get(traceId) {
469
+ return this.traces.get(traceId);
470
+ }
471
+ query(filter) {
472
+ const matched = [];
473
+ for (const trace of this.traces.values()) if (matchTrace(trace, filter)) matched.push(trace);
474
+ return this.sortNewestFirst(matched);
475
+ }
476
+ aggregate(filter) {
477
+ const aggregate = {
478
+ traces: 0,
479
+ completed: 0,
480
+ failed: 0,
481
+ cancelled: 0,
482
+ usage: emptyUsage(),
483
+ totalDuration: 0
484
+ };
485
+ for (const trace of this.traces.values()) {
486
+ if (!matchTrace(trace, filter)) continue;
487
+ aggregate.traces += 1;
488
+ aggregate.totalDuration += trace.duration;
489
+ aggregate.usage = sumUsage(aggregate.usage, trace.usage);
490
+ this.countStatus(aggregate, trace);
491
+ }
492
+ if (aggregate.usage.cost !== void 0) aggregate.cost = aggregate.usage.cost;
493
+ return aggregate;
494
+ }
495
+ clear() {
496
+ this.traces.clear();
497
+ }
498
+ /**
499
+ * Increment the matching terminal-status counter for one trace.
500
+ * Non-terminal statuses (`awaiting-input`, `max-iterations`) are
501
+ * counted in `traces` but tracked by none of the three headline
502
+ * counters — intentional, those three answer the common
503
+ * "succeeded / errored / aborted" question.
504
+ */
505
+ countStatus(aggregate, trace) {
506
+ switch (trace.root.status) {
507
+ case "completed":
508
+ aggregate.completed += 1;
509
+ break;
510
+ case "failed":
511
+ aggregate.failed += 1;
512
+ break;
513
+ case "cancelled":
514
+ aggregate.cancelled += 1;
515
+ break;
516
+ default: break;
517
+ }
518
+ }
519
+ /**
520
+ * Sort matched traces newest-started first. A copy is sorted so the
521
+ * underlying insertion order (which eviction depends on) is never
522
+ * disturbed.
523
+ */
524
+ sortNewestFirst(traces) {
525
+ return traces.sort((left, right) => Date.parse(right.startedAt) - Date.parse(left.startedAt));
526
+ }
527
+ /**
528
+ * Evict oldest-inserted traces until the store is within `capacity`.
529
+ * No-op when unbounded. The `Map` iterator yields keys in insertion
530
+ * order, so the first key is always the oldest.
531
+ */
532
+ evictOverflow() {
533
+ if (this.capacity <= 0) return;
534
+ while (this.traces.size > this.capacity) {
535
+ const oldest = this.traces.keys().next().value;
536
+ if (oldest === void 0) return;
537
+ this.traces.delete(oldest);
538
+ }
539
+ }
540
+ };
541
+ /**
542
+ * Create an in-memory trace store. Optionally bound it with `capacity`
543
+ * for long-lived processes; leave it unset for dev/test where you want
544
+ * every trace retained.
545
+ *
546
+ * @example
547
+ * const store = createInMemoryTraceStore({ capacity: 1000 });
548
+ * collector.use(store);
549
+ * // later:
550
+ * const recentFailures = store.query({ status: "failed" });
551
+ * const sessionSpend = store.aggregate({ sessionId });
552
+ */
553
+ function createInMemoryTraceStore(options) {
554
+ return new InMemoryTraceStore(options);
555
+ }
556
+
557
+ //#endregion
558
+ //#region ../@warlock.js/ai-panoptic/src/exporters/utils/total-cost.ts
559
+ /**
560
+ * Collapse a {@link Usage.cost} breakdown into a single USD scalar by
561
+ * summing every populated field. Mirrors the formula documented on
562
+ * `Usage.cost` (input + output + cachedInput + cachedOutput), and also
563
+ * folds in `reasoning` for forward-safety when a provider prices
564
+ * reasoning tokens as a separate channel. Returns `undefined` when no
565
+ * pricing was attached, so exporters can omit the cost attribute
566
+ * entirely rather than reporting a misleading `0`.
567
+ *
568
+ * @example
569
+ * totalCostUsd({ input: 1, output: 2, total: 3, cost: { input: 0.01, output: 0.04 } });
570
+ * // => 0.05
571
+ */
572
+ function totalCostUsd(usage) {
573
+ const cost = usage.cost;
574
+ if (!cost) return;
575
+ return (cost.input ?? 0) + (cost.output ?? 0) + (cost.cachedInput ?? 0) + (cost.cachedOutput ?? 0) + (cost.reasoning ?? 0);
576
+ }
577
+
578
+ //#endregion
579
+ //#region ../@warlock.js/ai-panoptic/src/exporters/utils/gen-ai-attributes.ts
580
+ /**
581
+ * Subset of the OpenTelemetry GenAI semantic-convention attribute keys
582
+ * Panoptic emits. Kept as a named constant map (not inline string
583
+ * literals scattered through the mapper) so the convention names live in
584
+ * one place and a convention bump is a single edit.
585
+ *
586
+ * @see https://opentelemetry.io/docs/specs/semconv/gen-ai/
587
+ */
588
+ const GEN_AI_ATTRIBUTES = {
589
+ operationName: "gen_ai.operation.name",
590
+ system: "gen_ai.system",
591
+ requestModel: "gen_ai.request.model",
592
+ responseModel: "gen_ai.response.model",
593
+ usageInputTokens: "gen_ai.usage.input_tokens",
594
+ usageOutputTokens: "gen_ai.usage.output_tokens",
595
+ conversationId: "gen_ai.conversation.id"
596
+ };
597
+ /**
598
+ * Panoptic-specific attribute keys that have no GenAI-convention
599
+ * equivalent. Namespaced under `warlock.*` so they never collide with a
600
+ * future `gen_ai.*` key the spec might add.
601
+ */
602
+ const WARLOCK_ATTRIBUTES = {
603
+ reportType: "warlock.report.type",
604
+ version: "warlock.version",
605
+ durationMs: "warlock.duration_ms",
606
+ totalTokens: "gen_ai.usage.total_tokens",
607
+ cachedTokens: "gen_ai.usage.cached_tokens",
608
+ reasoningTokens: "gen_ai.usage.reasoning_tokens",
609
+ costUsd: "warlock.cost.usd"
610
+ };
611
+ /**
612
+ * Project a {@link TraceSpan} onto the OpenTelemetry GenAI
613
+ * semantic-convention attribute set.
614
+ *
615
+ * The vendor-neutral {@link TraceSpan} carries identity, timing, status,
616
+ * and rolled-up `usage` as first-class fields; model identity and other
617
+ * provider detail live in the free-form `attributes` bag the collector
618
+ * populated. This mapper folds both into a flat `gen_ai.*` /
619
+ * `warlock.*` attribute map ready to set on an OTel span or hand to a
620
+ * Langfuse generation.
621
+ *
622
+ * - `gen_ai.operation.name` / `gen_ai.system` / `gen_ai.request.model`
623
+ * are forwarded from the span's `attributes` when the collector set
624
+ * them; never invented here.
625
+ * - Token counts come from the span's typed `usage` rollup.
626
+ * - The free-form `attributes` are merged last so an explicit collector
627
+ * value wins over a derived one.
628
+ *
629
+ * @example
630
+ * const attributes = toGenAiAttributes(span);
631
+ * // { "gen_ai.usage.input_tokens": 150, "gen_ai.usage.output_tokens": 320, ... }
632
+ */
633
+ function toGenAiAttributes(span) {
634
+ const attributes = {
635
+ [WARLOCK_ATTRIBUTES.reportType]: span.type,
636
+ [WARLOCK_ATTRIBUTES.durationMs]: span.duration,
637
+ [WARLOCK_ATTRIBUTES.totalTokens]: span.usage.total,
638
+ [GEN_AI_ATTRIBUTES.usageInputTokens]: span.usage.input,
639
+ [GEN_AI_ATTRIBUTES.usageOutputTokens]: span.usage.output
640
+ };
641
+ if (span.version !== void 0) attributes[WARLOCK_ATTRIBUTES.version] = span.version;
642
+ if (span.sessionId !== void 0) attributes[GEN_AI_ATTRIBUTES.conversationId] = span.sessionId;
643
+ if (span.usage.cachedTokens !== void 0) attributes[WARLOCK_ATTRIBUTES.cachedTokens] = span.usage.cachedTokens;
644
+ if (span.usage.reasoningTokens !== void 0) attributes[WARLOCK_ATTRIBUTES.reasoningTokens] = span.usage.reasoningTokens;
645
+ const cost = totalCostUsd(span.usage);
646
+ if (cost !== void 0) attributes[WARLOCK_ATTRIBUTES.costUsd] = cost;
647
+ mergeScalarAttributes(attributes, span.attributes);
648
+ return attributes;
649
+ }
650
+ /**
651
+ * Copy the scalar entries of a free-form attribute bag onto the target
652
+ * map. Non-scalar values (objects, arrays, functions) are skipped — OTel
653
+ * and Langfuse attribute values must be primitives, and the collector's
654
+ * bag may legitimately hold nested digests that don't belong on a span
655
+ * attribute. Explicit collector values overwrite derived ones.
656
+ */
657
+ function mergeScalarAttributes(target, source) {
658
+ if (!source) return;
659
+ for (const [key, value] of Object.entries(source)) if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") target[key] = value;
660
+ }
661
+
662
+ //#endregion
663
+ //#region ../@warlock.js/ai-panoptic/src/exporters/console/format-span-line.ts
664
+ /**
665
+ * Render a single {@link TraceSpan} as one scannable console line:
666
+ * `<status> <type> "<name>" — <duration>ms, <tokens> tok[, $<cost>]`.
667
+ * `depth` controls leading indentation when printing a tree. Pure — no
668
+ * side effects — so it is trivially testable and reused by both the
669
+ * per-trace summary and the per-span streaming line.
670
+ *
671
+ * @example
672
+ * formatSpanLine(span, 1);
673
+ * // ' ok agent "router" — 1240ms, 470 tok, $0.0021'
674
+ */
675
+ function formatSpanLine(span, depth = 0) {
676
+ const indent = " ".repeat(depth);
677
+ const marker = statusMarker(span.status);
678
+ const cost = totalCostUsd(span.usage);
679
+ const costSuffix = cost === void 0 ? "" : `, $${cost.toFixed(4)}`;
680
+ let line = `${indent}${marker} ${span.type} "${span.name}" — ${span.duration}ms, ${span.usage.total} tok${costSuffix}`;
681
+ if (span.error) line += ` [${span.error.type}: ${span.error.message}]`;
682
+ return line;
683
+ }
684
+ /**
685
+ * Short ASCII marker for a span's terminal status. Plain ASCII (no
686
+ * emoji/color codes) so output stays clean in log aggregators and CI.
687
+ */
688
+ function statusMarker(status) {
689
+ switch (status) {
690
+ case "completed": return "ok";
691
+ case "failed": return "ERR";
692
+ case "cancelled": return "cancel";
693
+ case "max-iterations": return "max-iter";
694
+ case "awaiting-input": return "await";
695
+ default: return status;
696
+ }
697
+ }
698
+
699
+ //#endregion
700
+ //#region ../@warlock.js/ai-panoptic/src/exporters/console/console-exporter.ts
701
+ const EXPORTER_NAME$3 = "console";
702
+ /**
703
+ * Zero-dependency {@link ExporterContract} that prints traces to a
704
+ * console-like sink. The simplest exporter — useful in development and
705
+ * as the reference implementation of the contract.
706
+ *
707
+ * By default it prints one summary line per completed trace. Set
708
+ * `tree: true` to print the full indented span tree, and
709
+ * `streaming: true` to also print each span the moment it finalizes
710
+ * (via the optional `exportSpan` hook).
711
+ *
712
+ * @example
713
+ * collector.use(consoleExporter());
714
+ * // ok workflow "checkout" — 2103ms, 1820 tok, $0.0094
715
+ *
716
+ * @example
717
+ * collector.use(consoleExporter({ tree: true }));
718
+ */
719
+ function consoleExporter(options = {}) {
720
+ const sink = options.console ?? console;
721
+ const tree = options.tree ?? false;
722
+ const exporter = {
723
+ name: EXPORTER_NAME$3,
724
+ export(trace) {
725
+ writeTrace(sink, trace, tree);
726
+ }
727
+ };
728
+ if (options.streaming) exporter.exportSpan = (span) => {
729
+ sink.log(formatSpanLine(span));
730
+ };
731
+ return exporter;
732
+ }
733
+ /**
734
+ * Write a completed trace — either a single root summary line or the
735
+ * full indented tree. Failed traces are routed to `console.error` so
736
+ * they surface at the right severity in log aggregators.
737
+ */
738
+ function writeTrace(sink, trace, tree) {
739
+ if (!tree) {
740
+ const line = formatSpanLine(trace.root);
741
+ writeAtSeverity(sink, trace.root.status, line);
742
+ return;
743
+ }
744
+ for (const span of walkSpans(trace.root)) {
745
+ const line = formatSpanLine(span, spanDepth(trace.root, span.spanId));
746
+ writeAtSeverity(sink, span.status, line);
747
+ }
748
+ }
749
+ /**
750
+ * Route a line to `error` when the span failed/cancelled, otherwise to
751
+ * `log`. Keeps healthy traces out of the error stream.
752
+ */
753
+ function writeAtSeverity(sink, status, line) {
754
+ if (status === "failed" || status === "cancelled") {
755
+ sink.error(line);
756
+ return;
757
+ }
758
+ sink.log(line);
759
+ }
760
+ /**
761
+ * Depth of `targetSpanId` below `root` for indentation. Walks the tree
762
+ * once; returns 0 when the span is the root or not found.
763
+ */
764
+ function spanDepth(root, targetSpanId, depth = 0) {
765
+ if (root.spanId === targetSpanId) return depth;
766
+ for (const child of root.children) {
767
+ const found = spanDepth(child, targetSpanId, depth + 1);
768
+ if (found > 0) return found;
769
+ }
770
+ return 0;
771
+ }
772
+
773
+ //#endregion
774
+ //#region ../@warlock.js/ai-panoptic/src/exporters/file/file-exporter.ts
775
+ const EXPORTER_NAME$2 = "file";
776
+ /**
777
+ * Zero-dependency {@link ExporterContract} that appends completed traces
778
+ * to a JSON-Lines file (one JSON record per line by default). Buffers in
779
+ * memory and flushes either every `flushEvery` traces or on an explicit
780
+ * `flush()` / `shutdown()`, so a batch of traces costs one append.
781
+ *
782
+ * Useful as a durable local sink (replay traces later, ship the file to
783
+ * a backend out of band) and as a test fixture for the pipeline without
784
+ * a vendor SDK.
785
+ *
786
+ * @example
787
+ * collector.use(fileExporter({ path: "storage/traces.jsonl" }));
788
+ * // on shutdown:
789
+ * await collector.shutdown(); // drains the buffer
790
+ */
791
+ function fileExporter(options) {
792
+ const writer = new FileTraceWriter(options);
793
+ return {
794
+ name: EXPORTER_NAME$2,
795
+ async export(trace) {
796
+ await writer.add(trace);
797
+ },
798
+ async flush() {
799
+ await writer.flush();
800
+ },
801
+ async shutdown() {
802
+ await writer.flush();
803
+ }
804
+ };
805
+ }
806
+ /**
807
+ * Internal buffered writer for {@link fileExporter}. Owns the pending
808
+ * trace buffer and the directory-created guard across the exporter's
809
+ * lifetime; kept unexported so callers only ever see the factory.
810
+ */
811
+ var FileTraceWriter = class {
812
+ constructor(options) {
813
+ this.buffer = [];
814
+ this.directoryReady = false;
815
+ this.path = options.path;
816
+ this.flushEvery = Math.max(1, options.flushEvery ?? 1);
817
+ this.pretty = options.pretty ?? false;
818
+ }
819
+ /**
820
+ * Buffer one trace and flush when the buffer reaches `flushEvery`.
821
+ */
822
+ async add(trace) {
823
+ this.buffer.push({
824
+ type: "trace",
825
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
826
+ trace
827
+ });
828
+ if (this.buffer.length >= this.flushEvery) await this.flush();
829
+ }
830
+ /**
831
+ * Serialize and append every buffered record, then clear the buffer.
832
+ * No-op when nothing is pending so callers can flush defensively.
833
+ */
834
+ async flush() {
835
+ if (this.buffer.length === 0) return;
836
+ const pending = this.buffer;
837
+ this.buffer = [];
838
+ await this.ensureDirectory();
839
+ const payload = pending.map((record) => this.serialize(record)).join("");
840
+ await (0, node_fs_promises.appendFile)(this.path, payload, "utf8");
841
+ }
842
+ /**
843
+ * Create the parent directory once, lazily, on the first write. Stores
844
+ * a guard so subsequent flushes skip the syscall.
845
+ */
846
+ async ensureDirectory() {
847
+ if (this.directoryReady) return;
848
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(this.path), { recursive: true });
849
+ this.directoryReady = true;
850
+ }
851
+ /**
852
+ * Render one record as a newline-terminated JSON string. Pretty mode
853
+ * indents for human reading; compact mode keeps the file valid JSON
854
+ * Lines (exactly one record per physical line).
855
+ */
856
+ serialize(record) {
857
+ return `${this.pretty ? JSON.stringify(record, void 0, 2) : JSON.stringify(record)}\n`;
858
+ }
859
+ };
860
+
861
+ //#endregion
862
+ //#region ../@warlock.js/ai-panoptic/src/exporters/langfuse/langfuse-exporter.ts
863
+ const EXPORTER_NAME$1 = "langfuse";
864
+ let LangfuseSdk;
865
+ let isModuleExists$1 = null;
866
+ let loadingPromise$1;
867
+ const LANGFUSE_INSTALL_INSTRUCTIONS = `
868
+ The Panoptic Langfuse exporter requires the langfuse package.
869
+ Install it with:
870
+
871
+ npm install langfuse
872
+
873
+ Or with your preferred package manager:
874
+
875
+ pnpm add langfuse
876
+ yarn add langfuse
877
+ `.trim();
878
+ /**
879
+ * Settle the lazy import of `langfuse` once, concurrency-safe. Only
880
+ * needed when the caller did not pass a ready `client`. A bare `catch`
881
+ * flips the flag to `false`; the curated install string surfaces at use
882
+ * time, never a raw module-resolution stack trace.
883
+ */
884
+ function loadLangfuse() {
885
+ if (isModuleExists$1 !== null) return Promise.resolve();
886
+ if (loadingPromise$1) return loadingPromise$1;
887
+ loadingPromise$1 = (async () => {
888
+ try {
889
+ LangfuseSdk = await import("langfuse");
890
+ isModuleExists$1 = true;
891
+ } catch {
892
+ isModuleExists$1 = false;
893
+ }
894
+ })();
895
+ return loadingPromise$1;
896
+ }
897
+ /**
898
+ * {@link ExporterContract} that maps Panoptic traces onto Langfuse
899
+ * traces and observations. Lazily imports `langfuse` so it stays an
900
+ * OPTIONAL peer — importing this module never forces the SDK to be
901
+ * installed, and a missing SDK surfaces as a curated "install this"
902
+ * error when the exporter first needs to build a client.
903
+ *
904
+ * The root {@link TraceSpan} becomes a Langfuse trace; each descendant
905
+ * becomes a nested observation — a `generation` when it produced tokens
906
+ * (LLM-backed agents, supervisors), otherwise a plain `span` (tools,
907
+ * callbacks). Timing, status, version, and rolled-up `usage` are mapped
908
+ * 1:1; the rest of the GenAI attributes ride along as observation
909
+ * metadata.
910
+ *
911
+ * @example
912
+ * collector.use(langfuseExporter({ publicKey: "pk-...", secretKey: "sk-..." }));
913
+ * // or reuse an existing client:
914
+ * collector.use(langfuseExporter({ client: myLangfuse }));
915
+ */
916
+ function langfuseExporter(options) {
917
+ let client = options.client;
918
+ if (!client) loadLangfuse();
919
+ const resolveClient = async () => {
920
+ if (client) return client;
921
+ await loadLangfuse();
922
+ if (!isModuleExists$1) throw new Error(LANGFUSE_INSTALL_INSTRUCTIONS);
923
+ client = new LangfuseSdk.Langfuse({
924
+ publicKey: options.publicKey,
925
+ secretKey: options.secretKey,
926
+ baseUrl: options.baseUrl
927
+ });
928
+ return client;
929
+ };
930
+ return {
931
+ name: EXPORTER_NAME$1,
932
+ async export(trace) {
933
+ emitTrace(await resolveClient(), trace);
934
+ },
935
+ async flush() {
936
+ if (!client) return;
937
+ await client.flushAsync();
938
+ },
939
+ async shutdown() {
940
+ if (!client) return;
941
+ await client.shutdownAsync();
942
+ }
943
+ };
944
+ }
945
+ /**
946
+ * Create the Langfuse trace from the root span, then recurse the
947
+ * children into nested observations.
948
+ */
949
+ function emitTrace(client, trace) {
950
+ const root = trace.root;
951
+ const langfuseTrace = client.trace({
952
+ id: root.traceId,
953
+ name: root.name,
954
+ sessionId: trace.sessionId,
955
+ version: root.version,
956
+ timestamp: new Date(root.startedAt),
957
+ metadata: toGenAiAttributes(root)
958
+ });
959
+ for (const child of root.children) emitObservation(langfuseTrace, child);
960
+ }
961
+ /**
962
+ * Map one {@link TraceSpan} onto a Langfuse observation under `parent`,
963
+ * then recurse its children. Token-producing spans become
964
+ * `generation`s; everything else becomes a plain `span`.
965
+ */
966
+ function emitObservation(parent, span) {
967
+ const body = {
968
+ id: span.spanId,
969
+ name: span.name,
970
+ startTime: new Date(span.startedAt),
971
+ endTime: new Date(span.endedAt),
972
+ level: toLevel(span),
973
+ statusMessage: span.error?.message,
974
+ version: span.version,
975
+ metadata: toGenAiAttributes(span)
976
+ };
977
+ let observation;
978
+ if (producedTokens(span)) {
979
+ body.usage = {
980
+ input: span.usage.input,
981
+ output: span.usage.output,
982
+ total: span.usage.total,
983
+ unit: "TOKENS"
984
+ };
985
+ observation = parent.generation(body);
986
+ } else observation = parent.span(body);
987
+ for (const child of span.children) emitObservation(observation, child);
988
+ observation.end({ endTime: body.endTime });
989
+ }
990
+ /**
991
+ * A span counts as an LLM `generation` when it metered any tokens. Pure
992
+ * tools/callbacks contribute zero own-cost and map to plain spans.
993
+ *
994
+ * **Known limitation.** `TraceSpan.usage` is the ROLLED-UP usage (this
995
+ * node's own cost plus the sum of its children), not own-usage — the
996
+ * core `BaseReport` exposes no separate own-usage field, so a composite
997
+ * node (e.g. an agent that itself made no model call but whose tool
998
+ * children did) is classified as a `generation` on its children's
999
+ * tokens. We cannot classify on own-usage until the report shape carries
1000
+ * it; the rolled-up `usage` block emitted on such a generation therefore
1001
+ * double-counts tokens already attributed to descendant observations.
1002
+ */
1003
+ function producedTokens(span) {
1004
+ return span.usage.total > 0;
1005
+ }
1006
+ /**
1007
+ * Map the Panoptic span status onto a Langfuse observation level —
1008
+ * failed/cancelled spans surface as `ERROR`, everything else `DEFAULT`.
1009
+ */
1010
+ function toLevel(span) {
1011
+ if (span.status === "failed" || span.status === "cancelled") return "ERROR";
1012
+ return "DEFAULT";
1013
+ }
1014
+
1015
+ //#endregion
1016
+ //#region ../@warlock.js/ai-panoptic/src/exporters/otel/otel-exporter.ts
1017
+ const EXPORTER_NAME = "otel";
1018
+ const DEFAULT_TRACER_NAME = "@warlock.js/ai-panoptic";
1019
+ let OtelApi;
1020
+ let isModuleExists = null;
1021
+ let loadingPromise;
1022
+ const OTEL_INSTALL_INSTRUCTIONS = `
1023
+ The Panoptic OpenTelemetry exporter requires the @opentelemetry/api package.
1024
+ Install it with:
1025
+
1026
+ npm install @opentelemetry/api
1027
+
1028
+ Or with your preferred package manager:
1029
+
1030
+ pnpm add @opentelemetry/api
1031
+ yarn add @opentelemetry/api
1032
+ `.trim();
1033
+ /**
1034
+ * Settle the lazy import of `@opentelemetry/api` once, concurrency-safe.
1035
+ * A bare `catch` flips the flag to `false`; the curated install string
1036
+ * surfaces at use time so a missing SDK never throws a raw module
1037
+ * resolution error.
1038
+ */
1039
+ function loadOtel() {
1040
+ if (isModuleExists !== null) return Promise.resolve();
1041
+ if (loadingPromise) return loadingPromise;
1042
+ loadingPromise = (async () => {
1043
+ try {
1044
+ OtelApi = await import("@opentelemetry/api");
1045
+ isModuleExists = true;
1046
+ } catch {
1047
+ isModuleExists = false;
1048
+ }
1049
+ })();
1050
+ return loadingPromise;
1051
+ }
1052
+ /**
1053
+ * {@link ExporterContract} that maps Panoptic traces onto OpenTelemetry
1054
+ * spans following the GenAI semantic conventions (`gen_ai.*`
1055
+ * attributes). Lazily imports `@opentelemetry/api` so it stays an
1056
+ * OPTIONAL peer — importing this module never forces the SDK to be
1057
+ * installed, and a missing SDK surfaces as a curated "install this"
1058
+ * error on first `export`, not a boot-time stack trace.
1059
+ *
1060
+ * The exporter emits onto a `Tracer` you supply (or fetches one from the
1061
+ * globally registered provider). It never configures the SDK — wiring a
1062
+ * `TracerProvider`, processors, and span exporters is the host app's
1063
+ * job, exactly as with any other OTel instrumentation.
1064
+ *
1065
+ * Each {@link TraceSpan} becomes one OTel span with the source span's
1066
+ * start/end times and parent relationship reconstructed, so the emitted
1067
+ * tree matches the original execution tree.
1068
+ *
1069
+ * @example
1070
+ * // app already set up @opentelemetry/sdk-trace-base + a provider
1071
+ * collector.use(otelExporter({ tracerName: "my-app", system: "openai" }));
1072
+ */
1073
+ function otelExporter(options = {}) {
1074
+ loadOtel();
1075
+ return {
1076
+ name: EXPORTER_NAME,
1077
+ async export(trace) {
1078
+ await loadOtel();
1079
+ if (!isModuleExists) throw new Error(OTEL_INSTALL_INSTRUCTIONS);
1080
+ emitSpan(resolveTracer(options), trace.root, void 0, options);
1081
+ }
1082
+ };
1083
+ }
1084
+ /**
1085
+ * Resolve the `Tracer` spans are emitted on — the caller-supplied one,
1086
+ * or one fetched from the globally registered provider by name.
1087
+ */
1088
+ function resolveTracer(options) {
1089
+ if (options.tracer) return options.tracer;
1090
+ return OtelApi.trace.getTracer(options.tracerName ?? DEFAULT_TRACER_NAME, options.tracerVersion);
1091
+ }
1092
+ /**
1093
+ * Recreate one {@link TraceSpan} (and its subtree) as OTel spans. The
1094
+ * span is started with the source `startedAt`, parented under
1095
+ * `parentContext` so the tree is preserved, annotated with GenAI
1096
+ * attributes, given the mapped status, and ended at `endedAt`. Children
1097
+ * recurse under this span's context.
1098
+ */
1099
+ function emitSpan(tracer, span, parentContext, options) {
1100
+ const startTime = toEpochMillis(span.startedAt);
1101
+ const baseContext = parentContext ?? OtelApi.context.active();
1102
+ const otelSpan = tracer.startSpan(span.name, { startTime }, baseContext);
1103
+ applyAttributes(otelSpan, span, options);
1104
+ applyStatus(otelSpan, span);
1105
+ const childContext = OtelApi.trace.setSpan(baseContext, otelSpan);
1106
+ for (const child of span.children) emitSpan(tracer, child, childContext, options);
1107
+ otelSpan.end(toEpochMillis(span.endedAt));
1108
+ }
1109
+ /**
1110
+ * Set the GenAI + Warlock attributes on the OTel span, defaulting
1111
+ * `gen_ai.system` from the exporter options when the span carried none.
1112
+ */
1113
+ function applyAttributes(otelSpan, span, options) {
1114
+ const attributes = toGenAiAttributes(span);
1115
+ if (options.system !== void 0 && attributes[GEN_AI_ATTRIBUTES.system] === void 0) attributes[GEN_AI_ATTRIBUTES.system] = options.system;
1116
+ otelSpan.setAttributes(attributes);
1117
+ }
1118
+ /**
1119
+ * Map the Panoptic span status onto the OTel span status, recording the
1120
+ * normalized error as an exception event + ERROR status when present.
1121
+ */
1122
+ function applyStatus(otelSpan, span) {
1123
+ const codes = OtelApi.SpanStatusCode;
1124
+ if (span.error) otelSpan.recordException({
1125
+ name: span.error.type,
1126
+ message: span.error.message,
1127
+ stack: span.error.stack
1128
+ });
1129
+ if (span.status === "failed" || span.status === "cancelled") {
1130
+ otelSpan.setStatus({
1131
+ code: codes.ERROR,
1132
+ message: span.error?.message
1133
+ });
1134
+ return;
1135
+ }
1136
+ if (span.status === "max-iterations" || span.status === "awaiting-input") {
1137
+ otelSpan.setStatus({
1138
+ code: codes.UNSET,
1139
+ message: span.status === "max-iterations" ? "Run hit the iteration cap without an explicit end" : "Run is awaiting the next input turn"
1140
+ });
1141
+ return;
1142
+ }
1143
+ otelSpan.setStatus({ code: codes.OK });
1144
+ }
1145
+ /**
1146
+ * Convert an ISO-8601 timestamp to epoch milliseconds — the `TimeInput`
1147
+ * form OTel's `startSpan` / `Span.end` accept directly.
1148
+ */
1149
+ function toEpochMillis(isoTimestamp) {
1150
+ return new Date(isoTimestamp).getTime();
1151
+ }
1152
+
1153
+ //#endregion
1154
+ //#region ../@warlock.js/ai-panoptic/src/panoptic/panoptic-middleware.ts
1155
+ /**
1156
+ * Build an {@link AgentMiddleware} that feeds a collector from the
1157
+ * `execute`- and `supervisor`-level hooks. An alternative wiring to event
1158
+ * subscription for apps that already compose cross-cutting concerns
1159
+ * through the agent middleware pipeline (`[cache, budget, guardrail,
1160
+ * observability]`). Declaring both hook maps lets a single middleware
1161
+ * object work uniformly on agents (which fire the `execute` map) and
1162
+ * supervisors (which fire the `supervisor` map) — registering it on a
1163
+ * supervisor would otherwise install and collect nothing silently.
1164
+ *
1165
+ * Both terminal paths are covered on each surface:
1166
+ * - `after` — fires on a run that produced a result. A run can complete
1167
+ * with `result.error` populated (the engine still calls `after`), so
1168
+ * the report AND the envelope error are collected; the error type and
1169
+ * message land on the root span.
1170
+ * - `onError` — fires when the run threw before assembling a result. The
1171
+ * error carries the partial result's report on its envelope; when
1172
+ * present it is collected, with the error itself threaded onto the
1173
+ * root span so failed runs still produce a trace.
1174
+ *
1175
+ * The hooks never return a value, so they never mutate the agent's /
1176
+ * supervisor's result. The `collect` call is fire-and-forget relative to
1177
+ * the run — the collector isolates exporter failures internally, and we
1178
+ * additionally swallow any rejection here so an observability fault can
1179
+ * never surface on the run's hot path.
1180
+ *
1181
+ * @param collector - the collector traces are fed into.
1182
+ * @param name - stable middleware name (kebab-case). Defaults to
1183
+ * `"panoptic"`.
1184
+ */
1185
+ function createPanopticMiddleware(collector, name = "panoptic") {
1186
+ const collectReport = (report, rootError) => {
1187
+ if (!isReport(report)) return;
1188
+ collector.collect(report, rootError).catch(() => {});
1189
+ };
1190
+ const onResult = (result) => {
1191
+ collectReport(result.report, result.error);
1192
+ };
1193
+ const onError = (error) => {
1194
+ collectReport(error.report, error);
1195
+ };
1196
+ const terminalHooks = {
1197
+ after(_ctx, result) {
1198
+ onResult(result);
1199
+ },
1200
+ onError(_ctx, error) {
1201
+ onError(error);
1202
+ }
1203
+ };
1204
+ return {
1205
+ name,
1206
+ execute: terminalHooks,
1207
+ supervisor: terminalHooks
1208
+ };
1209
+ }
1210
+ /**
1211
+ * Narrow an unknown value to a `BaseReport`-shaped object. Structural
1212
+ * (checks the lineage fields the collector reads) so it accepts any
1213
+ * primitive's report subtype without importing each concrete type.
1214
+ */
1215
+ function isReport(value) {
1216
+ return typeof value === "object" && value !== null && typeof value.runId === "string" && typeof value.rootRunId === "string";
1217
+ }
1218
+
1219
+ //#endregion
1220
+ //#region ../@warlock.js/ai-panoptic/src/panoptic/panoptic.ts
1221
+ /**
1222
+ * Terminal `*.completed` events of every core primitive that carries the
1223
+ * finalized `result` (and therefore the `report` tree). These fire once
1224
+ * per run regardless of outcome — the matching `*.error` event fires
1225
+ * first on failure, then `*.completed` still fires — so subscribing here
1226
+ * captures completed, failed, and cancelled runs alike.
1227
+ *
1228
+ * The orchestrator is intentionally absent: its `orchestrator.turn.*`
1229
+ * events carry only session identity, not a result. Collect an
1230
+ * orchestrator turn via {@link Panoptic.collect} with
1231
+ * `result.report` instead.
1232
+ */
1233
+ const DEFAULT_COMPLETED_EVENTS = [
1234
+ "agent.completed",
1235
+ "workflow.completed",
1236
+ "supervisor.completed"
1237
+ ];
1238
+ /**
1239
+ * The Panoptic subscriber — binds a collector + its exporters to the
1240
+ * three feed paths (events, middleware, direct). Instantiated via
1241
+ * {@link panoptic}; callers never see `new`.
1242
+ */
1243
+ var PanopticSubscriber = class {
1244
+ constructor(options = {}) {
1245
+ this.collector = options.collector ?? createCollector();
1246
+ for (const exporter of options.exporters ?? []) this.collector.use(exporter);
1247
+ this.completedEvents = options.completedEvents ?? [...DEFAULT_COMPLETED_EVENTS];
1248
+ this.middlewareName = options.middlewareName ?? "panoptic";
1249
+ }
1250
+ use(exporter) {
1251
+ this.collector.use(exporter);
1252
+ return this;
1253
+ }
1254
+ attach(target) {
1255
+ const unsubscribes = [];
1256
+ for (const event of this.completedEvents) {
1257
+ const unsubscribe = target.on(event, (payload) => {
1258
+ this.handleCompleted(payload);
1259
+ });
1260
+ unsubscribes.push(unsubscribe);
1261
+ }
1262
+ return () => {
1263
+ for (const unsubscribe of unsubscribes) unsubscribe();
1264
+ };
1265
+ }
1266
+ middleware() {
1267
+ return createPanopticMiddleware(this.collector, this.middlewareName);
1268
+ }
1269
+ async collect(report) {
1270
+ await this.collector.collect(report);
1271
+ }
1272
+ toTrace(report) {
1273
+ return this.collector.toTrace(report);
1274
+ }
1275
+ async flush() {
1276
+ await this.collector.flush();
1277
+ }
1278
+ async shutdown() {
1279
+ await this.collector.shutdown();
1280
+ }
1281
+ /**
1282
+ * Project one terminal `*.completed` payload's report into the
1283
+ * collector. The fan-out is fire-and-forget relative to the emitting
1284
+ * run: the core swallows handler errors, the collector isolates
1285
+ * exporter failures, and we additionally guard the rejection here so an
1286
+ * observability fault never escapes the event handler.
1287
+ */
1288
+ handleCompleted(payload) {
1289
+ const report = readReport(payload);
1290
+ if (!report) return;
1291
+ const rootError = readResultError(payload);
1292
+ this.collector.collect(report, rootError).catch(() => {});
1293
+ }
1294
+ };
1295
+ /**
1296
+ * Read the envelope error off a primitive's completed-event payload
1297
+ * (`{ result: { error } }`). The error rides on the result envelope, not
1298
+ * the report tree, so the collector needs it separately to populate a
1299
+ * failed root span. Returns `undefined` when the run succeeded.
1300
+ */
1301
+ function readResultError(payload) {
1302
+ return (payload?.result)?.error;
1303
+ }
1304
+ /**
1305
+ * Read the `report` tree off a primitive's completed-event payload.
1306
+ * Structural (no concrete-type import) so it accepts every primitive's
1307
+ * result subtype; returns `undefined` when the payload isn't the
1308
+ * expected `{ result: { report } }` shape.
1309
+ */
1310
+ function readReport(payload) {
1311
+ const report = (payload?.result)?.report;
1312
+ if (typeof report === "object" && report !== null && typeof report.runId === "string" && typeof report.rootRunId === "string") return report;
1313
+ }
1314
+ /**
1315
+ * Create a Panoptic subscriber — the one-call entry point that wires the
1316
+ * observability pipeline. Pass the exporters you want and Panoptic
1317
+ * builds a collector, registers them, and hands back a subscriber you can
1318
+ * `attach()` to any agent/workflow/supervisor, install as agent
1319
+ * `middleware()`, or feed reports to directly with `collect()`.
1320
+ *
1321
+ * @example
1322
+ * // Attach to a primitive's event stream (captures every run):
1323
+ * const observe = panoptic({
1324
+ * exporters: [consoleExporter(), otelExporter({ tracerName: "app" })],
1325
+ * });
1326
+ *
1327
+ * const agent = ai.agent({ model });
1328
+ * const detach = observe.attach(agent);
1329
+ *
1330
+ * await agent.execute("Summarize this");
1331
+ * // ...later, on shutdown:
1332
+ * await observe.shutdown();
1333
+ *
1334
+ * @example
1335
+ * // Or wire it through the agent middleware pipeline:
1336
+ * const observe = panoptic({ exporters: [langfuseExporter({ ... })] });
1337
+ * const agent = ai.agent({ model, middleware: [observe.middleware()] });
1338
+ *
1339
+ * @example
1340
+ * // Orchestrator turns carry no result-bearing event — collect directly:
1341
+ * const result = await orchestrator.execute(input, { sessionId });
1342
+ * await observe.collect(result.report);
1343
+ */
1344
+ function panoptic(options = {}) {
1345
+ return new PanopticSubscriber(options);
1346
+ }
1347
+
1348
+ //#endregion
1349
+ exports.GEN_AI_ATTRIBUTES = GEN_AI_ATTRIBUTES;
1350
+ exports.WARLOCK_ATTRIBUTES = WARLOCK_ATTRIBUTES;
1351
+ exports.consoleExporter = consoleExporter;
1352
+ exports.createCollector = createCollector;
1353
+ exports.createInMemoryTraceStore = createInMemoryTraceStore;
1354
+ exports.createPanopticMiddleware = createPanopticMiddleware;
1355
+ exports.emptyUsage = emptyUsage;
1356
+ exports.extractSpanAttributes = extractSpanAttributes;
1357
+ exports.fileExporter = fileExporter;
1358
+ exports.formatSpanLine = formatSpanLine;
1359
+ exports.langfuseExporter = langfuseExporter;
1360
+ exports.matchTrace = matchTrace;
1361
+ exports.normalizeError = normalizeError;
1362
+ exports.otelExporter = otelExporter;
1363
+ exports.panoptic = panoptic;
1364
+ exports.reportToSpan = reportToSpan;
1365
+ exports.reportToTrace = reportToTrace;
1366
+ exports.sumUsage = sumUsage;
1367
+ exports.toGenAiAttributes = toGenAiAttributes;
1368
+ exports.totalCostUsd = totalCostUsd;
1369
+ exports.walkSpans = walkSpans;
1370
+ //# sourceMappingURL=index.cjs.map