@runtypelabs/flue-otel 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -32
- package/dist/index.cjs +172 -46
- package/dist/index.d.cts +81 -299
- package/dist/index.d.ts +81 -299
- package/dist/index.mjs +172 -46
- package/package.json +6 -5
package/dist/index.mjs
CHANGED
|
@@ -42,35 +42,156 @@ function mapSettlementOutcome(outcome) {
|
|
|
42
42
|
return void 0;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// src/messages.ts
|
|
46
|
+
var ROLES = /* @__PURE__ */ new Set(["system", "user", "assistant"]);
|
|
47
|
+
var FLUE_ROLES = /* @__PURE__ */ new Set(["user", "assistant"]);
|
|
48
|
+
var SIGNAL_MESSAGE = /^\s*<([A-Za-z][\w-]*)\b[^>]*>[\s\S]*<\/\1>\s*$/;
|
|
49
|
+
function projectFlueMessages(value) {
|
|
50
|
+
if (!Array.isArray(value)) return [];
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const entry of value) {
|
|
53
|
+
const message = projectFlueMessage(entry);
|
|
54
|
+
if (message) out.push(message);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function projectFlueMessage(value) {
|
|
59
|
+
if (!isRecord(value)) return void 0;
|
|
60
|
+
const role = value.role;
|
|
61
|
+
if (typeof role !== "string" || !FLUE_ROLES.has(role)) return void 0;
|
|
62
|
+
const text = flueMessageText(value.content);
|
|
63
|
+
if (text === "") return void 0;
|
|
64
|
+
if (role === "user" && SIGNAL_MESSAGE.test(text)) return void 0;
|
|
65
|
+
return { role, parts: [{ type: "text", content: text }] };
|
|
66
|
+
}
|
|
67
|
+
function flueMessageText(content) {
|
|
68
|
+
if (typeof content === "string") return content;
|
|
69
|
+
if (!Array.isArray(content)) return "";
|
|
70
|
+
const texts = [];
|
|
71
|
+
for (const part of content) {
|
|
72
|
+
if (!isRecord(part) || part.type !== "text") continue;
|
|
73
|
+
if (typeof part.text === "string" && part.text.length > 0) texts.push(part.text);
|
|
74
|
+
}
|
|
75
|
+
return texts.join("\n");
|
|
76
|
+
}
|
|
77
|
+
function normalizeContentMessages(value) {
|
|
78
|
+
if (!Array.isArray(value)) return [];
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const entry of value) {
|
|
81
|
+
if (!isRecord(entry)) continue;
|
|
82
|
+
const role = entry.role;
|
|
83
|
+
if (typeof role !== "string" || !ROLES.has(role)) continue;
|
|
84
|
+
const text = contentMessageText(entry);
|
|
85
|
+
if (text === "") continue;
|
|
86
|
+
out.push({ role, parts: [{ type: "text", content: text }] });
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function contentTextOf(value) {
|
|
91
|
+
if (typeof value === "string") return value;
|
|
92
|
+
return Array.isArray(value) ? contentMessageText({ parts: value }) : "";
|
|
93
|
+
}
|
|
94
|
+
function contentMessageText(message) {
|
|
95
|
+
if (typeof message.content === "string") return message.content;
|
|
96
|
+
const parts = Array.isArray(message.parts) ? message.parts : Array.isArray(message.content) ? message.content : [];
|
|
97
|
+
const texts = [];
|
|
98
|
+
for (const part of parts) {
|
|
99
|
+
if (!isRecord(part) || part.type !== "text") continue;
|
|
100
|
+
const text = typeof part.content === "string" && part.content.length > 0 ? part.content : typeof part.text === "string" && part.text.length > 0 ? part.text : "";
|
|
101
|
+
if (text !== "") texts.push(text);
|
|
102
|
+
}
|
|
103
|
+
return texts.join("\n");
|
|
104
|
+
}
|
|
105
|
+
function isRecord(value) {
|
|
106
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
45
109
|
// src/content.ts
|
|
46
110
|
var DEFAULT_CONTENT_MAX_CHARS = 65536;
|
|
47
111
|
var INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
|
|
48
|
-
|
|
112
|
+
var INGEST_CONTENT_SPAN_CEILING = 393216;
|
|
113
|
+
function resolveContentPolicy(setting) {
|
|
114
|
+
const options = setting === false ? void 0 : setting;
|
|
115
|
+
const on = (value) => setting !== false && value !== false;
|
|
49
116
|
const requested = options?.maxChars;
|
|
50
117
|
const maxChars = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(Math.floor(requested), INGEST_CONTENT_ATTRIBUTE_CEILING) : DEFAULT_CONTENT_MAX_CHARS;
|
|
51
118
|
return {
|
|
52
|
-
toolArguments: options?.toolArguments
|
|
53
|
-
toolResults: options?.toolResults
|
|
119
|
+
toolArguments: on(options?.toolArguments),
|
|
120
|
+
toolResults: on(options?.toolResults),
|
|
121
|
+
inputMessages: on(options?.inputMessages),
|
|
122
|
+
outputMessages: on(options?.outputMessages),
|
|
123
|
+
systemInstructions: on(options?.systemInstructions),
|
|
54
124
|
maxChars,
|
|
125
|
+
envelopeMaxChars: Math.min(maxChars, Math.floor(INGEST_CONTENT_SPAN_CEILING / 3)),
|
|
55
126
|
redact: options?.redact
|
|
56
127
|
};
|
|
57
128
|
}
|
|
58
129
|
function encodeContentValue(value, policy, context3) {
|
|
59
130
|
if (value === void 0) return void 0;
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
try {
|
|
63
|
-
redacted = policy.redact(value, context3);
|
|
64
|
-
} catch {
|
|
65
|
-
return void 0;
|
|
66
|
-
}
|
|
67
|
-
if (redacted === void 0) return void 0;
|
|
68
|
-
}
|
|
131
|
+
const redacted = applyRedact(value, policy, context3);
|
|
132
|
+
if (redacted === void 0) return void 0;
|
|
69
133
|
if (context3.kind === "arguments") return encodeArguments(redacted, policy.maxChars);
|
|
70
134
|
const encoded = stringify(redacted);
|
|
71
135
|
if (encoded === void 0) return void 0;
|
|
72
136
|
return truncate(encoded, policy.maxChars);
|
|
73
137
|
}
|
|
138
|
+
function encodeMessagesValue(messages, policy, kind) {
|
|
139
|
+
if (messages.length === 0) return void 0;
|
|
140
|
+
const redacted = applyRedact(messages, policy, { kind });
|
|
141
|
+
if (redacted === void 0) return void 0;
|
|
142
|
+
const all = normalizeContentMessages(redacted);
|
|
143
|
+
if (all.length === 0) return void 0;
|
|
144
|
+
const maxChars = policy.envelopeMaxChars;
|
|
145
|
+
let start = all.length;
|
|
146
|
+
let total = 2;
|
|
147
|
+
while (start > 0) {
|
|
148
|
+
const length = JSON.stringify(all[start - 1]).length + (start < all.length ? 1 : 0);
|
|
149
|
+
if (total + length > maxChars) break;
|
|
150
|
+
total += length;
|
|
151
|
+
start -= 1;
|
|
152
|
+
}
|
|
153
|
+
if (start === 0) return JSON.stringify(all);
|
|
154
|
+
if (start < all.length) return JSON.stringify(all.slice(start));
|
|
155
|
+
return encodeTruncatedMessage(all[all.length - 1], maxChars);
|
|
156
|
+
}
|
|
157
|
+
function encodeSystemInstructionsValue(value, policy) {
|
|
158
|
+
if (value.length === 0) return void 0;
|
|
159
|
+
const redacted = applyRedact(value, policy, { kind: "system_instructions" });
|
|
160
|
+
if (redacted === void 0) return void 0;
|
|
161
|
+
const text = contentTextOf(redacted);
|
|
162
|
+
if (text.length === 0) return void 0;
|
|
163
|
+
return encodeTextParts(text, policy.envelopeMaxChars);
|
|
164
|
+
}
|
|
165
|
+
function encodeTruncatedMessage(message, maxChars) {
|
|
166
|
+
const text = message.parts.map((part) => part.content).join("\n");
|
|
167
|
+
return fitJson(
|
|
168
|
+
(cut) => JSON.stringify([{ role: message.role, parts: [{ type: "text", content: cut }] }]),
|
|
169
|
+
text,
|
|
170
|
+
maxChars
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
function encodeTextParts(text, maxChars) {
|
|
174
|
+
return fitJson((cut) => JSON.stringify([{ type: "text", content: cut }]), text, maxChars);
|
|
175
|
+
}
|
|
176
|
+
function fitJson(wrap, text, maxChars) {
|
|
177
|
+
let encoded = wrap(text);
|
|
178
|
+
if (encoded.length <= maxChars) return encoded;
|
|
179
|
+
let budget = maxChars - wrap("").length;
|
|
180
|
+
for (let round = 0; round < 8 && budget > 0; round += 1) {
|
|
181
|
+
encoded = wrap(truncate(text, budget));
|
|
182
|
+
if (encoded.length <= maxChars) return encoded;
|
|
183
|
+
budget -= encoded.length - maxChars;
|
|
184
|
+
}
|
|
185
|
+
return void 0;
|
|
186
|
+
}
|
|
187
|
+
function applyRedact(value, policy, context3) {
|
|
188
|
+
if (!policy.redact) return value;
|
|
189
|
+
try {
|
|
190
|
+
return policy.redact(value, context3);
|
|
191
|
+
} catch {
|
|
192
|
+
return void 0;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
74
195
|
function encodeArguments(value, maxChars) {
|
|
75
196
|
const shaped = isPlainObject(value) ? value : { value };
|
|
76
197
|
const encoded = stringify(shaped);
|
|
@@ -118,7 +239,7 @@ function isHighSurrogate(code) {
|
|
|
118
239
|
}
|
|
119
240
|
|
|
120
241
|
// package.json
|
|
121
|
-
var version = "0.
|
|
242
|
+
var version = "0.5.0";
|
|
122
243
|
|
|
123
244
|
// src/semconv.ts
|
|
124
245
|
var GEN_AI = {
|
|
@@ -147,6 +268,9 @@ var GEN_AI = {
|
|
|
147
268
|
serverPort: "server.port"
|
|
148
269
|
};
|
|
149
270
|
var GEN_AI_CONTENT = {
|
|
271
|
+
inputMessages: "gen_ai.input.messages",
|
|
272
|
+
outputMessages: "gen_ai.output.messages",
|
|
273
|
+
systemInstructions: "gen_ai.system_instructions",
|
|
150
274
|
toolCallArguments: "gen_ai.tool.call.arguments",
|
|
151
275
|
toolCallResult: "gen_ai.tool.call.result"
|
|
152
276
|
};
|
|
@@ -300,11 +424,13 @@ function createFlueProjection(options = {}) {
|
|
|
300
424
|
...isEnvelope ? { [GEN_AI.operationName]: GEN_AI_OPERATION_INVOKE_AGENT } : {},
|
|
301
425
|
...isEnvelope && event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
|
|
302
426
|
...isEnvelope && event.conversationId ? { [GEN_AI.conversationId]: event.conversationId } : {},
|
|
303
|
-
// The execution id LIFTS the tier and is claim-checked at ingest like
|
|
304
|
-
// any inbound id; it does not key the row, which stays derived from
|
|
305
|
-
// the trace id.
|
|
306
427
|
...isEnvelope && event.submissionId ? { [RUNTYPE.executionId]: event.submissionId } : {},
|
|
307
|
-
...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {}
|
|
428
|
+
...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {},
|
|
429
|
+
// WHY(README.md): the resource placement is the customer's to wire and is routinely skipped.
|
|
430
|
+
...isEnvelope ? {
|
|
431
|
+
[RUNTYPE.adapterName]: ADAPTER_NAME,
|
|
432
|
+
[RUNTYPE.adapterVersion]: ADAPTER_VERSION
|
|
433
|
+
} : {}
|
|
308
434
|
}
|
|
309
435
|
}
|
|
310
436
|
];
|
|
@@ -361,9 +487,7 @@ function createFlueProjection(options = {}) {
|
|
|
361
487
|
{
|
|
362
488
|
kind: "open",
|
|
363
489
|
ref,
|
|
364
|
-
//
|
|
365
|
-
// agent-invocation span in the trace is a coin flip over which one
|
|
366
|
-
// becomes the run's envelope.
|
|
490
|
+
// WHY(packages/flue-otel/src/projection.ts): Use a task span, not invoke_agent; one trace is persisted as one execution.
|
|
367
491
|
name: agent ? `flue.task ${agent}` : "flue.task",
|
|
368
492
|
spanKind: "internal",
|
|
369
493
|
...parentRef ? { parentRef } : {},
|
|
@@ -403,8 +527,6 @@ function createFlueProjection(options = {}) {
|
|
|
403
527
|
ref,
|
|
404
528
|
name: "flue.compaction",
|
|
405
529
|
spanKind: "internal",
|
|
406
|
-
// `session.compact()` is callable by the host outside any turn, and a
|
|
407
|
-
// `compact` operation is not intercepted, so this can be parentless.
|
|
408
530
|
requiresParent: true,
|
|
409
531
|
...parentRef ? { parentRef } : {},
|
|
410
532
|
...event.timestamp ? { startTime: event.timestamp } : {},
|
|
@@ -469,6 +591,7 @@ function createFlueProjection(options = {}) {
|
|
|
469
591
|
if (!envelope.modelPinned && purpose === "agent")
|
|
470
592
|
envelope.requestModel = request.requestedModel;
|
|
471
593
|
if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
|
|
594
|
+
if (position?.atEnvelopeScope) captureInput(envelope, request);
|
|
472
595
|
}
|
|
473
596
|
return [
|
|
474
597
|
{
|
|
@@ -514,20 +637,16 @@ function createFlueProjection(options = {}) {
|
|
|
514
637
|
if (purpose === "agent" && response.finishReason) {
|
|
515
638
|
envelope.lastFinishReason = response.finishReason;
|
|
516
639
|
}
|
|
640
|
+
if (content.outputMessages && purpose === "agent" && !event.taskId && !isError) {
|
|
641
|
+
const output = projectFlueMessage(response.output);
|
|
642
|
+
if (output?.role === "assistant") envelope.outputMessage = output;
|
|
643
|
+
}
|
|
517
644
|
}
|
|
518
645
|
const attributes = {
|
|
519
646
|
...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
|
|
520
647
|
...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
|
|
521
648
|
...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
|
|
522
649
|
...usageAttributes(response.usage),
|
|
523
|
-
// Provider-diagnostic pointers, emitted only when Flue actually provides
|
|
524
|
-
// them. Both are optional and provider-dependent (Workers AI attaches
|
|
525
|
-
// both today); an absent value costs one column, a synthesized one would
|
|
526
|
-
// render as a measurement. `gatewayLogId` is a pointer to the content
|
|
527
|
-
// without shipping the content — a customer can click through to that
|
|
528
|
-
// exact request in their own AI Gateway dashboard. `providerFinishReason`
|
|
529
|
-
// is the provider's exact finish value before normalization, which our
|
|
530
|
-
// `GEN_AI.finishReasons` above deliberately hides.
|
|
531
650
|
...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
|
|
532
651
|
...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
|
|
533
652
|
};
|
|
@@ -558,8 +677,6 @@ function createFlueProjection(options = {}) {
|
|
|
558
677
|
ref,
|
|
559
678
|
name: "flue.operation shell",
|
|
560
679
|
spanKind: "internal",
|
|
561
|
-
// `session.shell()` is callable by the host outside any turn, and a
|
|
562
|
-
// `shell` operation is not intercepted, so this can be parentless.
|
|
563
680
|
requiresParent: true,
|
|
564
681
|
...parentRef ? { parentRef } : {},
|
|
565
682
|
...event.timestamp ? { startTime: event.timestamp } : {},
|
|
@@ -591,10 +708,6 @@ function createFlueProjection(options = {}) {
|
|
|
591
708
|
[GEN_AI.toolName]: toolName,
|
|
592
709
|
[GEN_AI.toolCallId]: toolCallId,
|
|
593
710
|
...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
|
|
594
|
-
// The semconv `gen_ai.tool.type` domain is the transport-level one
|
|
595
|
-
// (`function` / `extension` / `datastore`); a sub-agent delegation is
|
|
596
|
-
// not a plain function call, so it is left unclaimed there while
|
|
597
|
-
// `runtype.tool.type` says what it actually is.
|
|
598
711
|
...isDelegation ? {} : { [GEN_AI.toolType]: "function" },
|
|
599
712
|
...toolType ? { [RUNTYPE.toolType]: toolType } : {},
|
|
600
713
|
...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
|
|
@@ -653,10 +766,28 @@ function createFlueProjection(options = {}) {
|
|
|
653
766
|
} : {},
|
|
654
767
|
...stopReason ? { [RUNTYPE.stopReason]: stopReason } : {},
|
|
655
768
|
...envelope.toolsReported ? { [RUNTYPE.toolsReported]: true } : {},
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
769
|
+
...envelope.maxIteration !== null ? { [RUNTYPE.iteration]: envelope.maxIteration } : {},
|
|
770
|
+
...envelopeContentAttributes(envelope)
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function captureInput(envelope, request) {
|
|
774
|
+
const input = request.input;
|
|
775
|
+
if (content.inputMessages) {
|
|
776
|
+
const messages = projectFlueMessages(input?.messages);
|
|
777
|
+
if (messages.length > 0) envelope.inputMessages = messages;
|
|
778
|
+
}
|
|
779
|
+
if (content.systemInstructions && typeof input?.systemPrompt === "string") {
|
|
780
|
+
envelope.systemInstructions = input.systemPrompt;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
function envelopeContentAttributes(envelope) {
|
|
784
|
+
const input = envelope.inputMessages ? encodeMessagesValue(envelope.inputMessages, content, "input_messages") : void 0;
|
|
785
|
+
const system = envelope.systemInstructions ? encodeSystemInstructionsValue(envelope.systemInstructions, content) : void 0;
|
|
786
|
+
const output = envelope.outputMessage ? encodeMessagesValue([envelope.outputMessage], content, "output_messages") : void 0;
|
|
787
|
+
return {
|
|
788
|
+
...input !== void 0 ? { [GEN_AI_CONTENT.inputMessages]: input } : {},
|
|
789
|
+
...system !== void 0 ? { [GEN_AI_CONTENT.systemInstructions]: system } : {},
|
|
790
|
+
...output !== void 0 ? { [GEN_AI_CONTENT.outputMessages]: output } : {}
|
|
660
791
|
};
|
|
661
792
|
}
|
|
662
793
|
function resolveEnvelopeRef(event) {
|
|
@@ -776,9 +907,7 @@ function positionAttributes(position) {
|
|
|
776
907
|
return {
|
|
777
908
|
...position.turnId ? { [RUNTYPE.turnId]: position.turnId } : {},
|
|
778
909
|
[RUNTYPE.turnIndex]: position.index,
|
|
779
|
-
//
|
|
780
|
-
// turns would otherwise inflate the run's iteration count, which ingest
|
|
781
|
-
// computes as the highest position across every span in the trace.
|
|
910
|
+
// WHY(packages/flue-otel/src/projection.ts): Do not stamp delegated turns with root iteration positions; they would inflate the run iteration count.
|
|
782
911
|
...position.atEnvelopeScope ? { [RUNTYPE.iteration]: position.index } : {}
|
|
783
912
|
};
|
|
784
913
|
}
|
|
@@ -804,9 +933,6 @@ function closeIntent(ref, event, isError, errorInfo) {
|
|
|
804
933
|
...isError ? {
|
|
805
934
|
error: {
|
|
806
935
|
type: errorInfo?.type ?? "_OTHER",
|
|
807
|
-
// The class NAME only. `errorInfo.message` and `.stack` are content
|
|
808
|
-
// and this release emits none: a provider message routinely quotes
|
|
809
|
-
// the prompt back, and a stack ships filesystem layout.
|
|
810
936
|
...errorInfo?.name ? { exceptionType: errorInfo.name } : {}
|
|
811
937
|
}
|
|
812
938
|
} : {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/flue-otel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "OpenTelemetry instrumentation for Flue agents that emits GenAI semconv spans plus Runtype's runtype.* extension vocabulary, so a Flue run lands in Runtype at full fidelity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -28,14 +28,15 @@
|
|
|
28
28
|
"@opentelemetry/api": "^1.9.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
+
"@flue/runtime": "^2.0.3",
|
|
31
32
|
"@opentelemetry/api": "^1.9.0",
|
|
32
33
|
"@opentelemetry/context-async-hooks": "^2.1.0",
|
|
33
34
|
"@opentelemetry/sdk-trace-base": "^2.1.0",
|
|
35
|
+
"@runtypelabs/execution-ingest": "0.6.25",
|
|
36
|
+
"@runtypelabs/shared": "3.41.0",
|
|
34
37
|
"tsup": "^8.0.2",
|
|
35
38
|
"typescript": "^6.0.3",
|
|
36
|
-
"vitest": "^4.1.0"
|
|
37
|
-
"@runtypelabs/execution-ingest": "0.6.12",
|
|
38
|
-
"@runtypelabs/shared": "3.32.0"
|
|
39
|
+
"vitest": "^4.1.0"
|
|
39
40
|
},
|
|
40
41
|
"publishConfig": {
|
|
41
42
|
"access": "public"
|
|
@@ -68,7 +69,7 @@
|
|
|
68
69
|
"build": "tsup",
|
|
69
70
|
"dev": "tsup --watch",
|
|
70
71
|
"clean": "rm -rf dist",
|
|
71
|
-
"typecheck": "tsgo --noEmit",
|
|
72
|
+
"typecheck": "tsgo --noEmit && tsgo --noEmit -p tsconfig.tests.json",
|
|
72
73
|
"test": "vitest run",
|
|
73
74
|
"test:watch": "vitest watch",
|
|
74
75
|
"lint": "eslint . --max-warnings 0"
|