@runtypelabs/flue-otel 0.4.0 → 0.5.1

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,49 +42,193 @@ 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 = /^<([A-Za-z_][\w.-]*) type="[^"]*"[^>]*>\n[\s\S]*\n<\/\1>$/;
49
+ var COMPACTION_SUMMARY_PREFIX = '<compaction type="context_summary"';
50
+ var HTML_TYPED_TAGS = /* @__PURE__ */ new Set([
51
+ "a",
52
+ "button",
53
+ "embed",
54
+ "input",
55
+ "link",
56
+ "menu",
57
+ "object",
58
+ "ol",
59
+ "script",
60
+ "source",
61
+ "style",
62
+ "ul"
63
+ ]);
64
+ function projectFlueMessages(value) {
65
+ if (!Array.isArray(value)) return [];
66
+ const out = [];
67
+ for (const entry of value) {
68
+ const message = projectFlueMessage(entry);
69
+ if (message) out.push(message);
70
+ }
71
+ return out;
72
+ }
73
+ function projectFlueMessage(value) {
74
+ if (!isRecord(value)) return void 0;
75
+ const role = value.role;
76
+ if (typeof role !== "string" || !FLUE_ROLES.has(role)) return void 0;
77
+ const text = flueMessageText(value.content);
78
+ if (text === "") return void 0;
79
+ if (role === "user" && isFlueSignal(value.content, text)) {
80
+ return { role: "system", parts: [{ type: "text", content: text }] };
81
+ }
82
+ return { role, parts: [{ type: "text", content: text }] };
83
+ }
84
+ function isFlueSignal(content, text) {
85
+ if (!isSingleText(content)) return false;
86
+ const signal = SIGNAL_MESSAGE.exec(text);
87
+ return signal !== null && !HTML_TYPED_TAGS.has(signal[1].toLowerCase());
88
+ }
89
+ function isSingleText(content) {
90
+ if (typeof content === "string") return true;
91
+ return Array.isArray(content) && content.length === 1;
92
+ }
93
+ function flueMessageText(content) {
94
+ return contentMessageText({ content });
95
+ }
96
+ function normalizeContentMessages(value) {
97
+ if (!Array.isArray(value)) return [];
98
+ const out = [];
99
+ for (const entry of value) {
100
+ if (!isRecord(entry)) continue;
101
+ const role = entry.role;
102
+ if (typeof role !== "string" || !ROLES.has(role)) continue;
103
+ const text = contentMessageText(entry);
104
+ if (text === "") continue;
105
+ out.push({ role, parts: [{ type: "text", content: text }] });
106
+ }
107
+ return out;
108
+ }
109
+ function contentTextOf(value) {
110
+ if (typeof value === "string") return value;
111
+ return Array.isArray(value) ? contentMessageText({ parts: value }) : "";
112
+ }
113
+ function contentMessageText(message) {
114
+ if (typeof message.content === "string") return message.content;
115
+ const parts = Array.isArray(message.parts) ? message.parts : Array.isArray(message.content) ? message.content : [];
116
+ const texts = [];
117
+ for (const part of parts) {
118
+ if (!isRecord(part) || part.type !== "text") continue;
119
+ const text = typeof part.content === "string" && part.content.length > 0 ? part.content : typeof part.text === "string" && part.text.length > 0 ? part.text : "";
120
+ if (text !== "") texts.push(text);
121
+ }
122
+ return texts.join("\n");
123
+ }
124
+ function isRecord(value) {
125
+ return value !== null && typeof value === "object" && !Array.isArray(value);
126
+ }
127
+
45
128
  // src/content.ts
46
129
  var DEFAULT_CONTENT_MAX_CHARS = 65536;
47
130
  var INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
