@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/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
- function resolveContentPolicy(options) {
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 === true,
53
- toolResults: options?.toolResults === true,
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
- let redacted = value;
61
- if (policy.redact) {
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.3.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
- // Deliberately NOT `invoke_agent`: see this module's header. A second
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
- // The highest loop position reached. Ingest records `max + 1` as the run's
657
- // iteration count, so the envelope alone reports it correctly even when
658
- // the batch carrying the model turns flushed earlier.
659
- ...envelope.maxIteration !== null ? { [RUNTYPE.iteration]: envelope.maxIteration } : {}
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
- // Only the root agent's loop has an ITERATION. A delegated sub-agent's
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.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"