@runtypelabs/flue-otel 0.5.4 → 0.5.6

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/dist/index.cjs CHANGED
@@ -1,1255 +1,1406 @@
1
- "use strict";
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __hasOwnProp = Object.prototype.hasOwnProperty;
6
- var __export = (target, all) => {
7
- for (var name in all)
8
- __defProp(target, name, { get: all[name], enumerable: true });
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
-
20
- // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
23
- ADAPTER_NAME: () => ADAPTER_NAME,
24
- ADAPTER_VERSION: () => ADAPTER_VERSION,
25
- DEFAULT_CONTENT_MAX_CHARS: () => DEFAULT_CONTENT_MAX_CHARS,
26
- GEN_AI: () => GEN_AI,
27
- GEN_AI_CONTENT: () => GEN_AI_CONTENT,
28
- INGEST_CONTENT_ATTRIBUTE_CEILING: () => INGEST_CONTENT_ATTRIBUTE_CEILING,
29
- RUNTYPE: () => RUNTYPE,
30
- RUNTYPE_FLUE_INSTRUMENTATION_KEY: () => RUNTYPE_FLUE_INSTRUMENTATION_KEY,
31
- RUNTYPE_SCHEMA_VERSION: () => RUNTYPE_SCHEMA_VERSION,
32
- RUNTYPE_STOP_REASONS: () => RUNTYPE_STOP_REASONS,
33
- RUNTYPE_TOOL_TYPES: () => RUNTYPE_TOOL_TYPES,
34
- createRuntypeFlueInstrumentation: () => createRuntypeFlueInstrumentation,
35
- runtypeFlueResourceAttributes: () => runtypeFlueResourceAttributes
36
- });
37
- module.exports = __toCommonJS(index_exports);
38
- var import_api2 = require("@opentelemetry/api");
39
-
40
- // src/compat.ts
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _opentelemetry_api = require("@opentelemetry/api");
3
+ //#region src/compat.ts
4
+ /**
5
+ * Infer implementation class only from datastore semantics or the framework task tool. Origin
6
+ * identifies who initiated a call, not its tool type; unsupported classes remain undefined.
7
+ */
41
8
  function resolveToolType(toolName, detail) {
42
- if (isTaskDelegationTool(toolName, detail)) return "subagent";
43
- if (detail.toolType === "datastore") return "data_connection";
44
- return void 0;
9
+ if (isTaskDelegationTool(toolName, detail)) return "subagent";
10
+ if (detail.toolType === "datastore") return "data_connection";
45
11
  }
12
+ /**
13
+ * Recognize framework task delegation. Keep its subagent tool span rather than a nested invoke_agent
14
+ * envelope, since one trace represents one execution.
15
+ */
46
16
  function isTaskDelegationTool(toolName, detail) {
47
- return detail.origin === "framework" && toolName === "task";
17
+ return detail.origin === "framework" && toolName === "task";
48
18
  }
19
+ /**
20
+ * Recognize host-initiated shell calls. They produce bookkeeping spans, not model tool rows.
21
+ */
49
22
  function isCallerShellCall(toolName, detail) {
50
- return detail.origin === "caller" && toolName === "bash";
23
+ return detail.origin === "caller" && toolName === "bash";
51
24
  }
25
+ /**
26
+ * Normalize finish reasons. Terminal tool_call/toolUse maps to unknown: it does not identify whether a
27
+ * turn cap, tool cap, or host abort ended the run.
28
+ */
52
29
  function mapFinishReason(finishReason) {
53
- if (!finishReason) return void 0;
54
- const normalized = finishReason.toLowerCase().replace(/[\s-]+/g, "_");
55
- if (END_TURN_REASONS.has(normalized)) return "end_turn";
56
- if (LENGTH_REASONS.has(normalized)) return "length";
57
- if (CONTENT_FILTER_REASONS.has(normalized)) return "content_filter";
58
- if (ERROR_REASONS.has(normalized)) return "error";
59
- return "unknown";
30
+ if (!finishReason) return void 0;
31
+ const normalized = finishReason.toLowerCase().replace(/[\s-]+/g, "_");
32
+ if (END_TURN_REASONS.has(normalized)) return "end_turn";
33
+ if (LENGTH_REASONS.has(normalized)) return "length";
34
+ if (CONTENT_FILTER_REASONS.has(normalized)) return "content_filter";
35
+ if (ERROR_REASONS.has(normalized)) return "error";
36
+ return "unknown";
60
37
  }