48
- function resolveContentPolicy(options) {
131
+ var INGEST_CONTENT_SPAN_CEILING = 393216;
132
+ function resolveContentPolicy(setting) {
133
+ const options = setting === false ? void 0 : setting;
134
+ const on = (value) => setting !== false && value !== false;
49
135
  const requested = options?.maxChars;
50
136
  const maxChars = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(Math.floor(requested), INGEST_CONTENT_ATTRIBUTE_CEILING) : DEFAULT_CONTENT_MAX_CHARS;
51
137
  return {
52
- toolArguments: options?.toolArguments === true,
53
- toolResults: options?.toolResults === true,
138
+ toolArguments: on(options?.toolArguments),
139
+ toolResults: on(options?.toolResults),
140
+ inputMessages: on(options?.inputMessages),
141
+ outputMessages: on(options?.outputMessages),
142
+ systemInstructions: on(options?.systemInstructions),
54
143
  maxChars,
144
+ envelopeMaxChars: Math.min(maxChars, Math.floor(INGEST_CONTENT_SPAN_CEILING / 3)),
145
+ toolMaxChars: Math.min(maxChars, Math.floor(INGEST_CONTENT_SPAN_CEILING / 2)),
55
146
  redact: options?.redact
56
147
  };
57
148
  }
58
149
  function encodeContentValue(value, policy, context3) {
59
150
  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
- }
69
- if (context3.kind === "arguments") return encodeArguments(redacted, policy.maxChars);
151
+ const redacted = applyRedact(value, policy, context3);
152
+ if (redacted === void 0) return void 0;
153
+ if (context3.kind === "arguments") return encodeArguments(redacted, policy.toolMaxChars);
70
154
  const encoded = stringify(redacted);
71
155
  if (encoded === void 0) return void 0;
72
- return truncate(encoded, policy.maxChars);
156
+ return truncate(encoded, policy.toolMaxChars);
157
+ }
158
+ function encodeMessagesValue(messages, policy, kind) {
159
+ if (messages.length === 0) return void 0;
160
+ const redacted = applyRedact(messages, policy, { kind });
161
+ if (redacted === void 0) return void 0;
162
+ const all = normalizeContentMessages(redacted);
163
+ if (all.length === 0) return void 0;
164
+ const maxChars = policy.envelopeMaxChars;
165
+ const perMessage = all.length > 1 ? Math.floor((maxChars - 3) / 2) : maxChars - 2;
166
+ const encoded = [];
167
+ for (const message of all) {
168
+ const one = JSON.stringify(message);
169
+ encoded.push(one.length <= perMessage ? one : encodeTruncatedMessage(message, perMessage) ?? "");
170
+ }
171
+ let start = all.length;
172
+ let total = 2;
173
+ while (start > 0) {
174
+ const length = encoded[start - 1].length + (start < all.length ? 1 : 0);
175
+ if (total + length > maxChars) break;
176
+ total += length;
177
+ start -= 1;
178
+ }
179
+ const kept = encoded.slice(start).filter((one) => one !== "");
180
+ if (kept.length === 0) return void 0;
181
+ return "[" + kept.join(",") + "]";
182
+ }
183
+ function encodeSystemInstructionsValue(value, policy) {
184
+ if (value.length === 0) return void 0;
185
+ const redacted = applyRedact(value, policy, { kind: "system_instructions" });
186
+ if (redacted === void 0) return void 0;
187
+ const text = contentTextOf(redacted);
188
+ if (text.length === 0) return void 0;
189
+ return encodeTextParts(text, policy.envelopeMaxChars);
190
+ }
191
+ function encodeTruncatedMessage(message, maxChars) {
192
+ const text = message.parts.map((part) => part.content).join("\n");
193
+ return fitJson(
194
+ (cut) => JSON.stringify({ role: message.role, parts: [{ type: "text", content: cut }] }),
195
+ text,
196
+ maxChars
197
+ );
198
+ }
199
+ function encodeTextParts(text, maxChars) {
200
+ return fitJson((cut) => JSON.stringify([{ type: "text", content: cut }]), text, maxChars);
201
+ }
202
+ function fitJson(wrap, text, maxChars) {
203
+ let encoded = wrap(text);
204
+ if (encoded.length <= maxChars) return encoded;
205
+ const shell = wrap("").length;
206
+ let budget = maxChars - shell;
207
+ for (let round = 0; round < 8; round += 1) {
208
+ if (budget <= truncationMarker(text.length).length) return void 0;
209
+ const cut = truncate(text, budget);
210
+ encoded = wrap(cut);
211
+ if (encoded.length <= maxChars) return encoded;
212
+ const rescaled = Math.floor(budget * (maxChars - shell) / (encoded.length - shell));
213
+ budget = Math.min(budget - 1, rescaled);
214
+ }
215
+ return void 0;
216
+ }
217
+ function applyRedact(value, policy, context3) {
218
+ if (!policy.redact) return value;
219
+ try {
220
+ return policy.redact(value, context3);
221
+ } catch {
222
+ return void 0;
223
+ }
73
224
  }
