@runtypelabs/flue-otel 0.4.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.4.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,9 +424,6 @@ 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
428
  ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {},
308
429
  // WHY(README.md): the resource placement is the customer's to wire and is routinely skipped.
@@ -366,9 +487,7 @@ function createFlueProjection(options = {}) {
366
487
  {
367
488
  kind: "open",
368
489
  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.
490
+ // WHY(packages/flue-otel/src/projection.ts): Use a task span, not invoke_agent; one trace is persisted as one execution.
372
491
  name: agent ? `flue.task ${agent}` : "flue.task",
373
492
  spanKind: "internal",
374
493
  ...parentRef ? { parentRef } : {},
@@ -408,8 +527,6 @@ function createFlueProjection(options = {}) {
408
527
  ref,
409
528
  name: "flue.compaction",
410
529
  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
530
  requiresParent: true,
414
531
  ...parentRef ? { parentRef } : {},
415
532
  ...event.timestamp ? { startTime: event.timestamp } : {},
@@ -474,6 +591,7 @@ function createFlueProjection(options = {}) {
474
591
  if (!envelope.modelPinned && purpose === "agent")
475
592
  envelope.requestModel = request.requestedModel;
476
593
  if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
594
+ if (position?.atEnvelopeScope) captureInput(envelope, request);
477
595
  }
478
596
  return [
479
597
  {
@@ -519,20 +637,16 @@ function createFlueProjection(options = {}) {
519
637
  if (purpose === "agent" && response.finishReason) {
520
638
  envelope.lastFinishReason = response.finishReason;
521
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
+ }
522
644
  }
523
645
  const attributes = {
524
646
  ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
525
647
  ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
526
648
  ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
527
649
  ...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
650
  ...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
537
651
  ...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
538
652
  };
@@ -563,8 +677,6 @@ function createFlueProjection(options = {}) {
563
677
  ref,
564
678
  name: "flue.operation shell",
565
679
  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
680
  requiresParent: true,
569
681
  ...parentRef ? { parentRef } : {},
570
682
  ...event.timestamp ? { startTime: event.timestamp } : {},
@@ -596,10 +708,6 @@ function createFlueProjection(options = {}) {
596
708
  [GEN_AI.toolName]: toolName,
597
709
  [GEN_AI.toolCallId]: toolCallId,
598
710
  ...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
711
  ...isDelegation ? {} : { [GEN_AI.toolType]: "function" },
604
712
  ...toolType ? { [RUNTYPE.toolType]: toolType } : {},
605
713
  ...event.origin ? { [FLUE.toolOrigin]: event.origin } : {},
@@ -658,10 +766,28 @@ function createFlueProjection(options = {}) {
658
766
  } : {},
659
767
  ...stopReason ? { [RUNTYPE.stopReason]: stopReason } : {},
660
768
  ...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 } : {}
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 } : {}
665
791
  };
666
792
  }
667
793
  function resolveEnvelopeRef(event) {
@@ -781,9 +907,7 @@ function positionAttributes(position) {
781
907
  return {
782
908
  ...position.turnId ? { [RUNTYPE.turnId]: position.turnId } : {},
783
909
  [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.
910
+ // WHY(packages/flue-otel/src/projection.ts): Do not stamp delegated turns with root iteration positions; they would inflate the run iteration count.
787
911
  ...position.atEnvelopeScope ? { [RUNTYPE.iteration]: position.index } : {}
788
912
  };
789
913
  }
@@ -809,9 +933,6 @@ function closeIntent(ref, event, isError, errorInfo) {
809
933
  ...isError ? {
810
934
  error: {
811
935
  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
936
  ...errorInfo?.name ? { exceptionType: errorInfo.name } : {}
816
937
  }
817
938
  } : {}
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.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,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.