@runtypelabs/flue-otel 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +1289 -1138
- package/dist/index.d.cts +318 -312
- package/dist/index.d.ts +318 -312
- package/dist/index.mjs +1276 -1107
- package/package.json +7 -7
package/dist/index.mjs
CHANGED
|
@@ -1,1224 +1,1393 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"safety"
|
|
49
|
+
const CONTENT_FILTER_REASONS = /* @__PURE__ */ new Set([
|
|
50
|
+
"content_filter",
|
|
51
|
+
"contentfilter",
|
|
52
|
+
"refusal",
|
|
53
|
+
"safety"
|
|
38
54
|
]);
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
return void 0;
|
|
66
|
+
if (outcome === "failed" || outcome === "aborted") return "error";
|
|
43
67
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
178
|
+
if (typeof value === "string") return value;
|
|
179
|
+
return Array.isArray(value) ? contentMessageText({ parts: value }) : "";
|
|
112
180
|
}
|
|
113
181
|
function contentMessageText(message) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
193
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
126
194
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
380
|
+
return `…[truncated ${droppedChars} chars]`;
|
|
259
381
|
}
|
|
260
382
|
function isHighSurrogate(code) {
|
|
261
|
-
|
|
383
|
+
return code >= 55296 && code <= 56319;
|
|
262
384
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
var version = "0.5.
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region package.json
|
|
387
|
+
var version = "0.5.5";
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
989
|
-
|
|
1156
|
+
const detail = event.errorInfo;
|
|
1157
|
+
return detail !== null && typeof detail === "object" ? detail : void 0;
|
|
990
1158
|
}
|
|
991
1159
|
function readUnknown(event, key) {
|
|
992
|
-
|
|
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
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
1175
|
+
const value = event[key];
|
|
1176
|
+
return typeof value === "boolean" ? value : void 0;
|
|
1005
1177
|
}
|
|
1006
1178
|
function readRecord(event, key) {
|
|
1007
|
-
|
|
1008
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
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
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
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
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
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
|
-
|
|
1251
|
+
return JSON.stringify([kind, ...fields.map((value) => value ?? null)]);
|
|
1075
1252
|
}
|
|
1076
|
-
|
|
1077
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
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
|
-
|
|
1144
|
-
|
|
1314
|
+
const date = new Date(value);
|
|
1315
|
+
return Number.isNaN(date.getTime()) ? void 0 : date;
|
|
1145
1316
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
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
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
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
|
-
|
|
1208
|
-
|
|
1388
|
+
const value = operation[key];
|
|
1389
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1209
1390
|
}
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
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
|
+
|