74
225
  function encodeArguments(value, maxChars) {
75
226
  const shaped = isPlainObject(value) ? value : { value };
76
227
  const encoded = stringify(shaped);
77
228
  if (encoded === void 0) return void 0;
78
229
  if (encoded.length <= maxChars) return encoded;
79
- let budget = maxChars - TRUNCATED_WRAPPER_OVERHEAD;
80
- for (let round = 0; round < 8 && budget > 0; round += 1) {
81
- const wrapped = JSON.stringify({ truncated: truncate(encoded, budget) });
82
- if (wrapped.length <= maxChars) return wrapped;
83
- budget -= wrapped.length - maxChars;
84
- }
85
- return JSON.stringify({ truncated: truncate(encoded, Math.max(0, budget)) });
230
+ return fitJson((cut) => JSON.stringify({ truncated: cut }), encoded, maxChars);
86
231
  }
87
- var TRUNCATED_WRAPPER_OVERHEAD = 16;
88
232
  function isPlainObject(value) {
89
233
  return value !== null && typeof value === "object" && !Array.isArray(value);
90
234
  }
@@ -118,7 +262,7 @@ function isHighSurrogate(code) {
118
262
  }
119
263
 
120
264
  // package.json
121
- var version = "0.4.0";
265
+ var version = "0.5.1";
122
266
 
123
267
  // src/semconv.ts
