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