61
- var END_TURN_REASONS = /* @__PURE__ */ new Set([
62
- "stop",
63
- "end_turn",
64
- "stop_sequence",
65
- "complete",
66
- "completed"
38
+ const END_TURN_REASONS = /* @__PURE__ */ new Set([
39
+ "stop",
40
+ "end_turn",
41
+ "stop_sequence",
42
+ "complete",
43
+ "completed"
67
44
  ]);
68
- var LENGTH_REASONS = /* @__PURE__ */ new Set(["length", "max_tokens", "maxtokens"]);
69
- var CONTENT_FILTER_REASONS = /* @__PURE__ */ new Set([
70
- "content_filter",
71
- "contentfilter",
72
- "refusal",
73
- "safety"
45
+ const LENGTH_REASONS = /* @__PURE__ */ new Set([
46
+ "length",
47
+ "max_tokens",
48
+ "maxtokens"
74
49
  ]);
75
- var ERROR_REASONS = /* @__PURE__ */ new Set(["error", "aborted", "abort", "cancelled"]);
50
+ const CONTENT_FILTER_REASONS = /* @__PURE__ */ new Set([
51
+ "content_filter",
52
+ "contentfilter",
53
+ "refusal",
54
+ "safety"
55
+ ]);
56
+ const ERROR_REASONS = /* @__PURE__ */ new Set([
57
+ "error",
58
+ "aborted",
59
+ "abort",
60
+ "cancelled"
61
+ ]);
62
+ /**
63
+ * A known submission settlement outranks a turn finish reason because it describes the run. completed
64
+ * does not explain why it stopped, so retain the terminal turn reason.
65
+ */
76
66
  function mapSettlementOutcome(outcome) {
77
- if (outcome === "failed" || outcome === "aborted") return "error";
78
- return void 0;
67
+ if (outcome === "failed" || outcome === "aborted") return "error";
79
68
  }
80
-
81
- // src/messages.ts
82
- var ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
83
- var FLUE_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
84
- var SIGNAL_MESSAGE = /^<([A-Za-z_][\w.-]*) type="[^"]*"[^>]*>\n[\s\S]*\n<\/\1>$/;
85
- var COMPACTION_SUMMARY_PREFIX = '<compaction type="context_summary"';
86
- var HTML_TYPED_TAGS = /* @__PURE__ */ new Set([
87
- "a",
88
- "button",
89
- "embed",
90
- "input",
91
- "link",
92
- "menu",
93
- "object",
94
- "ol",
95
- "script",
96
- "source",
97
- "style",
98
- "ul"
69
+ //#endregion
70
+ //#region src/messages.ts
71
+ const ROLES = /* @__PURE__ */ new Set([
72
+ "system",
73
+ "user",
74
+ "assistant"
75
+ ]);
76
+ const FLUE_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
77
+ /**
78
+ * A Flue signal exactly as `renderSignalMessage` frames it: one element whose first attribute is
79
+ * `type`, body on its own lines. Tag names follow Flue's `[A-Za-z_][A-Za-z0-9_.-]*`; attribute names
80
+ * are caller-defined, so the tail is `[^>]*`. A text-shape test can only REDUCE misclassification: a
81
+ * user's `<p>hi</p>` stays a user row, and so do HTML elements that conventionally lead with `type`.
82
+ */
83
+ const SIGNAL_MESSAGE = /^<([A-Za-z_][\w.-]*) type="[^"]*"[^>]*>\n[\s\S]*\n<\/\1>$/;
84
+ /** The row Flue puts first after a compaction, standing in for everything it dropped. */
85
+ const COMPACTION_SUMMARY_PREFIX = "<compaction type=\"context_summary\"";
86
+ /** HTML elements whose `type` attribute conventionally comes first; a user pasting one is not a signal. */
87
+ const HTML_TYPED_TAGS = /* @__PURE__ */ new Set([
88
+ "a",
89
+ "button",
90
+ "embed",
91
+ "input",
92
+ "link",
93
+ "menu",
94
+ "object",
95
+ "ol",
96
+ "script",
97
+ "source",
98
+ "style",
99
+ "ul"
99
100
  ]);
101
+ /**
102
+ * Flue's `LlmMessage[]` as text-only semconv messages, in order. A `toolResult` message, a system
103
+ * row, and tool-call / thinking / image parts are dropped: tool content and the system prompt have
104
+ * their own switches, and the rest is never exported. A Flue signal — delivered to the model as a
105
+ * user message — becomes a `system` row, verbatim, so it is never read as something the user said.
106
+ */
100
107
  function projectFlueMessages(value) {
101
- if (!Array.isArray(value)) return [];
102
- const out = [];
103
- for (const entry of value) {
104
- const message = projectFlueMessage(entry);
105
- if (message) out.push(message);
106
- }
107
- return out;
108
+ if (!Array.isArray(value)) return [];
109
+ const out = [];
110
+ for (const entry of value) {
111
+ const message = projectFlueMessage(entry);
112
+ if (message) out.push(message);
113
+ }
114
+ return out;
108
115
  }
116
+ /** One Flue message as a semconv message, or undefined when it carries no text a reader could show. */
109
117
  function projectFlueMessage(value) {
110
- if (!isRecord(value)) return void 0;
111
- const role = value.role;
112
- if (typeof role !== "string" || !FLUE_ROLES.has(role)) return void 0;
113
- const text = flueMessageText(value.content);
114
- if (text === "") return void 0;
115
- if (role === "user" && isFlueSignal(value.content, text)) {
116
- return { role: "system", parts: [{ type: "text", content: text }] };
117
- }
118
- return { role, parts: [{ type: "text", content: text }] };
118
+ if (!isRecord(value)) return void 0;
119
+ const role = value.role;
120
+ if (typeof role !== "string" || !FLUE_ROLES.has(role)) return void 0;
121
+ const text = flueMessageText(value.content);
122
+ if (text === "") return void 0;
123
+ if (role === "user" && isFlueSignal(value.content, text)) return {
124
+ role: "system",
125
+ parts: [{
126
+ type: "text",
127
+ content: text
128
+ }]
129
+ };
130
+ return {
131
+ role,
132
+ parts: [{
133
+ type: "text",
134
+ content: text
135
+ }]
136
+ };
119
137
  }
138
+ /** Whether a user-role body is a Flue signal: exact framing, one text, not an HTML `type`-first tag. */
120
139
  function isFlueSignal(content, text) {
121
- if (!isSingleText(content)) return false;
122
- const signal = SIGNAL_MESSAGE.exec(text);
123
- return signal !== null && !HTML_TYPED_TAGS.has(signal[1].toLowerCase());
140
+ if (!isSingleText(content)) return false;
141
+ const signal = SIGNAL_MESSAGE.exec(text);
142
+ return signal !== null && !HTML_TYPED_TAGS.has(signal[1].toLowerCase());
124
143
  }
144
+ /** Flue renders a signal as one string or one text part; a multi-part body is a user's own message. */
125
145
  function isSingleText(content) {
126
- if (typeof content === "string") return true;
127
- return Array.isArray(content) && content.length === 1;
146
+ if (typeof content === "string") return true;
147
+ return Array.isArray(content) && content.length === 1;
128
148
  }
149
+ /** The visible text of a Flue message body: a string as-is, or its `text` parts joined by newlines. */
129
150
  function flueMessageText(content) {
130
- return contentMessageText({ content });
151
+ return contentMessageText({ content });
131
152
  }
153
+ /**
154
+ * Re-read semconv messages after the redaction hook, which may return anything. Accepts the ratified
155
+ * `{ role, parts }` shape, the `{ role, content }` shorthand with a string or Flue-style text parts,
156
+ * and text parts keyed `content` or `text`; drops everything else.
157
+ */
132
158
  function normalizeContentMessages(value) {
133
- if (!Array.isArray(value)) return [];
134
- const out = [];
135
- for (const entry of value) {
136
- if (!isRecord(entry)) continue;
137
- const role = entry.role;
138
- if (typeof role !== "string" || !ROLES.has(role)) continue;
139
- const text = contentMessageText(entry);
140
- if (text === "") continue;
141
- out.push({ role, parts: [{ type: "text", content: text }] });
142
- }
143
- return out;
159
+ if (!Array.isArray(value)) return [];
160
+ const out = [];
161
+ for (const entry of value) {
162
+ if (!isRecord(entry)) continue;
163
+ const role = entry.role;
164
+ if (typeof role !== "string" || !ROLES.has(role)) continue;
165
+ const text = contentMessageText(entry);
166
+ if (text === "") continue;
167
+ out.push({
168
+ role,
169
+ parts: [{
170
+ type: "text",
171
+ content: text
172
+ }]
173
+ });
174
+ }
175
+ return out;
144
176
  }
177
+ /** The text of a redacted system prompt: a string, or Flue-style / semconv text parts; else empty. */
145
178
  function contentTextOf(value) {
146
- if (typeof value === "string") return value;
147
- return Array.isArray(value) ? contentMessageText({ parts: value }) : "";
179
+ if (typeof value === "string") return value;
180
+ return Array.isArray(value) ? contentMessageText({ parts: value }) : "";
148
181
  }
149
182
  function contentMessageText(message) {
150
- if (typeof message.content === "string") return message.content;
151
- const parts = Array.isArray(message.parts) ? message.parts : Array.isArray(message.content) ? message.content : [];
152
- const texts = [];
153
- for (const part of parts) {
154
- if (!isRecord(part) || part.type !== "text") continue;
155
- const text = typeof part.content === "string" && part.content.length > 0 ? part.content : typeof part.text === "string" && part.text.length > 0 ? part.text : "";
156
- if (text !== "") texts.push(text);
157
- }
158
- return texts.join("\n");
183
+ if (typeof message.content === "string") return message.content;
184
+ const parts = Array.isArray(message.parts) ? message.parts : Array.isArray(message.content) ? message.content : [];
185
+ const texts = [];
186
+ for (const part of parts) {
187
+ if (!isRecord(part) || part.type !== "text") continue;
188
+ const text = typeof part.content === "string" && part.content.length > 0 ? part.content : typeof part.text === "string" && part.text.length > 0 ? part.text : "";
189
+ if (text !== "") texts.push(text);
190
+ }
191
+ return texts.join("\n");
159
192
  }
160
193
  function isRecord(value) {
161
- return value !== null && typeof value === "object" && !Array.isArray(value);
194
+ return value !== null && typeof value === "object" && !Array.isArray(value);
162
195
  }
163
-
164
- // src/content.ts
165
- var DEFAULT_CONTENT_MAX_CHARS = 65536;
166
- var INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
167
- var INGEST_CONTENT_SPAN_CEILING = 393216;
196
+ //#endregion
197
+ //#region src/content.ts
198
+ /**
199
+ * The default per-value ceiling: 64 KiB of ASCII. Large enough for any
200
+ * realistic tool argument object and most results, and small enough that
201
+ * arguments plus result on one span sit well inside ingest's 384 KiB per-span
202
+ * ceiling — a customer who raises it should know that ceiling exists.
203
+ */
204
+ const DEFAULT_CONTENT_MAX_CHARS = 65536;
205
+ /**
206
+ * Inline the private ingest per-attribute ceiling so npm installs need no private dependency.
207
+ * tests/contract.test.ts pins equality.
208
+ */
209
+ const INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
210
+ /**
211
+ * Ingest's per-SPAN content ceiling, inlined for the same reason. The envelope carries up to three
212
+ * message values and a tool span two, so each is capped at its share; tests/contract.test.ts pins
213
+ * equality.
214
+ */
215
+ const INGEST_CONTENT_SPAN_CEILING = 393216;
168
216
  function resolveContentPolicy(setting) {
169
- const options = setting === false ? void 0 : setting;
170
- const on = (value) => setting !== false && value !== false;
171
- const requested = options?.maxChars;
172
- const maxChars = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(Math.floor(requested), INGEST_CONTENT_ATTRIBUTE_CEILING) : DEFAULT_CONTENT_MAX_CHARS;
173
- return {
174
- toolArguments: on(options?.toolArguments),
175
- toolResults: on(options?.toolResults),
176
- inputMessages: on(options?.inputMessages),
177
- outputMessages: on(options?.outputMessages),
178
- systemInstructions: on(options?.systemInstructions),
179
- maxChars,
180
- envelopeMaxChars: Math.min(maxChars, Math.floor(INGEST_CONTENT_SPAN_CEILING / 3)),
181
- toolMaxChars: Math.min(maxChars, Math.floor(INGEST_CONTENT_SPAN_CEILING / 2)),
182
- redact: options?.redact
183
- };
217
+ const options = setting === false ? void 0 : setting;
218
+ const on = (value) => setting !== false && value !== false;
219
+ const requested = options?.maxChars;
220
+ const maxChars = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(Math.floor(requested), INGEST_CONTENT_ATTRIBUTE_CEILING) : DEFAULT_CONTENT_MAX_CHARS;
221
+ return {
222
+ toolArguments: on(options?.toolArguments),
223
+ toolResults: on(options?.toolResults),
224
+ inputMessages: on(options?.inputMessages),
225
+ outputMessages: on(options?.outputMessages),
226
+ systemInstructions: on(options?.systemInstructions),
227
+ maxChars,
228
+ envelopeMaxChars: Math.min(maxChars, Math.floor(INGEST_CONTENT_SPAN_CEILING / 3)),
229
+ toolMaxChars: Math.min(maxChars, Math.floor(INGEST_CONTENT_SPAN_CEILING / 2)),
230
+ redact: options?.redact
231
+ };
184
232
  }
185
- function encodeContentValue(value, policy, context3) {
186
- if (value === void 0) return void 0;
187
- const redacted = applyRedact(value, policy, context3);
188
- if (redacted === void 0) return void 0;
189
- if (context3.kind === "arguments") return encodeArguments(redacted, policy.toolMaxChars);
190
- const encoded = stringify(redacted);
191
- if (encoded === void 0) return void 0;
192
- return truncate(encoded, policy.toolMaxChars);
233
+ /**
234
+ * Encode one payload without propagating serialization or redaction failures. Missing values,
235
+ * unsupported JSON values, undefined redactions, and thrown redactions emit nothing.
236
+ */
237
+ function encodeContentValue(value, policy, context) {
238
+ if (value === void 0) return void 0;
239
+ const redacted = applyRedact(value, policy, context);
240
+ if (redacted === void 0) return void 0;
241
+ if (context.kind === "arguments") return encodeArguments(redacted, policy.toolMaxChars);
242
+ const encoded = stringify(redacted);
243
+ if (encoded === void 0) return void 0;
244
+ return truncate(encoded, policy.toolMaxChars);
193
245
  }
246
+ /**
247
+ * Encode semconv messages as one JSON array under the envelope ceiling. Over budget, the OLDEST
248
+ * messages are dropped whole first so the newest one survives; a lone message that still does not
249
+ * fit has its text cut with the truncation marker. Undefined when nothing survives redaction.
250
+ */
194
251
  function encodeMessagesValue(messages, policy, kind) {
195
- if (messages.length === 0) return void 0;
196
- const redacted = applyRedact(messages, policy, { kind });
197
- if (redacted === void 0) return void 0;
198
- const all = normalizeContentMessages(redacted);
199
- if (all.length === 0) return void 0;
200
- const maxChars = policy.envelopeMaxChars;
201
- const perMessage = all.length > 1 ? Math.floor((maxChars - 3) / 2) : maxChars - 2;
202
- const encoded = [];
203
- for (const message of all) {
204
- const one = JSON.stringify(message);
205
- encoded.push(one.length <= perMessage ? one : encodeTruncatedMessage(message, perMessage) ?? "");
206
- }
207
- let start = all.length;
208
- let total = 2;
209
- while (start > 0) {
210
- const length = encoded[start - 1].length + (start < all.length ? 1 : 0);
211
- if (total + length > maxChars) break;
212
- total += length;
213
- start -= 1;
214
- }
215
- const kept = encoded.slice(start).filter((one) => one !== "");
216
- if (kept.length === 0) return void 0;
217
- return "[" + kept.join(",") + "]";
252
+ if (messages.length === 0) return void 0;
253
+ const redacted = applyRedact(messages, policy, { kind });
254
+ if (redacted === void 0) return void 0;
255
+ const all = normalizeContentMessages(redacted);
256
+ if (all.length === 0) return void 0;
257
+ const maxChars = policy.envelopeMaxChars;
258
+ const perMessage = all.length > 1 ? Math.floor((maxChars - 3) / 2) : maxChars - 2;
259
+ const encoded = [];
260
+ for (const message of all) {
261
+ const one = JSON.stringify(message);
262
+ encoded.push(one.length <= perMessage ? one : encodeTruncatedMessage(message, perMessage) ?? "");
263
+ }
264
+ let start = all.length;
265
+ let total = 2;
266
+ while (start > 0) {
267
+ const length = encoded[start - 1].length + (start < all.length ? 1 : 0);
268
+ if (total + length > maxChars) break;
269
+ total += length;
270
+ start -= 1;
271
+ }
272
+ const kept = encoded.slice(start).filter((one) => one !== "");
273
+ if (kept.length === 0) return void 0;
274
+ return "[" + kept.join(",") + "]";
218
275
  }
276
+ /**
277
+ * Encode the system prompt as one semconv text part, `[{ type: 'text', content }]`, under the
278
+ * envelope ceiling. The array form keeps a prompt that happens to be valid JSON from being re-read
279
+ * as data by ingest, which parses every string attribute first. A redaction must return a string or
280
+ * text parts; anything else emits nothing, since ingest would discard it silently.
281
+ */
219
282
  function encodeSystemInstructionsValue(value, policy) {
220
- if (value.length === 0) return void 0;
221
- const redacted = applyRedact(value, policy, { kind: "system_instructions" });
222
- if (redacted === void 0) return void 0;
223
- const text = contentTextOf(redacted);
224
- if (text.length === 0) return void 0;
225
- return encodeTextParts(text, policy.envelopeMaxChars);
283
+ if (value.length === 0) return void 0;
284
+ const redacted = applyRedact(value, policy, { kind: "system_instructions" });
285
+ if (redacted === void 0) return void 0;
286
+ const text = contentTextOf(redacted);
287
+ if (text.length === 0) return void 0;
288
+ return encodeTextParts(text, policy.envelopeMaxChars);
226
289
  }
290
+ /** Cut one message's text until its JSON object fits `maxChars`; undefined when even the marker cannot. */
227
291
  function encodeTruncatedMessage(message, maxChars) {
228
- const text = message.parts.map((part) => part.content).join("\n");
229
- return fitJson(
230
- (cut) => JSON.stringify({ role: message.role, parts: [{ type: "text", content: cut }] }),
231
- text,
232
- maxChars
233
- );
292
+ return fitJson((cut) => JSON.stringify({
293
+ role: message.role,
294
+ parts: [{
295
+ type: "text",
296
+ content: cut
297
+ }]
298
+ }), message.parts.map((part) => part.content).join("\n"), maxChars);
234
299
  }
235
300
  function encodeTextParts(text, maxChars) {
236
- return fitJson((cut) => JSON.stringify([{ type: "text", content: cut }]), text, maxChars);
301
+ return fitJson((cut) => JSON.stringify([{
302
+ type: "text",
303
+ content: cut
304
+ }]), text, maxChars);
237
305
  }
306
+ /**
307
+ * Shrink `text` until `wrap(text)` fits. JSON escaping can inflate a cut by up to 6×, so the raw
308
+ * budget is rescaled by the observed ratio each round rather than reduced by the overshoot, which
309
+ * would go negative on escape-heavy text. Undefined when not even one character plus the marker fits.
310
+ */
238
311
  function fitJson(wrap, text, maxChars) {
239
- let encoded = wrap(text);
240
- if (encoded.length <= maxChars) return encoded;
241
- const shell = wrap("").length;
242
- let budget = maxChars - shell;
243
- for (let round = 0; round < 8; round += 1) {
244
- if (budget <= truncationMarker(text.length).length) return void 0;
245
- const cut = truncate(text, budget);
246
- encoded = wrap(cut);
247
- if (encoded.length <= maxChars) return encoded;
248
- const rescaled = Math.floor(budget * (maxChars - shell) / (encoded.length - shell));
249
- budget = Math.min(budget - 1, rescaled);
250
- }
251
- return void 0;
312
+ let encoded = wrap(text);
313
+ if (encoded.length <= maxChars) return encoded;
314
+ const shell = wrap("").length;
315
+ let budget = maxChars - shell;
316
+ for (let round = 0; round < 8; round += 1) {
317
+ if (budget <= truncationMarker(text.length).length) return void 0;
318
+ encoded = wrap(truncate(text, budget));
319
+ if (encoded.length <= maxChars) return encoded;
320
+ const rescaled = Math.floor(budget * (maxChars - shell) / (encoded.length - shell));
321
+ budget = Math.min(budget - 1, rescaled);
322
+ }
252
323
  }
253
- function applyRedact(value, policy, context3) {
254
- if (!policy.redact) return value;
255
- try {
256
- return policy.redact(value, context3);
257
- } catch {
258
- return void 0;
259
- }
324
+ /**
325
+ * Run the redaction hook without letting it throw into the run. Returns undefined when the hook
326
+ * suppresses the value or fails, which the caller maps to "emit nothing".
327
+ */
328
+ function applyRedact(value, policy, context) {
329
+ if (!policy.redact) return value;
330
+ try {
331
+ return policy.redact(value, context);
332
+ } catch {
333
+ return;
334
+ }
260
335
  }
336
+ /**
337
+ * Encode arguments as a JSON object because ingest discards other shapes. Wrap non-objects as { value
338
+ * }; replace oversized objects with a valid { truncated } envelope rather than slicing JSON.
339
+ */
261
340
  function encodeArguments(value, maxChars) {
262
- const shaped = isPlainObject(value) ? value : { value };
263
- const encoded = stringify(shaped);
264
- if (encoded === void 0) return void 0;
265
- if (encoded.length <= maxChars) return encoded;
266
- return fitJson((cut) => JSON.stringify({ truncated: cut }), encoded, maxChars);
341
+ const encoded = stringify(isPlainObject(value) ? value : { value });
342
+ if (encoded === void 0) return void 0;
343
+ if (encoded.length <= maxChars) return encoded;
344
+ return fitJson((cut) => JSON.stringify({ truncated: cut }), encoded, maxChars);
267
345
  }
268
346
  function isPlainObject(value) {
269
- return value !== null && typeof value === "object" && !Array.isArray(value);
347
+ return value !== null && typeof value === "object" && !Array.isArray(value);
270
348
  }
349
+ /**
350
+ * A string as-is, everything else as JSON. The attribute type is a string, and
351
+ * ingest parses JSON back out; a tool result that is already prose would
352
+ * otherwise round-trip as a quoted, escaped literal.
353
+ */
271
354
  function stringify(value) {
272
- if (typeof value === "string") return value;
273
- try {
274
- const encoded = JSON.stringify(value);
275
- return typeof encoded === "string" ? encoded : void 0;
276
- } catch {
277
- return void 0;
278
- }
355
+ if (typeof value === "string") return value;
356
+ try {
357
+ const encoded = JSON.stringify(value);
358
+ return typeof encoded === "string" ? encoded : void 0;
359
+ } catch {
360
+ return;
361
+ }
279
362
  }
363
+ /**
364
+ * Reserve room for the truncation marker, then recompute its count. Budgets smaller than the marker
365
+ * still emit that full marker, so callers must allow for its minimum size.
366
+ */
280
367
  function truncate(value, maxChars) {
281
- if (value.length <= maxChars) return value;
282
- const upperBound = truncationMarker(value.length);
283
- let keep = Math.max(0, maxChars - upperBound.length);
284
- let marker = truncationMarker(value.length - keep);
285
- keep = Math.max(0, maxChars - marker.length);
286
- marker = truncationMarker(value.length - keep);
287
- if (keep > 0 && isHighSurrogate(value.charCodeAt(keep - 1))) {
288
- keep -= 1;
289
- marker = truncationMarker(value.length - keep);
290
- }
291
- return value.slice(0, keep) + marker;
368
+ if (value.length <= maxChars) return value;
369
+ const upperBound = truncationMarker(value.length);
370
+ let keep = Math.max(0, maxChars - upperBound.length);
371
+ let marker = truncationMarker(value.length - keep);
372
+ keep = Math.max(0, maxChars - marker.length);
373
+ marker = truncationMarker(value.length - keep);
374
+ if (keep > 0 && isHighSurrogate(value.charCodeAt(keep - 1))) {
375
+ keep -= 1;
376
+ marker = truncationMarker(value.length - keep);
377
+ }
378
+ return value.slice(0, keep) + marker;
292
379
  }
293
380
  function truncationMarker(droppedChars) {
294
- return `\u2026[truncated ${droppedChars} chars]`;
381
+ return `…[truncated ${droppedChars} chars]`;
295
382
  }
296
383
  function isHighSurrogate(code) {
297
- return code >= 55296 && code <= 56319;
384
+ return code >= 55296 && code <= 56319;
298
385
  }
299
-
300
- // package.json
301
- var version = "0.5.4";
302
-
303
- // src/semconv.ts
304
- var GEN_AI = {
305
- operationName: "gen_ai.operation.name",
306
- providerName: "gen_ai.provider.name",
307
- agentName: "gen_ai.agent.name",
308
- conversationId: "gen_ai.conversation.id",
309
- requestModel: "gen_ai.request.model",
310
- responseModel: "gen_ai.response.model",
311
- responseId: "gen_ai.response.id",
312
- requestStream: "gen_ai.request.stream",
313
- reasoningLevel: "gen_ai.request.reasoning.level",
314
- maxTokens: "gen_ai.request.max_tokens",
315
- temperature: "gen_ai.request.temperature",
316
- finishReasons: "gen_ai.response.finish_reasons",
317
- usageInputTokens: "gen_ai.usage.input_tokens",
318
- usageOutputTokens: "gen_ai.usage.output_tokens",
319
- usageCacheReadTokens: "gen_ai.usage.cache_read.input_tokens",
320
- usageCacheCreationTokens: "gen_ai.usage.cache_creation.input_tokens",
321
- toolName: "gen_ai.tool.name",
322
- toolCallId: "gen_ai.tool.call.id",
323
- toolType: "gen_ai.tool.type",
324
- conversationCompacted: "gen_ai.conversation.compacted",
325
- errorType: "error.type",
326
- serverAddress: "server.address",
327
- serverPort: "server.port"
386
+ //#endregion
387
+ //#region package.json
388
+ var version = "0.5.6";
389
+ //#endregion
390
+ //#region src/semconv.ts
391
+ /**
392
+ * GenAI semantic-convention attribute names. Mirrors the subset of
393
+ * `packages/shared/src/gen-ai-semconv.ts` this instrumentation can populate
394
+ * from Flue's STABLE observation plane.
395
+ */
396
+ const GEN_AI = {
397
+ operationName: "gen_ai.operation.name",
398
+ providerName: "gen_ai.provider.name",
399
+ agentName: "gen_ai.agent.name",
400
+ conversationId: "gen_ai.conversation.id",
401
+ requestModel: "gen_ai.request.model",
402
+ responseModel: "gen_ai.response.model",
403
+ responseId: "gen_ai.response.id",
404
+ requestStream: "gen_ai.request.stream",
405
+ reasoningLevel: "gen_ai.request.reasoning.level",
406
+ maxTokens: "gen_ai.request.max_tokens",
407
+ temperature: "gen_ai.request.temperature",
408
+ finishReasons: "gen_ai.response.finish_reasons",
409
+ usageInputTokens: "gen_ai.usage.input_tokens",
410
+ usageOutputTokens: "gen_ai.usage.output_tokens",
411
+ usageCacheReadTokens: "gen_ai.usage.cache_read.input_tokens",
412
+ usageCacheCreationTokens: "gen_ai.usage.cache_creation.input_tokens",
413
+ toolName: "gen_ai.tool.name",
414
+ toolCallId: "gen_ai.tool.call.id",
415
+ toolType: "gen_ai.tool.type",
416
+ conversationCompacted: "gen_ai.conversation.compacted",
417
+ errorType: "error.type",
418
+ serverAddress: "server.address",
419
+ serverPort: "server.port"
328
420
  };
329
- var GEN_AI_CONTENT = {
330
- inputMessages: "gen_ai.input.messages",
331
- outputMessages: "gen_ai.output.messages",
332
- systemInstructions: "gen_ai.system_instructions",
333
- toolCallArguments: "gen_ai.tool.call.arguments",
334
- toolCallResult: "gen_ai.tool.call.result"
421
+ /**
422
+ * Content attribute names, separate from the structural GEN_AI set. Tool content goes on
423
+ * `execute_tool` spans, message content on the `invoke_agent` envelope; each kind can be switched
424
+ * off through the content options. Mirrored in shared ingest semconv.
425
+ */
426
+ const GEN_AI_CONTENT = {
427
+ inputMessages: "gen_ai.input.messages",
428
+ outputMessages: "gen_ai.output.messages",
429
+ systemInstructions: "gen_ai.system_instructions",
430
+ toolCallArguments: "gen_ai.tool.call.arguments",
431
+ toolCallResult: "gen_ai.tool.call.result"
335
432
  };
336
- var GEN_AI_OPERATION_INVOKE_AGENT = "invoke_agent";
337
- var GEN_AI_OPERATION_CHAT = "chat";
338
- var GEN_AI_OPERATION_EXECUTE_TOOL = "execute_tool";
339
- var RUNTYPE = {
340
- agentId: "runtype.agent.id",
341
- schemaVersion: "runtype.schema.version",
342
- adapterName: "runtype.adapter.name",
343
- adapterVersion: "runtype.adapter.version",
344
- executionId: "runtype.execution.id",
345
- iteration: "runtype.iteration",
346
- iterations: "runtype.iterations",
347
- totalCost: "runtype.total_cost",
348
- stopReason: "runtype.stop_reason",
349
- toolsReported: "runtype.tools.reported",
350
- toolType: "runtype.tool.type",
351
- turnId: "runtype.turn.id",
352
- turnIndex: "runtype.turn.index",
353
- providerFinishReason: "runtype.provider.finish_reason",
354
- providerServiceTier: "runtype.provider.service_tier",
355
- gatewayLogId: "runtype.gateway.log_id"
433
+ /** The two GenAI operations this instrumentation claims. */
434
+ const GEN_AI_OPERATION_INVOKE_AGENT = "invoke_agent";
435
+ const GEN_AI_OPERATION_CHAT = "chat";
436
+ const GEN_AI_OPERATION_EXECUTE_TOOL = "execute_tool";
437
+ /**
438
+ * Runtype's extension vocabulary. Placement is part of the contract and the
439
+ * reader enforces it — see the `RUNTYPE_ATTRIBUTES` doc block in the shared
440
+ * module for which level each name belongs on.
441
+ */
442
+ const RUNTYPE = {
443
+ agentId: "runtype.agent.id",
444
+ schemaVersion: "runtype.schema.version",
445
+ adapterName: "runtype.adapter.name",
446
+ adapterVersion: "runtype.adapter.version",
447
+ executionId: "runtype.execution.id",
448
+ iteration: "runtype.iteration",
449
+ iterations: "runtype.iterations",
450
+ totalCost: "runtype.total_cost",
451
+ stopReason: "runtype.stop_reason",
452
+ toolsReported: "runtype.tools.reported",
453
+ toolType: "runtype.tool.type",
454
+ turnId: "runtype.turn.id",
455
+ turnIndex: "runtype.turn.index",
456
+ providerFinishReason: "runtype.provider.finish_reason",
457
+ providerServiceTier: "runtype.provider.service_tier",
458
+ gatewayLogId: "runtype.gateway.log_id"
356
459
  };
357
- var FLUE = {
358
- instanceId: "flue.instance.id",
359
- submissionId: "flue.submission.id",
360
- agentName: "flue.agent.name",
361
- harnessName: "flue.harness.name",
362
- sessionName: "flue.session.name",
363
- parentSessionName: "flue.parent_session.name",
364
- operationId: "flue.operation.id",
365
- operationKind: "flue.operation.kind",
366
- turnId: "flue.turn.id",
367
- turnPurpose: "flue.turn.purpose",
368
- taskId: "flue.task.id",
369
- eventIndex: "flue.event.index",
370
- toolOrigin: "flue.tool.origin",
371
- compactionReason: "flue.compaction.reason",
372
- usageTotalTokens: "flue.usage.total_tokens"
460
+ /**
461
+ * Keep Flue vendor identifiers for existing customer dashboard grouping. Canonical runtype attributes
462
+ * take precedence during normalization.
463
+ */
464
+ const FLUE = {
465
+ instanceId: "flue.instance.id",
466
+ submissionId: "flue.submission.id",
467
+ agentName: "flue.agent.name",
468
+ harnessName: "flue.harness.name",
469
+ sessionName: "flue.session.name",
470
+ parentSessionName: "flue.parent_session.name",
471
+ operationId: "flue.operation.id",
472
+ operationKind: "flue.operation.kind",
473
+ turnId: "flue.turn.id",
474
+ turnPurpose: "flue.turn.purpose",
475
+ taskId: "flue.task.id",
476
+ eventIndex: "flue.event.index",
477
+ toolOrigin: "flue.tool.origin",
478
+ compactionReason: "flue.compaction.reason",
479
+ usageTotalTokens: "flue.usage.total_tokens"
373
480
  };
374
- var RUNTYPE_TOOL_TYPES = [
375
- "flow",
376
- "mcp",
377
- "builtin",
378
- "custom",
379
- "external",
380
- "advisor",
381
- "subagent",
382
- "local",
383
- "data_connection",
384
- "search"
481
+ /**
482
+ * `runtype.tool.type` values — the closed domain that drives display and
483
+ * grader routing. Mirrors `RUNTYPE_TOOL_TYPES` in the shared module; only the
484
+ * members this instrumentation can honestly assert are ever emitted (see
485
+ * `projection.ts`).
486
+ */
487
+ const RUNTYPE_TOOL_TYPES = [
488
+ "flow",
489
+ "mcp",
490
+ "builtin",
491
+ "custom",
492
+ "external",
493
+ "advisor",
494
+ "subagent",
495
+ "local",
496
+ "data_connection",
497
+ "search"
385
498
  ];
386
- var RUNTYPE_STOP_REASONS = [
387
- "end_turn",
388
- "max_tool_calls",
389
- "length",
390
- "content_filter",
391
- "error",
392
- "unknown"
499
+ /**
500
+ * Terminal stop reasons, in Runtype's own wire vocabulary
501
+ * (`wireStopReasonSchema` in `packages/shared/src/utils/sse-event-schemas.ts`).
502
+ * `runtype.stop_reason` is written to the run row verbatim, so an invented
503
+ * value would render in the dashboard as a real fact.
504
+ */
505
+ const RUNTYPE_STOP_REASONS = [
506
+ "end_turn",
507
+ "max_tool_calls",
508
+ "length",
509
+ "content_filter",
510
+ "error",
511
+ "unknown"
393
512
  ];
394
- var RUNTYPE_SCHEMA_VERSION = "1";
395
- var ADAPTER_NAME = "@runtypelabs/flue-otel";
396
- var ADAPTER_VERSION = version;
397
-
398
- // src/projection.ts
513
+ /** The version of the `runtype.*` vocabulary this package was built against. */
514
+ const RUNTYPE_SCHEMA_VERSION = "1";
515
+ /** What this package reports itself as in `runtype.adapter.name`. */
516
+ const ADAPTER_NAME = "@runtypelabs/flue-otel";
517
+ /** What this package reports itself as in `runtype.adapter.version`. */
518
+ const ADAPTER_VERSION = version;
519
+ //#endregion
520
+ //#region src/projection.ts
521
+ /**
522
+ * Resource attributes for the customer's `Resource`. Exported rather than set
523
+ * internally because this package owns none of the SDK: the provider, the
524
+ * processor, the exporter and the resource are all the application's, and a
525
+ * library that reached into them would fight whatever the customer already runs.
526
+ */
399
527
  function runtypeFlueResourceAttributes(resource) {
400
- return {
401
- [RUNTYPE.schemaVersion]: RUNTYPE_SCHEMA_VERSION,
402
- [RUNTYPE.adapterName]: ADAPTER_NAME,
403
- [RUNTYPE.adapterVersion]: ADAPTER_VERSION,
404
- ...resource?.agentId ? { [RUNTYPE.agentId]: resource.agentId } : {}
405
- };
528
+ return {
529
+ [RUNTYPE.schemaVersion]: "1",
530
+ [RUNTYPE.adapterName]: ADAPTER_NAME,
531
+ [RUNTYPE.adapterVersion]: ADAPTER_VERSION,
532
+ ...resource?.agentId ? { [RUNTYPE.agentId]: resource.agentId } : {}
533
+ };
406
534
  }
407
535
  function createFlueProjection(options = {}) {
408
- const content = resolveContentPolicy(options.content);
409
- const open = /* @__PURE__ */ new Map();
410
- const envelopes = /* @__PURE__ */ new Map();
411
- const envelopeByChain = /* @__PURE__ */ new Map();
412
- const envelopeBySubmission = /* @__PURE__ */ new Map();
413
- const envelopeByOperationId = /* @__PURE__ */ new Map();
414
- const envelopeByTaskScope = /* @__PURE__ */ new Map();
415
- const turnPositions = /* @__PURE__ */ new Map();
416
- const taskTurnCounts = /* @__PURE__ */ new Map();
417
- function observe(observation) {
418
- switch (observation.type) {
419
- case "operation_start":
420
- return onOperationStart(observation);
421
- case "operation":
422
- return onOperationEnd(observation);
423
- case "task_start":
424
- return onTaskStart(observation);
425
- case "task":
426
- return onTaskEnd(observation);
427
- case "compaction_start":
428
- return onCompactionStart(observation);
429
- case "compaction":
430
- return onCompactionEnd(observation);
431
- case "turn_request":
432
- return onTurnRequest(observation);
433
- case "turn":
434
- return onTurnEnd(observation);
435
- case "tool_start":
436
- return onToolStart(observation);
437
- case "tool":
438
- return onToolEnd(observation);
439
- case "submission_settled":
440
- return onSubmissionSettled(observation);
441
- default:
442
- return [];
443
- }
444
- }
445
- function onOperationStart(event) {
446
- const kind = readString(event, "operationKind");
447
- if (kind !== "prompt" && kind !== "skill") return [];
448
- const ref = operationKey(event);
449
- if (open.has(ref)) return [];
450
- if (event.taskId && open.has(taskKey(event))) return [];
451
- const parentRef = resolveParentRef(event);
452
- const chain = chainKey(event);
453
- const isEnvelope = !envelopeByChain.has(chain);
454
- if (isEnvelope) {
455
- envelopeByChain.set(chain, ref);
456
- if (event.submissionId) envelopeBySubmission.set(event.submissionId, ref);
457
- if (event.operationId) envelopeByOperationId.set(event.operationId, ref);
458
- envelopes.set(ref, freshEnvelope());
459
- }
460
- open.set(ref, {
461
- ref,
462
- ...parentRef ? { parentRef } : {},
463
- chainKey: chain,
464
- isTask: false,
465
- ...isEnvelope ? {
466
- scope: {
467
- instanceId: event.instanceId,
468
- harness: event.harness,
469
- conversationId: event.conversationId,
470
- session: event.session
471
- }
472
- } : {}
473
- });
474
- const agentId = event.agentName ? options.agents?.[event.agentName] : void 0;
475
- return [
476
- {
477
- kind: "open",
478
- ref,
479
- name: isEnvelope ? event.agentName ? `invoke_agent ${event.agentName}` : "invoke_agent" : `flue.operation ${kind}`,
480
- spanKind: "internal",
481
- ...parentRef ? { parentRef } : {},
482
- ...event.timestamp ? { startTime: event.timestamp } : {},
483
- attributes: {
484
- ...identifiers(event),
485
- [FLUE.operationKind]: kind,
486
- ...isEnvelope ? { [GEN_AI.operationName]: GEN_AI_OPERATION_INVOKE_AGENT } : {},
487
- ...isEnvelope && event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
488
- ...isEnvelope && event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
489
- ...isEnvelope && event.submissionId ? { [RUNTYPE.executionId]: event.submissionId } : {},
490
- ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {},
491
- // WHY(README.md): the resource placement is the customer's to wire and is routinely skipped.
492
- ...isEnvelope ? {
493
- [RUNTYPE.adapterName]: ADAPTER_NAME,
494
- [RUNTYPE.adapterVersion]: ADAPTER_VERSION
495
- } : {}
496
- }
497
- }
498
- ];
499
- }
500
- function onOperationEnd(event) {
501
- const ref = operationKey(event);
502
- const record = open.get(ref);
503
- if (!record) return [];
504
- const intents = sweepDescendants(ref, event.timestamp);
505
- const envelope = envelopes.get(ref);
506
- const isError = readBoolean(event, "isError") === true;
507
- if (envelope) {
508
- intents.push({ kind: "update", ref, attributes: envelopeAttributes(envelope, isError) });
509
- envelopes.delete(ref);
510
- if (envelopeByChain.get(record.chainKey) === ref) envelopeByChain.delete(record.chainKey);
511
- for (const [submissionId, submissionRef] of envelopeBySubmission) {
512
- if (submissionRef === ref) envelopeBySubmission.delete(submissionId);
513
- }
514
- for (const [operationId, operationRef] of envelopeByOperationId) {
515
- if (operationRef === ref) envelopeByOperationId.delete(operationId);
516
- }
517
- for (const [taskScope, taskEnvelopeRef] of envelopeByTaskScope) {
518
- if (taskEnvelopeRef === ref) envelopeByTaskScope.delete(taskScope);
519
- }
520
- for (const [key, position] of turnPositions) {
521
- if (position.envelopeRef === ref) turnPositions.delete(key);
522
- }
523
- }
524
- intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
525
- open.delete(ref);
526
- return intents;
527
- }
528
- function onTaskStart(event) {
529
- const ref = taskKey(event);
530
- if (open.has(ref)) return [];
531
- const envelopeRef = event.operationId ? envelopeByOperationId.get(event.operationId) : void 0;
532
- if (envelopeRef) envelopeByTaskScope.set(ref, envelopeRef);
533
- const callerScope = {
534
- ...envelopeRef ? open.get(envelopeRef)?.scope ?? {} : {},
535
- operationId: event.operationId,
536
- turnId: event.turnId,
537
- toolCallId: event.toolCallId
538
- };
539
- const parentRef = resolveParentRef(callerScope) ?? envelopeRef;
540
- open.set(ref, {
541
- ref,
542
- ...parentRef ? { parentRef } : {},
543
- chainKey: chainKey(event),
544
- isTask: true
545
- });
546
- taskTurnCounts.set(ref, 0);
547
- const agent = readString(event, "agent");
548
- return [
549
- {
550
- kind: "open",
551
- ref,
552
- // WHY(packages/flue-otel/src/projection.ts): Use a task span, not invoke_agent; one trace is persisted as one execution.
553
- name: agent ? `flue.task ${agent}` : "flue.task",
554
- spanKind: "internal",
555
- ...parentRef ? { parentRef } : {},
556
- ...event.timestamp ? { startTime: event.timestamp } : {},
557
- attributes: {
558
- ...identifiers(event),
559
- ...agent ? { [GEN_AI.agentName]: agent } : {}
560
- }
561
- }
562
- ];
563
- }
564
- function onTaskEnd(event) {
565
- const ref = taskKey(event);
566
- if (!open.has(ref)) return [];
567
- const intents = sweepDescendants(ref, event.timestamp);
568
- intents.push(
569
- closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
570
- );
571
- open.delete(ref);
572
- taskTurnCounts.delete(ref);
573
- return intents;
574
- }
575
- function onCompactionStart(event) {
576
- const ref = compactionKey(event);
577
- if (open.has(ref)) return [];
578
- const parentRef = resolveParentRef(event);
579
- open.set(ref, {
580
- ref,
581
- ...parentRef ? { parentRef } : {},
582
- chainKey: chainKey(event),
583
- isTask: false
584
- });
585
- const reason = readString(event, "reason");
586
- return [
587
- {
588
- kind: "open",
589
- ref,
590
- name: "flue.compaction",
591
- spanKind: "internal",
592
- requiresParent: true,
593
- ...parentRef ? { parentRef } : {},
594
- ...event.timestamp ? { startTime: event.timestamp } : {},
595
- attributes: {
596
- ...identifiers(event),
597
- ...reason ? { [FLUE.compactionReason]: reason } : {}
598
- }
599
- }
600
- ];
601
- }
602
- function onCompactionEnd(event) {
603
- const ref = compactionKey(event);
604
- if (!open.has(ref)) return [];
605
- if (!event.taskId) {
606
- const envelope = resolveEnvelopeRef(event);
607
- const state = envelope ? envelopes.get(envelope) : void 0;
608
- if (state) state.compacted = true;
609
- }
610
- const intents = sweepDescendants(ref, event.timestamp);
611
- intents.push(
612
- closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
613
- );
614
- open.delete(ref);
615
- return intents;
616
- }
617
- function onTurnRequest(event) {
618
- const ref = turnKey(event);
619
- if (open.has(ref)) return [];
620
- const request = readRecord(event, "request");
621
- if (!request) return [];
622
- const purpose = readString(event, "purpose") ?? "agent";
623
- const compactionRef = compactionKey(event);
624
- const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
625
- open.set(ref, {
626
- ref,
627
- ...parentRef ? { parentRef } : {},
628
- chainKey: chainKey(event),
629
- isTask: false
630
- });
631
- const envelopeRef = resolveEnvelopeRef(event);
632
- const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
633
- let position;
634
- if (purpose === "agent") {
635
- if (!event.taskId && envelope && envelopeRef) {
636
- position = {
637
- turnId: event.turnId ?? "",
638
- index: envelope.turnCount,
639
- atEnvelopeScope: true,
640
- envelopeRef
641
- };
642
- envelope.turnCount += 1;
643
- envelope.maxIteration = position.index;
644
- } else if (event.taskId) {
645
- const taskRef = taskKey(event);
646
- const index = taskTurnCounts.get(taskRef) ?? 0;
647
- taskTurnCounts.set(taskRef, index + 1);
648
- position = {
649
- turnId: event.turnId ?? "",
650
- index,
651
- atEnvelopeScope: false,
652
- ...envelopeRef ? { envelopeRef } : {}
653
- };
654
- }
655
- if (position) turnPositions.set(ref, position);
656
- }
657
- if (envelope) {
658
- if (!envelope.modelPinned && purpose === "agent")
659
- envelope.requestModel = request.requestedModel;
660
- if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
661
- if (position?.atEnvelopeScope) captureInput(envelope, request);
662
- }
663
- return [
664
- {
665
- kind: "open",
666
- ref,
667
- name: request.requestedModel ? `${GEN_AI_OPERATION_CHAT} ${request.requestedModel}` : GEN_AI_OPERATION_CHAT,
668
- spanKind: "client",
669
- ...parentRef ? { parentRef } : {},
670
- ...event.timestamp ? { startTime: event.timestamp } : {},
671
- attributes: {
672
- ...identifiers(event),
673
- [GEN_AI.operationName]: GEN_AI_OPERATION_CHAT,
674
- [FLUE.turnPurpose]: purpose,
675
- ...request.providerName ? { [GEN_AI.providerName]: request.providerName } : {},
676
- ...request.requestedModel ? { [GEN_AI.requestModel]: request.requestedModel } : {},
677
- ...event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
678
- ...event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
679
- ...request.reasoningLevel ? { [GEN_AI.reasoningLevel]: request.reasoningLevel } : {},
680
- ...typeof request.maxTokens === "number" ? { [GEN_AI.maxTokens]: request.maxTokens } : {},
681
- ...typeof request.temperature === "number" ? { [GEN_AI.temperature]: request.temperature } : {},
682
- ...request.serverAddress ? { [GEN_AI.serverAddress]: request.serverAddress } : {},
683
- ...typeof request.serverPort === "number" ? { [GEN_AI.serverPort]: request.serverPort } : {},
684
- ...request.contextCompacted ? { [GEN_AI.conversationCompacted]: true } : {},
685
- ...positionAttributes(position)
686
- }
687
- }
688
- ];
689
- }
690
- function onTurnEnd(event) {
691
- const ref = turnKey(event);
692
- if (!open.has(ref)) return [];
693
- const response = readRecord(event, "response") ?? {};
694
- const purpose = readString(event, "purpose") ?? "agent";
695
- const isError = readBoolean(event, "isError") === true;
696
- const envelopeRef = resolveEnvelopeRef(event);
697
- const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
698
- if (envelope) {
699
- if (!envelope.modelPinned && purpose === "agent") {
700
- envelope.responseModel = response.responseModel;
701
- envelope.modelPinned = true;
702
- }
703
- foldUsage(envelope, response.usage);
704
- if (purpose === "agent" && response.finishReason) {
705
- envelope.lastFinishReason = response.finishReason;
706
- }
707
- if (content.outputMessages && purpose === "agent" && !event.taskId && !isError) {
708
- const output = projectFlueMessage(response.output);
709
- if (output?.role === "assistant") envelope.outputMessage = output;
710
- }
711
- }
712
- const attributes = {
713
- ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
714
- ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
715
- ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
716
- ...usageAttributes(response.usage),
717
- ...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
718
- ...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
719
- };
720
- const intents = [];
721
- if (Object.keys(attributes).length > 0) intents.push({ kind: "update", ref, attributes });
722
- intents.push(closeIntent(ref, event, isError, response.error ?? readErrorInfo(event)));
723
- open.delete(ref);
724
- return intents;
725
- }
726
- function onToolStart(event) {
727
- const toolName = readString(event, "toolName");
728
- const toolCallId = readString(event, "toolCallId");
729
- if (!toolName || !toolCallId) return [];
730
- const ref = toolKey(event);
731
- if (open.has(ref)) return [];
732
- const parentRef = resolveParentRef(event);
733
- open.set(ref, {
734
- ref,
735
- ...parentRef ? { parentRef } : {},
736
- chainKey: chainKey(event),
737
- isTask: false
738
- });
739
- const position = turnPositions.get(turnKey(event));
740
- if (isCallerShellCall(toolName, event)) {
741
- return [
742
- {
743
- kind: "open",
744
- ref,
745
- name: "flue.operation shell",
746
- spanKind: "internal",
747
- requiresParent: true,
748
- ...parentRef ? { parentRef } : {},
749
- ...event.timestamp ? { startTime: event.timestamp } : {},
750
- attributes: {
751
- ...identifiers(event),
752
- ...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
753
- ...positionAttributes(position)
754
- }
755
- }
756
- ];
757
- }
758
- const isDelegation = isTaskDelegationTool(toolName, event);
759
- const toolType = resolveToolType(toolName, event);
760
- const args = content.toolArguments ? encodeContentValue(readUnknown(event, "args"), content, {
761
- kind: "arguments",
762
- toolName
763
- }) : void 0;
764
- return [
765
- {
766
- kind: "open",
767
- ref,
768
- name: `${GEN_AI_OPERATION_EXECUTE_TOOL} ${toolName}`,
769
- spanKind: "internal",
770
- ...parentRef ? { parentRef } : {},
771
- ...event.timestamp ? { startTime: event.timestamp } : {},
772
- attributes: {
773
- ...identifiers(event),
774
- [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
775
- [GEN_AI.toolName]: toolName,
776
- [GEN_AI.toolCallId]: toolCallId,
777
- ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
778
- ...isDelegation ? {} : { [GEN_AI.toolType]: "function" },
779
- ...toolType ? { [RUNTYPE.toolType]: toolType } : {},
780
- ...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
781
- ...event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
782
- ...event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
783
- ...positionAttributes(position)
784
- }
785
- }
786
- ];
787
- }
788
- function onToolEnd(event) {
789
- const ref = toolKey(event);
790
- if (!open.has(ref)) return [];
791
- const intents = sweepDescendants(ref, event.timestamp);
792
- const isError = readBoolean(event, "isError") === true;
793
- const toolName = readString(event, "toolName");
794
- if (content.toolResults && !isError && toolName && !isCallerShellCall(toolName, event)) {
795
- const result = encodeContentValue(readToolResult(event), content, {
796
- kind: "result",
797
- toolName
798
- });
799
- if (result !== void 0) {
800
- intents.push({
801
- kind: "update",
802
- ref,
803
- attributes: { [GEN_AI_CONTENT.toolCallResult]: result }
804
- });
805
- }
806
- }
807
- intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
808
- open.delete(ref);
809
- return intents;
810
- }
811
- function onSubmissionSettled(event) {
812
- const outcome = readString(event, "outcome");
813
- if (!outcome) return [];
814
- const stopReason = mapSettlementOutcome(outcome);
815
- if (!stopReason) return [];
816
- const submissionId = readString(event, "submissionId");
817
- if (!submissionId) return [];
818
- const ref = envelopeBySubmission.get(submissionId);
819
- const envelope = ref ? envelopes.get(ref) : void 0;
820
- if (envelope) envelope.settlementStopReason = stopReason;
821
- return [];
822
- }
823
- function envelopeAttributes(envelope, isError) {
824
- const stopReason = isError ? "error" : envelope.settlementStopReason ?? mapFinishReason(envelope.lastFinishReason);
825
- return {
826
- ...envelope.requestModel ? { [GEN_AI.requestModel]: envelope.requestModel } : {},
827
- ...envelope.responseModel ? { [GEN_AI.responseModel]: envelope.responseModel } : {},
828
- ...envelope.sawUsage ? {
829
- [GEN_AI.usageInputTokens]: envelope.inputTokens,
830
- [GEN_AI.usageOutputTokens]: envelope.outputTokens,
831
- [GEN_AI.usageCacheReadTokens]: envelope.cacheReadTokens,
832
- [GEN_AI.usageCacheCreationTokens]: envelope.cacheCreationTokens
833
- } : {},
834
- ...stopReason ? { [RUNTYPE.stopReason]: stopReason } : {},
835
- ...envelope.toolsReported ? { [RUNTYPE.toolsReported]: true } : {},
836
- ...envelope.maxIteration !== null ? { [RUNTYPE.iteration]: envelope.maxIteration } : {},
837
- ...envelopeContentAttributes(envelope)
838
- };
839
- }
840
- function captureInput(envelope, request) {
841
- const input = request.input;
842
- if (envelope.inputRaw && (envelope.compacted || isCompactedHistory(input?.messages))) return;
843
- if (content.inputMessages && Array.isArray(input?.messages) && input.messages.length > 0) {
844
- envelope.inputRaw = input.messages;
845
- envelope.inputBaseCount ??= input.messages.length;
846
- }
847
- if (content.systemInstructions && typeof input?.systemPrompt === "string") {
848
- envelope.systemInstructions = input.systemPrompt;
849
- }
850
- }
851
- function isCompactedHistory(messages) {
852
- if (!Array.isArray(messages) || messages.length === 0) return false;
853
- const first = messages[0];
854
- if (!first || first.role !== "user") return false;
855
- return flueMessageText(first.content).startsWith(COMPACTION_SUMMARY_PREFIX);
856
- }
857
- function projectInput(envelope) {
858
- const raw = envelope.inputRaw ?? [];
859
- const base = envelope.inputBaseCount ?? raw.length;
860
- return [
861
- ...projectFlueMessages(raw.slice(0, base)),
862
- ...projectFlueMessages(raw.slice(base)).filter((message) => message.role !== "assistant")
863
- ];
864
- }
865
- function envelopeContentAttributes(envelope) {
866
- const input = envelope.inputRaw ? encodeMessagesValue(projectInput(envelope), content, "input_messages") : void 0;
867
- const system = envelope.systemInstructions ? encodeSystemInstructionsValue(envelope.systemInstructions, content) : void 0;
868
- const output = envelope.outputMessage ? encodeMessagesValue([envelope.outputMessage], content, "output_messages") : void 0;
869
- return {
870
- ...input !== void 0 ? { [GEN_AI_CONTENT.inputMessages]: input } : {},
871
- ...system !== void 0 ? { [GEN_AI_CONTENT.systemInstructions]: system } : {},
872
- ...output !== void 0 ? { [GEN_AI_CONTENT.outputMessages]: output } : {}
873
- };
874
- }
875
- function resolveEnvelopeRef(event) {
876
- if (event.taskId) {
877
- const viaTask = envelopeByTaskScope.get(taskKey(event));
878
- if (viaTask) return viaTask;
879
- }
880
- return envelopeByChain.get(chainKey(event));
881
- }
882
- function resolveParentRef(scope) {
883
- if (scope.taskId) {
884
- const ref = taskKey(scope);
885
- if (open.has(ref)) return ref;
886
- }
887
- if (scope.toolCallId) {
888
- const ref = toolKey(scope);
889
- if (open.has(ref)) return ref;
890
- }
891
- if (scope.operationId) {
892
- const ref = operationKey(scope);
893
- if (open.has(ref)) return ref;
894
- }
895
- return void 0;
896
- }
897
- function sweepDescendants(parentRef, endTime) {
898
- const doomed = [];
899
- const frontier = [parentRef];
900
- while (frontier.length > 0) {
901
- const current = frontier.pop();
902
- for (const record of open.values()) {
903
- if (record.parentRef !== current) continue;
904
- doomed.push(record.ref);
905
- frontier.push(record.ref);
906
- }
907
- }
908
- const intents = [];
909
- for (const ref of doomed.reverse()) {
910
- const record = open.get(ref);
911
- if (!record) continue;
912
- intents.push({
913
- kind: "close",
914
- ref,
915
- ...endTime ? { endTime } : {},
916
- error: { type: "interrupted" }
917
- });
918
- open.delete(ref);
919
- if (record.isTask) taskTurnCounts.delete(ref);
920
- }
921
- return intents;
922
- }
923
- function drain(endTime) {
924
- const intents = [];
925
- for (const ref of [...open.keys()].reverse()) {
926
- const envelope = envelopes.get(ref);
927
- if (envelope) {
928
- intents.push({ kind: "update", ref, attributes: envelopeAttributes(envelope, true) });
929
- }
930
- intents.push({
931
- kind: "close",
932
- ref,
933
- ...endTime ? { endTime } : {},
934
- error: { type: "interrupted" }
935
- });
936
- }
937
- open.clear();
938
- envelopes.clear();
939
- envelopeByChain.clear();
940
- envelopeBySubmission.clear();
941
- envelopeByOperationId.clear();
942
- envelopeByTaskScope.clear();
943
- turnPositions.clear();
944
- taskTurnCounts.clear();
945
- return intents;
946
- }
947
- return { observe, drain, hasOpenSpan: (ref) => open.has(ref) };
536
+ const content = resolveContentPolicy(options.content);
537
+ /** Every open span, in open order, so `drain` can close deepest-first. */
538
+ const open = /* @__PURE__ */ new Map();
539
+ const envelopes = /* @__PURE__ */ new Map();
540
+ /** chainKey → the ref of the envelope currently bounding that agent run. */
541
+ const envelopeByChain = /* @__PURE__ */ new Map();
542
+ /**
543
+ * Index durable envelopes by submissionId: coordinator settlements lack the conversation/session
544
+ * fields used by run events and cannot share their chain key.
545
+ */
546
+ const envelopeBySubmission = /* @__PURE__ */ new Map();
547
+ /**
548
+ * Join child work to its envelope using operationId and task scope. A delegated session has different
549
+ * chain keys, so chain-only lookup loses its usage.
550
+ */
551
+ const envelopeByOperationId = /* @__PURE__ */ new Map();
552
+ const envelopeByTaskScope = /* @__PURE__ */ new Map();
553
+ /** turnKey → its loop position, retained until the owning envelope closes. */
554
+ const turnPositions = /* @__PURE__ */ new Map();
555
+ /** Task ref → that sub-agent's own turn counter. */
556
+ const taskTurnCounts = /* @__PURE__ */ new Map();
557
+ function observe(observation) {
558
+ switch (observation.type) {
559
+ case "operation_start": return onOperationStart(observation);
560
+ case "operation": return onOperationEnd(observation);
561
+ case "task_start": return onTaskStart(observation);
562
+ case "task": return onTaskEnd(observation);
563
+ case "compaction_start": return onCompactionStart(observation);
564
+ case "compaction": return onCompactionEnd(observation);
565
+ case "turn_request": return onTurnRequest(observation);
566
+ case "turn": return onTurnEnd(observation);
567
+ case "tool_start": return onToolStart(observation);
568
+ case "tool": return onToolEnd(observation);
569
+ case "submission_settled": return onSubmissionSettled(observation);
570
+ default: return [];
571
+ }
572
+ }
573
+ function onOperationStart(event) {
574
+ const kind = readString(event, "operationKind");
575
+ if (kind !== "prompt" && kind !== "skill") return [];
576
+ const ref = operationKey(event);
577
+ if (open.has(ref)) return [];
578
+ if (event.taskId && open.has(taskKey(event))) return [];
579
+ const parentRef = resolveParentRef(event);
580
+ const chain = chainKey(event);
581
+ const isEnvelope = !envelopeByChain.has(chain);
582
+ if (isEnvelope) {
583
+ envelopeByChain.set(chain, ref);
584
+ if (event.submissionId) envelopeBySubmission.set(event.submissionId, ref);
585
+ if (event.operationId) envelopeByOperationId.set(event.operationId, ref);
586
+ envelopes.set(ref, freshEnvelope());
587
+ }
588
+ open.set(ref, {
589
+ ref,
590
+ ...parentRef ? { parentRef } : {},
591
+ chainKey: chain,
592
+ isTask: false,
593
+ ...isEnvelope ? { scope: {
594
+ instanceId: event.instanceId,
595
+ harness: event.harness,
596
+ conversationId: event.conversationId,
597
+ session: event.session
598
+ } } : {}
599
+ });
600
+ const agentId = event.agentName ? options.agents?.[event.agentName] : void 0;
601
+ return [{
602
+ kind: "open",
603
+ ref,
604
+ name: isEnvelope ? event.agentName ? `invoke_agent ${event.agentName}` : "invoke_agent" : `flue.operation ${kind}`,
605
+ spanKind: "internal",
606
+ ...parentRef ? { parentRef } : {},
607
+ ...event.timestamp ? { startTime: event.timestamp } : {},
608
+ attributes: {
609
+ ...identifiers(event),
610
+ [FLUE.operationKind]: kind,
611
+ ...isEnvelope ? { [GEN_AI.operationName]: GEN_AI_OPERATION_INVOKE_AGENT } : {},
612
+ ...isEnvelope && event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
613
+ ...isEnvelope && event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
614
+ ...isEnvelope && event.submissionId ? { [RUNTYPE.executionId]: event.submissionId } : {},
615
+ ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {},
616
+ ...isEnvelope ? {
617
+ [RUNTYPE.adapterName]: ADAPTER_NAME,
618
+ [RUNTYPE.adapterVersion]: ADAPTER_VERSION
619
+ } : {}
620
+ }
621
+ }];
622
+ }
623
+ function onOperationEnd(event) {
624
+ const ref = operationKey(event);
625
+ const record = open.get(ref);
626
+ if (!record) return [];
627
+ const intents = sweepDescendants(ref, event.timestamp);
628
+ const envelope = envelopes.get(ref);
629
+ const isError = readBoolean(event, "isError") === true;
630
+ if (envelope) {
631
+ intents.push({
632
+ kind: "update",
633
+ ref,
634
+ attributes: envelopeAttributes(envelope, isError)
635
+ });
636
+ envelopes.delete(ref);
637
+ if (envelopeByChain.get(record.chainKey) === ref) envelopeByChain.delete(record.chainKey);
638
+ for (const [submissionId, submissionRef] of envelopeBySubmission) if (submissionRef === ref) envelopeBySubmission.delete(submissionId);
639
+ for (const [operationId, operationRef] of envelopeByOperationId) if (operationRef === ref) envelopeByOperationId.delete(operationId);
640
+ for (const [taskScope, taskEnvelopeRef] of envelopeByTaskScope) if (taskEnvelopeRef === ref) envelopeByTaskScope.delete(taskScope);
641
+ for (const [key, position] of turnPositions) if (position.envelopeRef === ref) turnPositions.delete(key);
642
+ }
643
+ intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
644
+ open.delete(ref);
645
+ return intents;
646
+ }
647
+ function onTaskStart(event) {
648
+ const ref = taskKey(event);
649
+ if (open.has(ref)) return [];
650
+ const envelopeRef = event.operationId ? envelopeByOperationId.get(event.operationId) : void 0;
651
+ if (envelopeRef) envelopeByTaskScope.set(ref, envelopeRef);
652
+ const parentRef = resolveParentRef({
653
+ ...envelopeRef ? open.get(envelopeRef)?.scope ?? {} : {},
654
+ operationId: event.operationId,
655
+ turnId: event.turnId,
656
+ toolCallId: event.toolCallId
657
+ }) ?? envelopeRef;
658
+ open.set(ref, {
659
+ ref,
660
+ ...parentRef ? { parentRef } : {},
661
+ chainKey: chainKey(event),
662
+ isTask: true
663
+ });
664
+ taskTurnCounts.set(ref, 0);
665
+ const agent = readString(event, "agent");
666
+ return [{
667
+ kind: "open",
668
+ ref,
669
+ name: agent ? `flue.task ${agent}` : "flue.task",
670
+ spanKind: "internal",
671
+ ...parentRef ? { parentRef } : {},
672
+ ...event.timestamp ? { startTime: event.timestamp } : {},
673
+ attributes: {
674
+ ...identifiers(event),
675
+ ...agent ? { [GEN_AI.agentName]: agent } : {}
676
+ }
677
+ }];
678
+ }
679
+ function onTaskEnd(event) {
680
+ const ref = taskKey(event);
681
+ if (!open.has(ref)) return [];
682
+ const intents = sweepDescendants(ref, event.timestamp);
683
+ intents.push(closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event)));
684
+ open.delete(ref);
685
+ taskTurnCounts.delete(ref);
686
+ return intents;
687
+ }
688
+ function onCompactionStart(event) {
689
+ const ref = compactionKey(event);
690
+ if (open.has(ref)) return [];
691
+ const parentRef = resolveParentRef(event);
692
+ open.set(ref, {
693
+ ref,
694
+ ...parentRef ? { parentRef } : {},
695
+ chainKey: chainKey(event),
696
+ isTask: false
697
+ });
698
+ const reason = readString(event, "reason");
699
+ return [{
700
+ kind: "open",
701
+ ref,
702
+ name: "flue.compaction",
703
+ spanKind: "internal",
704
+ requiresParent: true,
705
+ ...parentRef ? { parentRef } : {},
706
+ ...event.timestamp ? { startTime: event.timestamp } : {},
707
+ attributes: {
708
+ ...identifiers(event),
709
+ ...reason ? { [FLUE.compactionReason]: reason } : {}
710
+ }
711
+ }];
712
+ }
713
+ function onCompactionEnd(event) {
714
+ const ref = compactionKey(event);
715
+ if (!open.has(ref)) return [];
716
+ if (!event.taskId) {
717
+ const envelope = resolveEnvelopeRef(event);
718
+ const state = envelope ? envelopes.get(envelope) : void 0;
719
+ if (state) state.compacted = true;
720
+ }
721
+ const intents = sweepDescendants(ref, event.timestamp);
722
+ intents.push(closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event)));
723
+ open.delete(ref);
724
+ return intents;
725
+ }
726
+ function onTurnRequest(event) {
727
+ const ref = turnKey(event);
728
+ if (open.has(ref)) return [];
729
+ const request = readRecord(event, "request");
730
+ if (!request) return [];
731
+ const purpose = readString(event, "purpose") ?? "agent";
732
+ const compactionRef = compactionKey(event);
733
+ const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
734
+ open.set(ref, {
735
+ ref,
736
+ ...parentRef ? { parentRef } : {},
737
+ chainKey: chainKey(event),
738
+ isTask: false
739
+ });
740
+ const envelopeRef = resolveEnvelopeRef(event);
741
+ const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
742
+ let position;
743
+ if (purpose === "agent") {
744
+ if (!event.taskId && envelope && envelopeRef) {
745
+ position = {
746
+ turnId: event.turnId ?? "",
747
+ index: envelope.turnCount,
748
+ atEnvelopeScope: true,
749
+ envelopeRef
750
+ };
751
+ envelope.turnCount += 1;
752
+ envelope.maxIteration = position.index;
753
+ } else if (event.taskId) {
754
+ const taskRef = taskKey(event);
755
+ const index = taskTurnCounts.get(taskRef) ?? 0;
756
+ taskTurnCounts.set(taskRef, index + 1);
757
+ position = {
758
+ turnId: event.turnId ?? "",
759
+ index,
760
+ atEnvelopeScope: false,
761
+ ...envelopeRef ? { envelopeRef } : {}
762
+ };
763
+ }
764
+ if (position) turnPositions.set(ref, position);
765
+ }
766
+ if (envelope) {
767
+ if (!envelope.modelPinned && purpose === "agent") envelope.requestModel = request.requestedModel;
768
+ if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
769
+ if (position?.atEnvelopeScope) captureInput(envelope, request);
770
+ }
771
+ return [{
772
+ kind: "open",
773
+ ref,
774
+ name: request.requestedModel ? `${GEN_AI_OPERATION_CHAT} ${request.requestedModel}` : GEN_AI_OPERATION_CHAT,
775
+ spanKind: "client",
776
+ ...parentRef ? { parentRef } : {},
777
+ ...event.timestamp ? { startTime: event.timestamp } : {},
778
+ attributes: {
779
+ ...identifiers(event),
780
+ [GEN_AI.operationName]: GEN_AI_OPERATION_CHAT,
781
+ [FLUE.turnPurpose]: purpose,
782
+ ...request.providerName ? { [GEN_AI.providerName]: request.providerName } : {},
783
+ ...request.requestedModel ? { [GEN_AI.requestModel]: request.requestedModel } : {},
784
+ ...event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
785
+ ...event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
786
+ ...request.reasoningLevel ? { [GEN_AI.reasoningLevel]: request.reasoningLevel } : {},
787
+ ...typeof request.maxTokens === "number" ? { [GEN_AI.maxTokens]: request.maxTokens } : {},
788
+ ...typeof request.temperature === "number" ? { [GEN_AI.temperature]: request.temperature } : {},
789
+ ...request.serverAddress ? { [GEN_AI.serverAddress]: request.serverAddress } : {},
790
+ ...typeof request.serverPort === "number" ? { [GEN_AI.serverPort]: request.serverPort } : {},
791
+ ...request.contextCompacted ? { [GEN_AI.conversationCompacted]: true } : {},
792
+ ...positionAttributes(position)
793
+ }
794
+ }];
795
+ }
796
+ function onTurnEnd(event) {
797
+ const ref = turnKey(event);
798
+ if (!open.has(ref)) return [];
799
+ const response = readRecord(event, "response") ?? {};
800
+ const purpose = readString(event, "purpose") ?? "agent";
801
+ const isError = readBoolean(event, "isError") === true;
802
+ const envelopeRef = resolveEnvelopeRef(event);
803
+ const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
804
+ if (envelope) {
805
+ if (!envelope.modelPinned && purpose === "agent") {
806
+ envelope.responseModel = response.responseModel;
807
+ envelope.modelPinned = true;
808
+ }
809
+ foldUsage(envelope, response.usage);
810
+ if (purpose === "agent" && response.finishReason) envelope.lastFinishReason = response.finishReason;
811
+ if (content.outputMessages && purpose === "agent" && !event.taskId && !isError) {
812
+ const output = projectFlueMessage(response.output);
813
+ if (output?.role === "assistant") envelope.outputMessage = output;
814
+ }
815
+ }
816
+ const attributes = {
817
+ ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
818
+ ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
819
+ ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
820
+ ...usageAttributes(response.usage),
821
+ ...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
822
+ ...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
823
+ };
824
+ const intents = [];
825
+ if (Object.keys(attributes).length > 0) intents.push({
826
+ kind: "update",
827
+ ref,
828
+ attributes
829
+ });
830
+ intents.push(closeIntent(ref, event, isError, response.error ?? readErrorInfo(event)));
831
+ open.delete(ref);
832
+ return intents;
833
+ }
834
+ function onToolStart(event) {
835
+ const toolName = readString(event, "toolName");
836
+ const toolCallId = readString(event, "toolCallId");
837
+ if (!toolName || !toolCallId) return [];
838
+ const ref = toolKey(event);
839
+ if (open.has(ref)) return [];
840
+ const parentRef = resolveParentRef(event);
841
+ open.set(ref, {
842
+ ref,
843
+ ...parentRef ? { parentRef } : {},
844
+ chainKey: chainKey(event),
845
+ isTask: false
846
+ });
847
+ const position = turnPositions.get(turnKey(event));
848
+ if (isCallerShellCall(toolName, event)) return [{
849
+ kind: "open",
850
+ ref,
851
+ name: "flue.operation shell",
852
+ spanKind: "internal",
853
+ requiresParent: true,
854
+ ...parentRef ? { parentRef } : {},
855
+ ...event.timestamp ? { startTime: event.timestamp } : {},
856
+ attributes: {
857
+ ...identifiers(event),
858
+ ...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
859
+ ...positionAttributes(position)
860
+ }
861
+ }];
862
+ const isDelegation = isTaskDelegationTool(toolName, event);
863
+ const toolType = resolveToolType(toolName, event);
864
+ const args = content.toolArguments ? encodeContentValue(readUnknown(event, "args"), content, {
865
+ kind: "arguments",
866
+ toolName
867
+ }) : void 0;
868
+ return [{
869
+ kind: "open",
870
+ ref,
871
+ name: `${GEN_AI_OPERATION_EXECUTE_TOOL} ${toolName}`,
872
+ spanKind: "internal",
873
+ ...parentRef ? { parentRef } : {},
874
+ ...event.timestamp ? { startTime: event.timestamp } : {},
875
+ attributes: {
876
+ ...identifiers(event),
877
+ [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
878
+ [GEN_AI.toolName]: toolName,
879
+ [GEN_AI.toolCallId]: toolCallId,
880
+ ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
881
+ ...isDelegation ? {} : { [GEN_AI.toolType]: "function" },
882
+ ...toolType ? { [RUNTYPE.toolType]: toolType } : {},
883
+ ...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
884
+ ...event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
885
+ ...event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
886
+ ...positionAttributes(position)
887
+ }
888
+ }];
889
+ }
890
+ function onToolEnd(event) {
891
+ const ref = toolKey(event);
892
+ if (!open.has(ref)) return [];
893
+ const intents = sweepDescendants(ref, event.timestamp);
894
+ const isError = readBoolean(event, "isError") === true;
895
+ const toolName = readString(event, "toolName");
896
+ if (content.toolResults && !isError && toolName && !isCallerShellCall(toolName, event)) {
897
+ const result = encodeContentValue(readToolResult(event), content, {
898
+ kind: "result",
899
+ toolName
900
+ });
901
+ if (result !== void 0) intents.push({
902
+ kind: "update",
903
+ ref,
904
+ attributes: { [GEN_AI_CONTENT.toolCallResult]: result }
905
+ });
906
+ }
907
+ intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
908
+ open.delete(ref);
909
+ return intents;
910
+ }
911
+ function onSubmissionSettled(event) {
912
+ const outcome = readString(event, "outcome");
913
+ if (!outcome) return [];
914
+ const stopReason = mapSettlementOutcome(outcome);
915
+ if (!stopReason) return [];
916
+ const submissionId = readString(event, "submissionId");
917
+ if (!submissionId) return [];
918
+ const ref = envelopeBySubmission.get(submissionId);
919
+ const envelope = ref ? envelopes.get(ref) : void 0;
920
+ if (envelope) envelope.settlementStopReason = stopReason;
921
+ return [];
922
+ }
923
+ /**
924
+ * Fold the run-level facts onto the envelope. Applied once, at close, so a
925
+ * split export cannot ship a half-summed total: the batch carrying the
926
+ * terminal carries the complete roll-up by construction.
927
+ */
928
+ function envelopeAttributes(envelope, isError) {
929
+ const stopReason = isError ? "error" : envelope.settlementStopReason ?? mapFinishReason(envelope.lastFinishReason);
930
+ return {
931
+ ...envelope.requestModel ? { [GEN_AI.requestModel]: envelope.requestModel } : {},
932
+ ...envelope.responseModel ? { [GEN_AI.responseModel]: envelope.responseModel } : {},
933
+ ...envelope.sawUsage ? {
934
+ [GEN_AI.usageInputTokens]: envelope.inputTokens,
935
+ [GEN_AI.usageOutputTokens]: envelope.outputTokens,
936
+ [GEN_AI.usageCacheReadTokens]: envelope.cacheReadTokens,
937
+ [GEN_AI.usageCacheCreationTokens]: envelope.cacheCreationTokens
938
+ } : {},
939
+ ...stopReason ? { [RUNTYPE.stopReason]: stopReason } : {},
940
+ ...envelope.toolsReported ? { [RUNTYPE.toolsReported]: true } : {},
941
+ ...envelope.maxIteration !== null ? { [RUNTYPE.iteration]: envelope.maxIteration } : {},
942
+ ...envelopeContentAttributes(envelope)
943
+ };
944
+ }
945
+ /**
946
+ * The first agent turn's history is the conversation the run was given. Later turns re-capture it
947
+ * plus any user message dispatched mid-run, but never the run's own assistant rows (the tool spans
948
+ * and the output carry those) and never a post-compaction summary in place of an earlier capture.
949
+ */
950
+ function captureInput(envelope, request) {
951
+ const input = request.input;
952
+ if (envelope.inputRaw && (envelope.compacted || isCompactedHistory(input?.messages))) return;
953
+ if (content.inputMessages && Array.isArray(input?.messages) && input.messages.length > 0) {
954
+ envelope.inputRaw = input.messages;
955
+ envelope.inputBaseCount ??= input.messages.length;
956
+ }
957
+ if (content.systemInstructions && typeof input?.systemPrompt === "string") envelope.systemInstructions = input.systemPrompt;
958
+ }
959
+ /**
960
+ * Flue does not flag a compacted request on either line, so the summary row it puts first is the
961
+ * signal; the `compaction` observation is the other, and both are honoured.
962
+ */
963
+ function isCompactedHistory(messages) {
964
+ if (!Array.isArray(messages) || messages.length === 0) return false;
965
+ const first = messages[0];
966
+ if (!first || first.role !== "user") return false;
967
+ return flueMessageText(first.content).startsWith(COMPACTION_SUMMARY_PREFIX);
968
+ }
969
+ /** The seed: the first turn's history plus later user rows, never the run's own assistant rows. */
970
+ function projectInput(envelope) {
971
+ const raw = envelope.inputRaw ?? [];
972
+ const base = envelope.inputBaseCount ?? raw.length;
973
+ return [...projectFlueMessages(raw.slice(0, base)), ...projectFlueMessages(raw.slice(base)).filter((message) => message.role !== "assistant")];
974
+ }
975
+ /** Encoded once, at close, so the redaction hook and the size ceiling run once per run. */
976
+ function envelopeContentAttributes(envelope) {
977
+ const input = envelope.inputRaw ? encodeMessagesValue(projectInput(envelope), content, "input_messages") : void 0;
978
+ const system = envelope.systemInstructions ? encodeSystemInstructionsValue(envelope.systemInstructions, content) : void 0;
979
+ const output = envelope.outputMessage ? encodeMessagesValue([envelope.outputMessage], content, "output_messages") : void 0;
980
+ return {
981
+ ...input !== void 0 ? { [GEN_AI_CONTENT.inputMessages]: input } : {},
982
+ ...system !== void 0 ? { [GEN_AI_CONTENT.systemInstructions]: system } : {},
983
+ ...output !== void 0 ? { [GEN_AI_CONTENT.outputMessages]: output } : {}
984
+ };
985
+ }
986
+ /**
987
+ * Find the enclosing task, tool, or operation span. Undefined lets the driver inherit the
988
+ * interceptor's active context for nesting not expressible in correlation fields.
989
+ */
990
+ /**
991
+ * The envelope a span's work belongs to.
992
+ *
993
+ * Task-scoped events resolve through the task index, because a delegated
994
+ * sub-agent's chain fields describe its own child session and can never match
995
+ * the caller's. Everything else resolves by chain, as before.
996
+ */
997
+ function resolveEnvelopeRef(event) {
998
+ if (event.taskId) {
999
+ const viaTask = envelopeByTaskScope.get(taskKey(event));
1000
+ if (viaTask) return viaTask;
1001
+ }
1002
+ return envelopeByChain.get(chainKey(event));
1003
+ }
1004
+ function resolveParentRef(scope) {
1005
+ if (scope.taskId) {
1006
+ const ref = taskKey(scope);
1007
+ if (open.has(ref)) return ref;
1008
+ }
1009
+ if (scope.toolCallId) {
1010
+ const ref = toolKey(scope);
1011
+ if (open.has(ref)) return ref;
1012
+ }
1013
+ if (scope.operationId) {
1014
+ const ref = operationKey(scope);
1015
+ if (open.has(ref)) return ref;
1016
+ }
1017
+ }
1018
+ /**
1019
+ * Close descendants deepest-first using recorded parentRef links; delegated task keys need not share a
1020
+ * prefix with their parent.
1021
+ */
1022
+ function sweepDescendants(parentRef, endTime) {
1023
+ const doomed = [];
1024
+ const frontier = [parentRef];
1025
+ while (frontier.length > 0) {
1026
+ const current = frontier.pop();
1027
+ for (const record of open.values()) {
1028
+ if (record.parentRef !== current) continue;
1029
+ doomed.push(record.ref);
1030
+ frontier.push(record.ref);
1031
+ }
1032
+ }
1033
+ const intents = [];
1034
+ for (const ref of doomed.reverse()) {
1035
+ const record = open.get(ref);
1036
+ if (!record) continue;
1037
+ intents.push({
1038
+ kind: "close",
1039
+ ref,
1040
+ ...endTime ? { endTime } : {},
1041
+ error: { type: "interrupted" }
1042
+ });
1043
+ open.delete(ref);
1044
+ if (record.isTask) taskTurnCounts.delete(ref);
1045
+ }
1046
+ return intents;
1047
+ }
1048
+ function drain(endTime) {
1049
+ const intents = [];
1050
+ for (const ref of [...open.keys()].reverse()) {
1051
+ const envelope = envelopes.get(ref);
1052
+ if (envelope) intents.push({
1053
+ kind: "update",
1054
+ ref,
1055
+ attributes: envelopeAttributes(envelope, true)
1056
+ });
1057
+ intents.push({
1058
+ kind: "close",
1059
+ ref,
1060
+ ...endTime ? { endTime } : {},
1061
+ error: { type: "interrupted" }
1062
+ });
1063
+ }
1064
+ open.clear();
1065
+ envelopes.clear();
1066
+ envelopeByChain.clear();
1067
+ envelopeBySubmission.clear();
1068
+ envelopeByOperationId.clear();
1069
+ envelopeByTaskScope.clear();
1070
+ turnPositions.clear();
1071
+ taskTurnCounts.clear();
1072
+ return intents;
1073
+ }
1074
+ return {
1075
+ observe,
1076
+ drain,
1077
+ hasOpenSpan: (ref) => open.has(ref)
1078
+ };
948
1079
  }
949
1080
  function freshEnvelope() {
950
- return {
951
- turnCount: 0,
952
- maxIteration: null,
953
- modelPinned: false,
954
- inputTokens: 0,
955
- outputTokens: 0,
956
- cacheReadTokens: 0,
957
- cacheCreationTokens: 0,
958
- sawUsage: false,
959
- toolsReported: false
960
- };
1081
+ return {
1082
+ turnCount: 0,
1083
+ maxIteration: null,
1084
+ modelPinned: false,
1085
+ inputTokens: 0,
1086
+ outputTokens: 0,
1087
+ cacheReadTokens: 0,
1088
+ cacheCreationTokens: 0,
1089
+ sawUsage: false,
1090
+ toolsReported: false
1091
+ };
961
1092
  }
962
1093
  function foldUsage(envelope, usage) {
963
- if (!usage) return;
964
- const input = positiveNumber(usage.input);
965
- const output = positiveNumber(usage.output);
966
- const cacheRead = positiveNumber(usage.cacheRead);
967
- const cacheWrite = positiveNumber(usage.cacheWrite);
968
- if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return;
969
- envelope.sawUsage = true;
970
- envelope.inputTokens += input + cacheRead + cacheWrite;
971
- envelope.outputTokens += output;
972
- envelope.cacheReadTokens += cacheRead;
973
- envelope.cacheCreationTokens += cacheWrite;
1094
+ if (!usage) return;
1095
+ const input = positiveNumber(usage.input);
1096
+ const output = positiveNumber(usage.output);
1097
+ const cacheRead = positiveNumber(usage.cacheRead);
1098
+ const cacheWrite = positiveNumber(usage.cacheWrite);
1099
+ if (input === 0 && output === 0 && cacheRead === 0 && cacheWrite === 0) return;
1100
+ envelope.sawUsage = true;
1101
+ envelope.inputTokens += input + cacheRead + cacheWrite;
1102
+ envelope.outputTokens += output;
1103
+ envelope.cacheReadTokens += cacheRead;
1104
+ envelope.cacheCreationTokens += cacheWrite;
974
1105
  }
975
1106
  function usageAttributes(usage) {
976
- if (!usage) return {};
977
- const cacheRead = positiveNumber(usage.cacheRead);
978
- const cacheWrite = positiveNumber(usage.cacheWrite);
979
- return {
980
- [GEN_AI.usageInputTokens]: positiveNumber(usage.input) + cacheRead + cacheWrite,
981
- [GEN_AI.usageOutputTokens]: positiveNumber(usage.output),
982
- [GEN_AI.usageCacheReadTokens]: cacheRead,
983
- [GEN_AI.usageCacheCreationTokens]: cacheWrite,
984
- ...typeof usage.totalTokens === "number" ? { [FLUE.usageTotalTokens]: usage.totalTokens } : {}
985
- };
1107
+ if (!usage) return {};
1108
+ const cacheRead = positiveNumber(usage.cacheRead);
1109
+ const cacheWrite = positiveNumber(usage.cacheWrite);
1110
+ return {
1111
+ [GEN_AI.usageInputTokens]: positiveNumber(usage.input) + cacheRead + cacheWrite,
1112
+ [GEN_AI.usageOutputTokens]: positiveNumber(usage.output),
1113
+ [GEN_AI.usageCacheReadTokens]: cacheRead,
1114
+ [GEN_AI.usageCacheCreationTokens]: cacheWrite,
1115
+ ...typeof usage.totalTokens === "number" ? { [FLUE.usageTotalTokens]: usage.totalTokens } : {}
1116
+ };
986
1117
  }
987
1118
  function positionAttributes(position) {
988
- if (!position) return {};
989
- return {
990
- ...position.turnId ? { [RUNTYPE.turnId]: position.turnId } : {},
991
- [RUNTYPE.turnIndex]: position.index,
992
- // WHY(packages/flue-otel/src/projection.ts): Do not stamp delegated turns with root iteration positions; they would inflate the run iteration count.
993
- ...position.atEnvelopeScope ? { [RUNTYPE.iteration]: position.index } : {}
994
- };
1119
+ if (!position) return {};
1120
+ return {
1121
+ ...position.turnId ? { [RUNTYPE.turnId]: position.turnId } : {},
1122
+ [RUNTYPE.turnIndex]: position.index,
1123
+ ...position.atEnvelopeScope ? { [RUNTYPE.iteration]: position.index } : {}
1124
+ };
995
1125
  }
1126
+ /**
1127
+ * The vendor correlation identifiers, on every span. These are what the
1128
+ * server-side Flue normalizer detects on, and what a customer's existing
1129
+ * dashboards are grouped by; see the note on `FLUE` in `semconv.ts`.
1130
+ */
996
1131
  function identifiers(event) {
997
- return {
998
- ...event.instanceId ? { [FLUE.instanceId]: event.instanceId } : {},
999
- ...event.submissionId ? { [FLUE.submissionId]: event.submissionId } : {},
1000
- ...event.agentName ? { [FLUE.agentName]: event.agentName } : {},
1001
- ...event.harness ? { [FLUE.harnessName]: event.harness } : {},
1002
- ...event.session ? { [FLUE.sessionName]: event.session } : {},
1003
- ...event.parentSession ? { [FLUE.parentSessionName]: event.parentSession } : {},
1004
- ...event.operationId ? { [FLUE.operationId]: event.operationId } : {},
1005
- ...event.turnId ? { [FLUE.turnId]: event.turnId } : {},
1006
- ...event.taskId ? { [FLUE.taskId]: event.taskId } : {},
1007
- ...typeof event.eventIndex === "number" ? { [FLUE.eventIndex]: event.eventIndex } : {}
1008
- };
1132
+ return {
1133
+ ...event.instanceId ? { [FLUE.instanceId]: event.instanceId } : {},
1134
+ ...event.submissionId ? { [FLUE.submissionId]: event.submissionId } : {},
1135
+ ...event.agentName ? { [FLUE.agentName]: event.agentName } : {},
1136
+ ...event.harness ? { [FLUE.harnessName]: event.harness } : {},
1137
+ ...event.session ? { [FLUE.sessionName]: event.session } : {},
1138
+ ...event.parentSession ? { [FLUE.parentSessionName]: event.parentSession } : {},
1139
+ ...event.operationId ? { [FLUE.operationId]: event.operationId } : {},
1140
+ ...event.turnId ? { [FLUE.turnId]: event.turnId } : {},
1141
+ ...event.taskId ? { [FLUE.taskId]: event.taskId } : {},
1142
+ ...typeof event.eventIndex === "number" ? { [FLUE.eventIndex]: event.eventIndex } : {}
1143
+ };
1009
1144
  }
1010
1145
  function closeIntent(ref, event, isError, errorInfo) {
1011
- return {
1012
- kind: "close",
1013
- ref,
1014
- ...event.timestamp ? { endTime: event.timestamp } : {},
1015
- ...isError ? {
1016
- error: {
1017
- type: errorInfo?.type ?? "_OTHER",
1018
- ...errorInfo?.name ? { exceptionType: errorInfo.name } : {}
1019
- }
1020
- } : {}
1021
- };
1146
+ return {
1147
+ kind: "close",
1148
+ ref,
1149
+ ...event.timestamp ? { endTime: event.timestamp } : {},
1150
+ ...isError ? { error: {
1151
+ type: errorInfo?.type ?? "_OTHER",
1152
+ ...errorInfo?.name ? { exceptionType: errorInfo.name } : {}
1153
+ } } : {}
1154
+ };
1022
1155
  }
1023
1156
  function readErrorInfo(event) {
1024
- const detail = event.errorInfo;
1025
- return detail !== null && typeof detail === "object" ? detail : void 0;
1157
+ const detail = event.errorInfo;
1158
+ return detail !== null && typeof detail === "object" ? detail : void 0;
1026
1159
  }
1027
1160
  function readUnknown(event, key) {
1028
- return event[key];
1161
+ return event[key];
1029
1162
  }
1163
+ /**
1164
+ * Prefer effectiveResult when the key exists, even if undefined: it is the post-processed value the
1165
+ * model saw. Fall back to raw result only when the key is absent.
1166
+ */
1030
1167
  function readToolResult(event) {
1031
- const record = event;
1032
- return Object.prototype.hasOwnProperty.call(record, "effectiveResult") ? record.effectiveResult : record.result;
1168
+ const record = event;
1169
+ return Object.prototype.hasOwnProperty.call(record, "effectiveResult") ? record.effectiveResult : record.result;
1033
1170
  }
1034
1171
  function readString(event, key) {
1035
- const value = event[key];
1036
- return typeof value === "string" && value.length > 0 ? value : void 0;
1172
+ const value = event[key];
1173
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1037
1174
  }
1038
1175
  function readBoolean(event, key) {
1039
- const value = event[key];
1040
- return typeof value === "boolean" ? value : void 0;
1176
+ const value = event[key];
1177
+ return typeof value === "boolean" ? value : void 0;
1041
1178
  }
1042
1179
  function readRecord(event, key) {
1043
- const value = event[key];
1044
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1180
+ const value = event[key];
1181
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
1045
1182
  }
1046
1183
  function positiveNumber(value) {
1047
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
1184
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
1048
1185
  }
1186
+ /**
1187
+ * Scope one run by instance, harness, conversation, and session. Excluding operation/submission ids
1188
+ * relies on Flue serializing operations per session; adding them would disconnect turn-to-envelope
1189
+ * lookup.
1190
+ */
1049
1191
  function chainKey(value) {
1050
- return identityKey("chain", [
1051
- value.instanceId,
1052
- value.harness,
1053
- value.conversationId,
1054
- value.session
1055
- ]);
1192
+ return identityKey("chain", [
1193
+ value.instanceId,
1194
+ value.harness,
1195
+ value.conversationId,
1196
+ value.session
1197
+ ]);
1056
1198
  }
1057
1199
  function operationKey(value) {
1058
- return identityKey("operation", [
1059
- value.instanceId,
1060
- value.harness,
1061
- value.conversationId,
1062
- value.session,
1063
- value.taskId,
1064
- value.operationId
1065
- ]);
1200
+ return identityKey("operation", [
1201
+ value.instanceId,
1202
+ value.harness,
1203
+ value.conversationId,
1204
+ value.session,
1205
+ value.taskId,
1206
+ value.operationId
1207
+ ]);
1066
1208
  }
1067
1209
  function taskKey(value) {
1068
- return identityKey("task", [
1069
- value.instanceId,
1070
- value.harness,
1071
- value.conversationId,
1072
- value.session,
1073
- value.taskId
1074
- ]);
1210
+ return identityKey("task", [
1211
+ value.instanceId,
1212
+ value.harness,
1213
+ value.conversationId,
1214
+ value.session,
1215
+ value.taskId
1216
+ ]);
1075
1217
  }
1076
1218
  function compactionKey(value) {
1077
- return identityKey("compaction", [
1078
- value.instanceId,
1079
- value.harness,
1080
- value.conversationId,
1081
- value.session,
1082
- value.taskId,
1083
- value.operationId
1084
- ]);
1219
+ return identityKey("compaction", [
1220
+ value.instanceId,
1221
+ value.harness,
1222
+ value.conversationId,
1223
+ value.session,
1224
+ value.taskId,
1225
+ value.operationId
1226
+ ]);
1085
1227
  }
1086
1228
  function turnKey(value) {
1087
- return identityKey("turn", [
1088
- value.instanceId,
1089
- value.harness,
1090
- value.conversationId,
1091
- value.session,
1092
- value.taskId,
1093
- value.operationId,
1094
- value.turnId
1095
- ]);
1229
+ return identityKey("turn", [
1230
+ value.instanceId,
1231
+ value.harness,
1232
+ value.conversationId,
1233
+ value.session,
1234
+ value.taskId,
1235
+ value.operationId,
1236
+ value.turnId
1237
+ ]);
1096
1238
  }
1097
1239
  function toolKey(value) {
1098
- return identityKey("tool", [
1099
- value.instanceId,
1100
- value.harness,
1101
- value.conversationId,
1102
- value.session,
1103
- value.taskId,
1104
- value.operationId,
1105
- value.turnId,
1106
- value.toolCallId
1107
- ]);
1240
+ return identityKey("tool", [
1241
+ value.instanceId,
1242
+ value.harness,
1243
+ value.conversationId,
1244
+ value.session,
1245
+ value.taskId,
1246
+ value.operationId,
1247
+ value.turnId,
1248
+ value.toolCallId
1249
+ ]);
1108
1250
  }
1109
1251
  function identityKey(kind, fields) {
1110
- return JSON.stringify([kind, ...fields.map((value) => value ?? null)]);
1252
+ return JSON.stringify([kind, ...fields.map((value) => value ?? null)]);
1111
1253
  }
1112
-
1113
- // src/spans.ts
1114
- var import_api = require("@opentelemetry/api");
1254
+ //#endregion
1255
+ //#region src/spans.ts
1115
1256
  function createSpanDriver(tracer) {
1116
- const live = /* @__PURE__ */ new Map();
1117
- function apply(intents) {
1118
- for (const intent of intents) {
1119
- switch (intent.kind) {
1120
- case "open": {
1121
- if (live.has(intent.ref)) break;
1122
- const parent = intent.parentRef ? live.get(intent.parentRef) : void 0;
1123
- const active = import_api.context.active();
1124
- const parentContext = parent ? import_api.trace.setSpan(active, parent) : import_api.trace.getSpanContext(active) ? active : void 0;
1125
- if (parentContext === void 0 && intent.requiresParent) break;
1126
- const span = tracer.startSpan(
1127
- intent.name,
1128
- {
1129
- kind: intent.spanKind === "client" ? import_api.SpanKind.CLIENT : import_api.SpanKind.INTERNAL,
1130
- root: parentContext === void 0,
1131
- ...intent.startTime ? { startTime: toDate(intent.startTime) } : {},
1132
- attributes: intent.attributes
1133
- },
1134
- parentContext
1135
- );
1136
- live.set(intent.ref, span);
1137
- break;
1138
- }
1139
- case "update": {
1140
- live.get(intent.ref)?.setAttributes(intent.attributes);
1141
- break;
1142
- }
1143
- case "close": {
1144
- const span = live.get(intent.ref);
1145
- if (!span) break;
1146
- if (intent.error) {
1147
- span.setAttribute(GEN_AI.errorType, intent.error.type);
1148
- span.setStatus({ code: import_api.SpanStatusCode.ERROR });
1149
- if (intent.error.exceptionType) {
1150
- span.recordException({ name: intent.error.exceptionType });
1151
- }
1152
- }
1153
- span.end(intent.endTime ? toDate(intent.endTime) : void 0);
1154
- live.delete(intent.ref);
1155
- break;
1156
- }
1157
- }
1158
- }
1159
- }
1160
- return { apply, spanFor: (ref) => live.get(ref) };
1257
+ const live = /* @__PURE__ */ new Map();
1258
+ function apply(intents) {
1259
+ for (const intent of intents) switch (intent.kind) {
1260
+ case "open": {
1261
+ if (live.has(intent.ref)) break;
1262
+ const parent = intent.parentRef ? live.get(intent.parentRef) : void 0;
1263
+ const active = _opentelemetry_api.context.active();
1264
+ const parentContext = parent ? _opentelemetry_api.trace.setSpan(active, parent) : _opentelemetry_api.trace.getSpanContext(active) ? active : void 0;
1265
+ if (parentContext === void 0 && intent.requiresParent) break;
1266
+ const span = tracer.startSpan(intent.name, {
1267
+ kind: intent.spanKind === "client" ? _opentelemetry_api.SpanKind.CLIENT : _opentelemetry_api.SpanKind.INTERNAL,
1268
+ root: parentContext === void 0,
1269
+ ...intent.startTime ? { startTime: toDate(intent.startTime) } : {},
1270
+ attributes: intent.attributes
1271
+ }, parentContext);
1272
+ live.set(intent.ref, span);
1273
+ break;
1274
+ }
1275
+ case "update":
1276
+ live.get(intent.ref)?.setAttributes(intent.attributes);
1277
+ break;
1278
+ case "close": {
1279
+ const span = live.get(intent.ref);
1280
+ if (!span) break;
1281
+ if (intent.error) {
1282
+ span.setAttribute(GEN_AI.errorType, intent.error.type);
1283
+ span.setStatus({ code: _opentelemetry_api.SpanStatusCode.ERROR });
1284
+ if (intent.error.exceptionType) span.recordException({ name: intent.error.exceptionType });
1285
+ }
1286
+ span.end(intent.endTime ? toDate(intent.endTime) : void 0);
1287
+ live.delete(intent.ref);
1288
+ break;
1289
+ }
1290
+ }
1291
+ }
1292
+ return {
1293
+ apply,
1294
+ spanFor: (ref) => live.get(ref)
1295
+ };
1161
1296
  }
1297
+ /**
1298
+ * Prefer the span for this operation, then the dispatched W3C traceCarrier. Carrier extraction needs
1299
+ * an application-registered propagator; the no-op propagator otherwise preserves context.
1300
+ */
1162
1301
  function contextForOperation(span, carrier) {
1163
- if (span) return import_api.trace.setSpan(import_api.context.active(), span);
1164
- if (carrier) {
1165
- return import_api.propagation.extract(import_api.context.active(), carrier, {
1166
- keys: () => carrier.tracestate ? ["traceparent", "tracestate"] : ["traceparent"],
1167
- get: (value, key) => value[key]
1168
- });
1169
- }
1170
- return void 0;
1302
+ if (span) return _opentelemetry_api.trace.setSpan(_opentelemetry_api.context.active(), span);
1303
+ if (carrier) return _opentelemetry_api.propagation.extract(_opentelemetry_api.context.active(), carrier, {
1304
+ keys: () => carrier.tracestate ? ["traceparent", "tracestate"] : ["traceparent"],
1305
+ get: (value, key) => value[key]
1306
+ });
1171
1307
  }
1308
+ /**
1309
+ * Flue timestamps are ISO-8601 strings. An unparseable one would make
1310
+ * `span.end()` throw inside the runtime's own emit path, so it degrades to the
1311
+ * SDK's clock instead: a span on the wrong clock is a smaller problem than an
1312
+ * instrumentation that can break the agent it observes.
1313
+ */
1172
1314
  function toDate(value) {
1173
- const date = new Date(value);
1174
- return Number.isNaN(date.getTime()) ? void 0 : date;
1315
+ const date = new Date(value);
1316
+ return Number.isNaN(date.getTime()) ? void 0 : date;
1175
1317
  }
1176
-
1177
- // src/index.ts
1178
- var RUNTYPE_FLUE_INSTRUMENTATION_KEY = /* @__PURE__ */ Symbol.for(
1179
- "@runtypelabs/flue-otel"
1180
- );
1318
+ //#endregion
1319
+ //#region src/index.ts
1320
+ /**
1321
+ * The instrumentation key. Distinct from stock's
1322
+ * `Symbol.for('@flue/opentelemetry')` so `instrument()` treats the two as
1323
+ * different subscribers and composes them rather than replacing one.
1324
+ */
1325
+ const RUNTYPE_FLUE_INSTRUMENTATION_KEY = Symbol.for("@runtypelabs/flue-otel");
1326
+ /**
1327
+ * Build an observe/interceptor pair for Flue's instrument(). The interceptor activates spans and joins
1328
+ * dispatched trace carriers. Configure the OTel SDK externally, and point only one instrumentation at
1329
+ * Runtype to avoid double counting.
1330
+ */
1181
1331
  function createRuntypeFlueInstrumentation(options = {}) {
1182
- const tracer = options.tracer ?? import_api2.trace.getTracer(ADAPTER_NAME);
1183
- const projection = createFlueProjection(options);
1184
- const driver = createSpanDriver(tracer);
1185
- let disposed = false;
1186
- const observe = (observation) => {
1187
- if (disposed) return;
1188
- try {
1189
- driver.apply(projection.observe(observation));
1190
- } catch {
1191
- }
1192
- };
1193
- const interceptor = (operation, executionContext, next) => {
1194
- if (disposed) return next();
1195
- const span = spanForOperation(operation, executionContext);
1196
- const active = contextForOperation(span, executionContext.traceCarrier);
1197
- if (!active) return next();
1198
- return import_api2.context.with(active, next);
1199
- };
1200
- function spanForOperation(operation, executionContext) {
1201
- switch (operation.type) {
1202
- case "agent":
1203
- return driver.spanFor(
1204
- operationKey({ ...executionContext, operationId: readIdField(operation, "operationId") })
1205
- );
1206
- case "model":
1207
- return driver.spanFor(
1208
- turnKey({ ...executionContext, turnId: readIdField(operation, "turnId") })
1209
- );
1210
- case "tool":
1211
- return driver.spanFor(
1212
- toolKey({ ...executionContext, toolCallId: readIdField(operation, "toolCallId") })
1213
- );
1214
- case "task":
1215
- return driver.spanFor(
1216
- taskKey({ ...executionContext, taskId: readIdField(operation, "taskId") })
1217
- );
1218
- default:
1219
- return void 0;
1220
- }
1221
- }
1222
- return {
1223
- key: RUNTYPE_FLUE_INSTRUMENTATION_KEY,
1224
- observe,
1225
- interceptor,
1226
- dispose() {
1227
- if (disposed) return;
1228
- disposed = true;
1229
- try {
1230
- driver.apply(projection.drain((/* @__PURE__ */ new Date()).toISOString()));
1231
- } catch {
1232
- }
1233
- }
1234
- };
1332
+ const tracer = options.tracer ?? _opentelemetry_api.trace.getTracer("@runtypelabs/flue-otel");
1333
+ const projection = createFlueProjection(options);
1334
+ const driver = createSpanDriver(tracer);
1335
+ let disposed = false;
1336
+ const observe = (observation) => {
1337
+ if (disposed) return;
1338
+ try {
1339
+ driver.apply(projection.observe(observation));
1340
+ } catch {}
1341
+ };
1342
+ const interceptor = (operation, executionContext, next) => {
1343
+ if (disposed) return next();
1344
+ const active = contextForOperation(spanForOperation(operation, executionContext), executionContext.traceCarrier);
1345
+ if (!active) return next();
1346
+ return _opentelemetry_api.context.with(active, next);
1347
+ };
1348
+ function spanForOperation(operation, executionContext) {
1349
+ switch (operation.type) {
1350
+ case "agent": return driver.spanFor(operationKey({
1351
+ ...executionContext,
1352
+ operationId: readIdField(operation, "operationId")
1353
+ }));
1354
+ case "model": return driver.spanFor(turnKey({
1355
+ ...executionContext,
1356
+ turnId: readIdField(operation, "turnId")
1357
+ }));
1358
+ case "tool": return driver.spanFor(toolKey({
1359
+ ...executionContext,
1360
+ toolCallId: readIdField(operation, "toolCallId")
1361
+ }));
1362
+ case "task": return driver.spanFor(taskKey({
1363
+ ...executionContext,
1364
+ taskId: readIdField(operation, "taskId")
1365
+ }));
1366
+ default: return;
1367
+ }
1368
+ }
1369
+ return {
1370
+ key: RUNTYPE_FLUE_INSTRUMENTATION_KEY,
1371
+ observe,
1372
+ interceptor,
1373
+ dispose() {
1374
+ if (disposed) return;
1375
+ disposed = true;
1376
+ try {
1377
+ driver.apply(projection.drain((/* @__PURE__ */ new Date()).toISOString()));
1378
+ } catch {}
1379
+ }
1380
+ };
1235
1381
  }
1382
+ /**
1383
+ * Read a correlation id off the operation. The `FlueExecutionOperation` union
1384
+ * carries an open `{ type: string }` arm so an unknown future kind falls
1385
+ * through rather than failing to compile, which defeats discriminated-union
1386
+ * narrowing — hence the keyed read instead of a property access.
1387
+ */
1236
1388
  function readIdField(operation, key) {
1237
- const value = operation[key];
1238
- return typeof value === "string" && value.length > 0 ? value : void 0;
1389
+ const value = operation[key];
1390
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1239
1391
  }
1240
- // Annotate the CommonJS export names for ESM import in node:
1241
- 0 && (module.exports = {
1242
- ADAPTER_NAME,
1243
- ADAPTER_VERSION,
1244
- DEFAULT_CONTENT_MAX_CHARS,
1245
- GEN_AI,
1246
- GEN_AI_CONTENT,
1247
- INGEST_CONTENT_ATTRIBUTE_CEILING,
1248
- RUNTYPE,
1249
- RUNTYPE_FLUE_INSTRUMENTATION_KEY,
1250
- RUNTYPE_SCHEMA_VERSION,
1251
- RUNTYPE_STOP_REASONS,
1252
- RUNTYPE_TOOL_TYPES,
1253
- createRuntypeFlueInstrumentation,
1254
- runtypeFlueResourceAttributes
1255
- });
1392
+ //#endregion
1393
+ exports.ADAPTER_NAME = ADAPTER_NAME;
1394
+ exports.ADAPTER_VERSION = ADAPTER_VERSION;
1395
+ exports.DEFAULT_CONTENT_MAX_CHARS = DEFAULT_CONTENT_MAX_CHARS;
1396
+ exports.GEN_AI = GEN_AI;
1397
+ exports.GEN_AI_CONTENT = GEN_AI_CONTENT;
1398
+ exports.INGEST_CONTENT_ATTRIBUTE_CEILING = INGEST_CONTENT_ATTRIBUTE_CEILING;
1399
+ exports.RUNTYPE = RUNTYPE;
1400
+ exports.RUNTYPE_FLUE_INSTRUMENTATION_KEY = RUNTYPE_FLUE_INSTRUMENTATION_KEY;
1401
+ exports.RUNTYPE_SCHEMA_VERSION = RUNTYPE_SCHEMA_VERSION;
1402
+ exports.RUNTYPE_STOP_REASONS = RUNTYPE_STOP_REASONS;
1403
+ exports.RUNTYPE_TOOL_TYPES = RUNTYPE_TOOL_TYPES;
1404
+ exports.createRuntypeFlueInstrumentation = createRuntypeFlueInstrumentation;
1405
+ exports.runtypeFlueResourceAttributes = runtypeFlueResourceAttributes;
1406
+