124
268
  var GEN_AI = {
@@ -147,6 +291,9 @@ var GEN_AI = {
147
291
  serverPort: "server.port"
148
292
  };
149
293
  var GEN_AI_CONTENT = {
294
+ inputMessages: "gen_ai.input.messages",
295
+ outputMessages: "gen_ai.output.messages",
296
+ systemInstructions: "gen_ai.system_instructions",
150
297
  toolCallArguments: "gen_ai.tool.call.arguments",
151
298
  toolCallResult: "gen_ai.tool.call.result"
152
299
  };
@@ -300,9 +447,6 @@ function createFlueProjection(options = {}) {
300
447
  ...isEnvelope ? { [GEN_AI.operationName]: GEN_AI_OPERATION_INVOKE_AGENT } : {},
301
448
  ...isEnvelope && event.agentName ? { [GEN_AI.agentName]: event.agentName } : {},
302
449
  ...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
450
  ...isEnvelope && event.submissionId ? { [RUNTYPE.executionId]: event.submissionId } : {},
307
451
  ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {},
308
452
  // WHY(README.md): the resource placement is the customer's to wire and is routinely skipped.
@@ -366,9 +510,7 @@ function createFlueProjection(options = {}) {
366
510
  {
367
511
  kind: "open",
368
512
  ref,
369
- // Deliberately NOT `invoke_agent`: see this module's header. A second
370
- // agent-invocation span in the trace is a coin flip over which one
371
- // becomes the run's envelope.
513
+ // WHY(packages/flue-otel/src/projection.ts): Use a task span, not invoke_agent; one trace is persisted as one execution.
372
514
  name: agent ? `flue.task ${agent}` : "flue.task",
373
515
  spanKind: "internal",
374
516
  ...parentRef ? { parentRef } : {},
@@ -408,8 +550,6 @@ function createFlueProjection(options = {}) {
408
550
  ref,
409
551
  name: "flue.compaction",
410
552
  spanKind: "internal",
411
- // `session.compact()` is callable by the host outside any turn, and a
412
- // `compact` operation is not intercepted, so this can be parentless.
413
553
  requiresParent: true,
414
554
  ...parentRef ? { parentRef } : {},
415
555
  ...event.timestamp ? { startTime: event.timestamp } : {},
@@ -423,6 +563,11 @@ function createFlueProjection(options = {}) {
423
563
  function onCompactionEnd(event) {
424
564
  const ref = compactionKey(event);
425
565
  if (!open.has(ref)) return [];
566
+ if (!event.taskId) {
567
+ const envelope = resolveEnvelopeRef(event);
568
+ const state = envelope ? envelopes.get(envelope) : void 0;
569
+ if (state) state.compacted = true;
570
+ }
426
571
  const intents = sweepDescendants(ref, event.timestamp);
427
572
  intents.push(
428
573
  closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
@@ -474,6 +619,7 @@ function createFlueProjection(options = {}) {
474
619
  if (!envelope.modelPinned && purpose === "agent")
475
620
  envelope.requestModel = request.requestedModel;
476
621
  if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
622
+ if (position?.atEnvelopeScope) captureInput(envelope, request);
477
623
  }
478
624
  return [
479
625
  {
@@ -519,20 +665,16 @@ function createFlueProjection(options = {}) {
519
665
  if (purpose === "agent" && response.finishReason) {
520
666
  envelope.lastFinishReason = response.finishReason;
521
667
  }
668
+ if (content.outputMessages && purpose === "agent" && !event.taskId && !isError) {
669
+ const output = projectFlueMessage(response.output);
670
+ if (output?.role === "assistant") envelope.outputMessage = output;
671
+ }
522
672
  }
523
673
  const attributes = {
524
674
  ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
525
675
  ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
526
676
  ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
527
677
  ...usageAttributes(response.usage),
528
- // Provider-diagnostic pointers, emitted only when Flue actually provides
529
- // them. Both are optional and provider-dependent (Workers AI attaches
530
- // both today); an absent value costs one column, a synthesized one would
531
- // render as a measurement. `gatewayLogId` is a pointer to the content
532
- // without shipping the content — a customer can click through to that
533
- // exact request in their own AI Gateway dashboard. `providerFinishReason`
534
- // is the provider's exact finish value before normalization, which our
535
- // `GEN_AI.finishReasons` above deliberately hides.
536
678
  ...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
537
679
  ...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
538
680
  };
@@ -563,8 +705,6 @@ function createFlueProjection(options = {}) {
563
705
  ref,
564
706
  name: "flue.operation shell",
565
707
  spanKind: "internal",
566
- // `session.shell()` is callable by the host outside any turn, and a
567
- // `shell` operation is not intercepted, so this can be parentless.
568
708
  requiresParent: true,
569
709
  ...parentRef ? { parentRef } : {},
570
710
  ...event.timestamp ? { startTime: event.timestamp } : {},
@@ -596,10 +736,6 @@ function createFlueProjection(options = {}) {
596
736
  [GEN_AI.toolName]: toolName,
597
737
  [GEN_AI.toolCallId]: toolCallId,
598
738
  ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
599
- // The semconv `gen_ai.tool.type` domain is the transport-level one
600
- // (`function` / `extension` / `datastore`); a sub-agent delegation is
601
- // not a plain function call, so it is left unclaimed there while
602
- // `runtype.tool.type` says what it actually is.
603
739
  ...isDelegation ? {} : { [GEN_AI.toolType]: "function" },
604
740
  ...toolType ? { [RUNTYPE.toolType]: toolType } : {},
605
741
  ...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
@@ -658,10 +794,43 @@ function createFlueProjection(options = {}) {
658
794
  } : {},
659
795
  ...stopReason ? { [RUNTYPE.stopReason]: stopReason } : {},
660
796
  ...envelope.toolsReported ? { [RUNTYPE.toolsReported]: true } : {},
661
- // The highest loop position reached. Ingest records `max + 1` as the run's
662
- // iteration count, so the envelope alone reports it correctly even when
663
- // the batch carrying the model turns flushed earlier.
664
- ...envelope.maxIteration !== null ? { [RUNTYPE.iteration]: envelope.maxIteration } : {}
797
+ ...envelope.maxIteration !== null ? { [RUNTYPE.iteration]: envelope.maxIteration } : {},
798
+ ...envelopeContentAttributes(envelope)
799
+ };
800
+ }
801
+ function captureInput(envelope, request) {
802
+ const input = request.input;
803
+ if (envelope.inputRaw && (envelope.compacted || isCompactedHistory(input?.messages))) return;
804
+ if (content.inputMessages && Array.isArray(input?.messages) && input.messages.length > 0) {
805
+ envelope.inputRaw = input.messages;
806
+ envelope.inputBaseCount ??= input.messages.length;
807
+ }
808
+ if (content.systemInstructions && typeof input?.systemPrompt === "string") {
809
+ envelope.systemInstructions = input.systemPrompt;
810
+ }
811
+ }
812
+ function isCompactedHistory(messages) {
813
+ if (!Array.isArray(messages) || messages.length === 0) return false;
814
+ const first = messages[0];
815
+ if (!first || first.role !== "user") return false;
816
+ return flueMessageText(first.content).startsWith(COMPACTION_SUMMARY_PREFIX);
817
+ }
818
+ function projectInput(envelope) {
819
+ const raw = envelope.inputRaw ?? [];
820
+ const base = envelope.inputBaseCount ?? raw.length;
821
+ return [
822
+ ...projectFlueMessages(raw.slice(0, base)),
823
+ ...projectFlueMessages(raw.slice(base)).filter((message) => message.role !== "assistant")
824
+ ];
825
+ }
826
+ function envelopeContentAttributes(envelope) {
827
+ const input = envelope.inputRaw ? encodeMessagesValue(projectInput(envelope), content, "input_messages") : void 0;
828
+ const system = envelope.systemInstructions ? encodeSystemInstructionsValue(envelope.systemInstructions, content) : void 0;
829
+ const output = envelope.outputMessage ? encodeMessagesValue([envelope.outputMessage], content, "output_messages") : void 0;
830
+ return {
831
+ ...input !== void 0 ? { [GEN_AI_CONTENT.inputMessages]: input } : {},
832
+ ...system !== void 0 ? { [GEN_AI_CONTENT.systemInstructions]: system } : {},
833
+ ...output !== void 0 ? { [GEN_AI_CONTENT.outputMessages]: output } : {}
665
834
  };
666
835
  }
667
836
  function resolveEnvelopeRef(event) {
@@ -781,9 +950,7 @@ function positionAttributes(position) {
781
950
  return {
782
951
  ...position.turnId ? { [RUNTYPE.turnId]: position.turnId } : {},
783
952
  [RUNTYPE.turnIndex]: position.index,
784
- // Only the root agent's loop has an ITERATION. A delegated sub-agent's
785
- // turns would otherwise inflate the run's iteration count, which ingest
786
- // computes as the highest position across every span in the trace.
953
+ // WHY(packages/flue-otel/src/projection.ts): Do not stamp delegated turns with root iteration positions; they would inflate the run iteration count.
787
954
  ...position.atEnvelopeScope ? { [RUNTYPE.iteration]: position.index } : {}
788
955
  };
789
956
  }
@@ -809,9 +976,6 @@ function closeIntent(ref, event, isError, errorInfo) {
809
976
  ...isError ? {
810
977
  error: {
811
978
  type: errorInfo?.type ?? "_OTHER",
812
- // The class NAME only. `errorInfo.message` and `.stack` are content
813
- // and this release emits none: a provider message routinely quotes
814
- // the prompt back, and a stack ships filesystem layout.
815
979
  ...errorInfo?.name ? { exceptionType: errorInfo.name } : {}
816
980
  }
817
981
  } : {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/flue-otel",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
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,11 +28,12 @@
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",
34
- "@runtypelabs/execution-ingest": "0.6.18",
35
- "@runtypelabs/shared": "3.35.1",
35
+ "@runtypelabs/execution-ingest": "0.6.25",
36
+ "@runtypelabs/shared": "3.41.0",
36
37
  "tsup": "^8.0.2",
37
38
  "typescript": "^6.0.3",
38
39
  "vitest": "^4.1.0"
@@ -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"
package/CHANGELOG.md DELETED
@@ -1,105 +0,0 @@
1
- # @runtypelabs/flue-otel
2
-
3
- ## 0.4.0
4
-
5
- ### Minor Changes
6
-
7
- - 21f5bbb: Make the producing instrumentation's version detectable server-side. `@runtypelabs/flue-otel` now stamps `runtype.adapter.name` / `runtype.adapter.version` on each run's `invoke_agent` span, so a trace identifies its producer without the customer wiring `runtypeFlueResourceAttributes` into their SDK `Resource`. OTLP trace ingest resolves the pair (resource level first, envelope span second) and records it on the run's log row as `otlpAdapterName` / `otlpAdapterVersion`, regardless of logging policy.
8
-
9
- ## 0.3.0
10
-
11
- ### Minor Changes
12
-
13
- - 01df868: Add an opt-in `content` option that emits tool arguments (`gen_ai.tool.call.arguments`, at span open) and tool results (`gen_ai.tool.call.result`, before span end) on `execute_tool` spans, so a Flue run's tool cards in Runtype show their input and output. Off by default; per-kind switches, a per-value `maxChars` ceiling with an explicit truncation marker (clamped to the ingest per-attribute ceiling), and a `redact` hook. A failed tool's result, the host's shell call, and prompts/completions are still never sent.
14
-
15
- ## 0.2.2
16
-
17
- ### Patch Changes
18
-
19
- - 5f00348: Carry Workers AI `gatewayLogId` and `providerFinishReason` onto the chat span
20
-
21
- Flue's `turn` event carries two allowlisted raw-provider fields that
22
- `@runtypelabs/flue-otel` previously dropped. Both are now emitted on the `chat`
23
- span as `runtype.*` extension attributes, gated on presence so an absent value
24
- costs nothing:
25
- - `runtype.gateway.log_id` — the response's own gateway log id (e.g.
26
- Cloudflare AI Gateway's `cf-aig-log-id`), a pointer to the request in the
27
- customer's own AI Gateway dashboard without shipping the content.
28
- - `runtype.provider.finish_reason` — the provider's exact finish value before
29
- normalization (e.g. Workers AI's `tool_calls` behind the normalized
30
- `toolUse`), for diagnosis when a run stops unexpectedly.
31
-
32
- Both attribute names are added to the shared `RUNTYPE_ATTRIBUTES` vocabulary
33
- (`packages/shared/src/otlp-runtype-semconv.ts`) so the emit side and the ingest
34
- side stay in step. They are additive diagnostics and do not count toward the
35
- achieved fidelity tier.
36
-
37
- ## 0.2.1
38
-
39
- ### Patch Changes
40
-
41
- - 625c455: Rewrite the README from first principles. It now leads with install, quick
42
- start and the no-SDK recipe, then configuration, attribution, shutdown, and
43
- what the package emits, instead of narrating how the package differs from
44
- earlier instrumentations.
45
-
46
- ## 0.2.0
47
-
48
- ### Minor Changes
49
-
50
- - 2439b84: New package: `@runtypelabs/flue-otel`, an OpenTelemetry instrumentation for Flue
51
- agents that reports runs to Runtype at `t2-runtype` fidelity.
52
-
53
- Stock `@flue/opentelemetry` already exports GenAI semconv spans, and the ingest
54
- normalizer shipped in #7336 lifts those to t2 by reading Flue's own `flue.*`
55
- identifiers. This package is the client-side complement, for the facts a
56
- server-side normalizer structurally cannot reach:
57
- - **Absolute loop position.** `runtype.turn.index` / `runtype.iteration` are
58
- deliberately NOT derived at ingest, because Flue puts no absolute ordinal on
59
- the wire and ranking opaque turn ids ranks whatever the export batch happened
60
- to carry — a five-turn run whose closing batch holds two turns would be
61
- recorded as two iterations. In process the count is exact.
62
- - **A run-level usage roll-up on the envelope.** Summed from `turn` leaves, so a
63
- split export cannot lose the total: the batch carrying the terminal carries
64
- the complete sum by construction.
65
- - **Per-agent attribution for N agents in one process**, via the span-level
66
- `runtype.agent.id` placement, keyed by Flue agent name.
67
- - **One `invoke_agent` span per trace.** Stock opens a nested one for every
68
- `task` delegation, which is a coin flip over which invocation bounds the run
69
- (ingest's envelope selection is first-match over exporter-controlled array
70
- order). Here a delegation is reported as an `execute_tool` span typed
71
- `subagent`, and the sub-agent's work gets a non-agent `flue.task` span.
72
-
73
- Works on both Flue lines (`>=1.0.0-beta.9` and 2.x) from one entry point, via a
74
- capability probe rather than version comparison. Depends on `@opentelemetry/api`
75
- only — no SDK, no provider, no exporter, no flush; the application owns those.
76
-
77
- Emits no content in this release: no prompts, completions, tool arguments or
78
- results, error messages, or stack traces.
79
-
80
- `@runtypelabs/shared` exports the wire stop-reason tuple as `WIRE_STOP_REASONS`
81
- (the zod enum is now derived from it, so the two cannot disagree). The ingest
82
- reader writes `runtype.stop_reason` to `agent_executions.stop_reason` verbatim
83
- with no enum validation, so an external producer has to spell those six strings
84
- itself; exporting the tuple is what lets the producer's contract test pin its
85
- copy and turn a rename into a red build instead of a silently wrong column.
86
-
87
- ### Patch Changes
88
-
89
- - 3b4f69c: License `@runtypelabs/flue-otel` under MIT rather than Apache-2.0, and ship the
90
- license text in the published tarball.
91
-
92
- The package declared `Apache-2.0` in `package.json` and in its README while
93
- carrying no license file at all, so the published artifact asserted a grant
94
- whose terms it never included. `LICENSE` is now added to `files`, matching how
95
- every other published package here (`cli`, `python-sdk`, `ruby-sdk`,
96
- `java-sdk`, `php-sdk`) ships its license.
97
-
98
- - 3b4f69c: Normalize package authorship and copyright metadata.
99
-
100
- The `author` field reads `Runtype` rather than `Runtype Labs` across every
101
- workspace package that set it, and the copyright holder in each license file is
102
- now `Runtype, Inc` — replacing `Travrse Labs, Inc` in the CLI and the four Fern
103
- SDKs, and the `Runtype Labs` that the new `flue-otel` license was written with.
104
-
105
- Metadata only: no source, dependency, or behavior change.