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