@runtypelabs/flue-otel 0.2.2 → 0.4.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/CHANGELOG.md ADDED
@@ -0,0 +1,105 @@
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.
package/README.md CHANGED
@@ -10,8 +10,9 @@ token usage, cost, loop iterations, tool calls and stop reason.
10
10
  - Works with Flue `>=1.0.0-beta.9` and 2.x from one entry point.
11
11
  - Depends on `@opentelemetry/api` only. It brings no SDK, provider, exporter or
12
12
  sampler; it writes through whatever your application has registered.
13
- - Emits identifiers, structure and metrics only. No prompts, completions, tool
14
- arguments, tool results, error messages or stack traces reach the wire.
13
+ - Emits identifiers, structure and metrics by default. No prompts, completions,
14
+ tool arguments, tool results, error messages or stack traces reach the wire
15
+ unless you opt in; the one opt-in is [tool content](#tool-content).
15
16
 
16
17
  Full guide: [Instrumenting a Flue agent](https://docs.runtype.com/developer-guides/guides/flue-instrumentation).
17
18
 
@@ -84,10 +85,11 @@ process.on('beforeExit', () => void provider.shutdown())
84
85
 
85
86
  `createRuntypeFlueInstrumentation(options?)` accepts:
86
87
 
87
- | Option | Type | Purpose |
88
- | -------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
89
- | `agents` | `Record<string, string>` | Runtype agent id per Flue agent name, e.g. `{ triage: 'agent_01j...' }`. Stamped as `runtype.agent.id` on that agent's `invoke_agent` span. See [Attribution](#attribution). |
90
- | `tracer` | `Tracer` | Where spans are written. Defaults to `trace.getTracer('@runtypelabs/flue-otel')` on the global provider. Pass one to route Runtype spans through a separate provider. |
88
+ | Option | Type | Purpose |
89
+ | --------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
90
+ | `agents` | `Record<string, string>` | Runtype agent id per Flue agent name, e.g. `{ triage: 'agent_01j...' }`. Stamped as `runtype.agent.id` on that agent's `invoke_agent` span. See [Attribution](#attribution). |
91
+ | `tracer` | `Tracer` | Where spans are written. Defaults to `trace.getTracer('@runtypelabs/flue-otel')` on the global provider. Pass one to route Runtype spans through a separate provider. |
92
+ | `content` | `FlueContentOptions` | Opt in to tool arguments and results on `execute_tool` spans. Off by default. See [Tool content](#tool-content). |
91
93
 
92
94
  The returned object implements Flue's full `FlueInstrumentation` contract:
93
95
  `observe` (builds spans from Flue's observation stream), `interceptor` (makes
@@ -104,12 +106,19 @@ subscriber. Its key is exported as `RUNTYPE_FLUE_INSTRUMENTATION_KEY`.
104
106
 
105
107
  - `runtypeFlueResourceAttributes({ agentId? })` builds the `runtype.*` resource
106
108
  attributes (`schema.version`, `adapter.name`, `adapter.version`, and
107
- `agent.id` when given). Spread it into your SDK `Resource`.
108
- - `GEN_AI` and `RUNTYPE`: the attribute-name constants this package emits.
109
+ `agent.id` when given). Spread it into your SDK `Resource`. The adapter pair
110
+ is also stamped on each run's `invoke_agent` span, so Runtype can tell which
111
+ version produced a trace even when the resource is not wired; the resource
112
+ placement still wins where both are present.
113
+ - `GEN_AI` and `RUNTYPE`: the attribute-name constants this package emits by
114
+ default; `GEN_AI_CONTENT`: the two it emits under the `content` opt-in.
115
+ - `DEFAULT_CONTENT_MAX_CHARS` and `INGEST_CONTENT_ATTRIBUTE_CEILING`: the
116
+ content size defaults, see [Tool content](#tool-content).
109
117
  - `RUNTYPE_STOP_REASONS`, `RUNTYPE_TOOL_TYPES`, `RUNTYPE_SCHEMA_VERSION`,
110
118
  `ADAPTER_NAME`, `ADAPTER_VERSION`, plus the `RuntypeStopReason`,
111
119
  `RuntypeToolType`, `FlueInstrumentation`, `FlueProjectionOptions`,
112
- `SpanAttributes` and `SpanIntent` types.
120
+ `FlueContentOptions`, `FlueContentRedactContext`, `SpanAttributes` and
121
+ `SpanIntent` types.
113
122
 
114
123
  ## Attribution
115
124
 
@@ -179,7 +188,7 @@ backends.
179
188
  | -------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
180
189
  | `invoke_agent <agent>` | once per agent invocation | `gen_ai.agent.name`, `gen_ai.conversation.id`, the run's request/response model, summed token usage across every turn (`gen_ai.usage.*`), `runtype.stop_reason`, `runtype.tools.reported`, the highest loop iteration (`runtype.iteration`), `runtype.execution.id`, and `runtype.agent.id` when `agents` names the agent |
181
190
  | `chat <model>` | once per model turn | `gen_ai.provider.name`, request/response model, response id, finish reason, per-turn usage, request parameters (`max_tokens`, `temperature`, reasoning level, server address), `runtype.turn.id` / `runtype.turn.index` / `runtype.iteration`, and `runtype.provider.finish_reason` / `runtype.gateway.log_id` when the provider records them (Workers AI attaches both — the gateway log id is a pointer to that exact request in your AI Gateway dashboard) |
182
- | `execute_tool <tool>` | once per model-requested tool call | `gen_ai.tool.name`, `gen_ai.tool.call.id`, the loop position it belongs to, `gen_ai.tool.type` (`function`) for every tool except a sub-agent delegation, and `runtype.tool.type` when the tool's class is known |
191
+ | `execute_tool <tool>` | once per model-requested tool call | `gen_ai.tool.name`, `gen_ai.tool.call.id`, the loop position it belongs to, `gen_ai.tool.type` (`function`) for every tool except a sub-agent delegation, and `runtype.tool.type` when the tool's class is known, and `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result` only under the [`content`](#tool-content) opt-in |
183
192
  | `flue.task <agent>`, `flue.compaction`, `flue.operation shell` | delegation, compaction, host shell call | correlation ids only; framework structure, not agent invocations |
184
193
 
185
194
  Every span also carries Flue's own `flue.*` correlation attributes
@@ -220,6 +229,73 @@ Rules the projection follows:
220
229
 
221
230
  An absent attribute costs one column. A wrong one renders as a measurement.
222
231
 
232
+ ## Tool content
233
+
234
+ By default an `execute_tool` span says which tool ran and how long it took, and
235
+ Runtype's trace view shows its card as "No tool content in this trace". To see
236
+ arguments and results on that card, opt in:
237
+
238
+ ```ts
239
+ createRuntypeFlueInstrumentation({
240
+ content: {
241
+ toolArguments: true, // gen_ai.tool.call.arguments, set when the span opens
242
+ toolResults: true, // gen_ai.tool.call.result, set just before the span ends
243
+ maxChars: 65_536, // per value, marker included; this is the default
244
+ redact: (value, { kind, toolName }) => value, // optional; return undefined to drop
245
+ },
246
+ })
247
+ ```
248
+
249
+ Each switch is independent and defaults to off, so `{}` changes nothing. The
250
+ values come from Flue's stable `tool_start.args` and `tool.result` payloads
251
+ (2.x's `effectiveResult`, what the model was actually shown, when present).
252
+
253
+ The attribute names are the GenAI semconv `gen_ai.tool.call.arguments` and
254
+ `gen_ai.tool.call.result` (exported as `GEN_AI_CONTENT`), which Runtype reads
255
+ onto the tool card's input and output. The two are encoded differently because
256
+ Runtype projects them differently:
257
+
258
+ - **Arguments are always a JSON object**, since that is the only shape the
259
+ reader projects. A non-object value (a bare string, an array) is carried as
260
+ `{ "value": ... }` rather than dropped. Over `maxChars`, the object is not
261
+ cut in place (a mid-document cut is not JSON and would be discarded); it is
262
+ replaced by `{ "truncated": "<head of the JSON>…[truncated N chars]" }`.
263
+ - **A result is text**: a string as-is, anything else as JSON. Over `maxChars`
264
+ it is cut and ends in `…[truncated N chars]`, so a cut value never looks
265
+ complete.
266
+
267
+ Ceilings worth knowing, because they decide what Runtype keeps:
268
+
269
+ - `maxChars` defaults to `DEFAULT_CONTENT_MAX_CHARS` (64 KiB) and is clamped to
270
+ `INGEST_CONTENT_ATTRIBUTE_CEILING` (256 KiB). Runtype drops a single content
271
+ attribute above that ceiling **whole** rather than cutting it, which is why
272
+ the package cuts first.
273
+ - One span may carry at most 384 KiB of content across every attribute, and one
274
+ export request at most 2 MiB of content. Arguments plus result at the default
275
+ fit inside the per-span ceiling; raise `maxChars` with that in mind.
276
+ - Content makes spans large. A `BatchSpanProcessor` at its default 512 spans per
277
+ request can push an export past Runtype's 8 MiB request bound, which rejects
278
+ the whole batch (envelope and usage included, not only the content). With
279
+ both switches on, set `maxExportBatchSize` to something like 64.
280
+
281
+ What is still never sent, whatever you set:
282
+
283
+ - **A failed tool's result.** On both Flue lines it is commonly the error
284
+ payload, and error messages stay off the wire. The span still closes with its
285
+ error type and exception class name.
286
+ - **The host's own shell call** (`session.shell()`), which is not a tool row.
287
+ - **Prompts and completions.** They live on `AgentMessage`, which Flue marks
288
+ unstable. This package will read them once Flue exposes a stable shape. One
289
+ edge to know: a delegation to a sub-agent is a tool call, and its
290
+ **arguments carry the prompt handed to that sub-agent**. If that matters for
291
+ your deployment, drop it with `redact` (return `undefined` when `toolName`
292
+ is the delegation tool).
293
+
294
+ `redact` runs on the raw value before encoding, so it can address fields by
295
+ name; it receives `{ kind: 'arguments' | 'result', toolName }`. Returning
296
+ `undefined` drops the attribute for that call. If it throws, the attribute is
297
+ dropped and the span is otherwise unaffected.
298
+
223
299
  ## Flue compatibility
224
300
 
225
301
  The instrumentation reads only Flue's stable observation surface: event type
package/dist/index.cjs CHANGED
@@ -22,7 +22,10 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  ADAPTER_NAME: () => ADAPTER_NAME,
24
24
  ADAPTER_VERSION: () => ADAPTER_VERSION,
25
+ DEFAULT_CONTENT_MAX_CHARS: () => DEFAULT_CONTENT_MAX_CHARS,
25
26
  GEN_AI: () => GEN_AI,
27
+ GEN_AI_CONTENT: () => GEN_AI_CONTENT,
28
+ INGEST_CONTENT_ATTRIBUTE_CEILING: () => INGEST_CONTENT_ATTRIBUTE_CEILING,
26
29
  RUNTYPE: () => RUNTYPE,
27
30
  RUNTYPE_FLUE_INSTRUMENTATION_KEY: () => RUNTYPE_FLUE_INSTRUMENTATION_KEY,
28
31
  RUNTYPE_SCHEMA_VERSION: () => RUNTYPE_SCHEMA_VERSION,
@@ -75,8 +78,83 @@ function mapSettlementOutcome(outcome) {
75
78
  return void 0;
76
79
  }
77
80
 
81
+ // src/content.ts
82
+ var DEFAULT_CONTENT_MAX_CHARS = 65536;
83
+ var INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
84
+ function resolveContentPolicy(options) {
85
+ const requested = options?.maxChars;
86
+ const maxChars = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(Math.floor(requested), INGEST_CONTENT_ATTRIBUTE_CEILING) : DEFAULT_CONTENT_MAX_CHARS;
87
+ return {
88
+ toolArguments: options?.toolArguments === true,
89
+ toolResults: options?.toolResults === true,
90
+ maxChars,
91
+ redact: options?.redact
92
+ };
93
+ }
94
+ function encodeContentValue(value, policy, context3) {
95
+ if (value === void 0) return void 0;
96
+ let redacted = value;
97
+ if (policy.redact) {
98
+ try {
99
+ redacted = policy.redact(value, context3);
100
+ } catch {
101
+ return void 0;
102
+ }
103
+ if (redacted === void 0) return void 0;
104
+ }
105
+ if (context3.kind === "arguments") return encodeArguments(redacted, policy.maxChars);
106
+ const encoded = stringify(redacted);
107
+ if (encoded === void 0) return void 0;
108
+ return truncate(encoded, policy.maxChars);
109
+ }
110
+ function encodeArguments(value, maxChars) {
111
+ const shaped = isPlainObject(value) ? value : { value };
112
+ const encoded = stringify(shaped);
113
+ if (encoded === void 0) return void 0;
114
+ if (encoded.length <= maxChars) return encoded;
115
+ let budget = maxChars - TRUNCATED_WRAPPER_OVERHEAD;
116
+ for (let round = 0; round < 8 && budget > 0; round += 1) {
117
+ const wrapped = JSON.stringify({ truncated: truncate(encoded, budget) });
118
+ if (wrapped.length <= maxChars) return wrapped;
119
+ budget -= wrapped.length - maxChars;
120
+ }
121
+ return JSON.stringify({ truncated: truncate(encoded, Math.max(0, budget)) });
122
+ }
123
+ var TRUNCATED_WRAPPER_OVERHEAD = 16;
124
+ function isPlainObject(value) {
125
+ return value !== null && typeof value === "object" && !Array.isArray(value);
126
+ }
127
+ function stringify(value) {
128
+ if (typeof value === "string") return value;
129
+ try {
130
+ const encoded = JSON.stringify(value);
131
+ return typeof encoded === "string" ? encoded : void 0;
132
+ } catch {
133
+ return void 0;
134
+ }
135
+ }
136
+ function truncate(value, maxChars) {
137
+ if (value.length <= maxChars) return value;
138
+ const upperBound = truncationMarker(value.length);
139
+ let keep = Math.max(0, maxChars - upperBound.length);
140
+ let marker = truncationMarker(value.length - keep);
141
+ keep = Math.max(0, maxChars - marker.length);
142
+ marker = truncationMarker(value.length - keep);
143
+ if (keep > 0 && isHighSurrogate(value.charCodeAt(keep - 1))) {
144
+ keep -= 1;
145
+ marker = truncationMarker(value.length - keep);
146
+ }
147
+ return value.slice(0, keep) + marker;
148
+ }
149
+ function truncationMarker(droppedChars) {
150
+ return `\u2026[truncated ${droppedChars} chars]`;
151
+ }
152
+ function isHighSurrogate(code) {
153
+ return code >= 55296 && code <= 56319;
154
+ }
155
+
78
156
  // package.json
79
- var version = "0.2.2";
157
+ var version = "0.4.0";
80
158
 
81
159
  // src/semconv.ts
82
160
  var GEN_AI = {
@@ -104,6 +182,10 @@ var GEN_AI = {
104
182
  serverAddress: "server.address",
105
183
  serverPort: "server.port"
106
184
  };
185
+ var GEN_AI_CONTENT = {
186
+ toolCallArguments: "gen_ai.tool.call.arguments",
187
+ toolCallResult: "gen_ai.tool.call.result"
188
+ };
107
189
  var GEN_AI_OPERATION_INVOKE_AGENT = "invoke_agent";
108
190
  var GEN_AI_OPERATION_CHAT = "chat";
109
191
  var GEN_AI_OPERATION_EXECUTE_TOOL = "execute_tool";
@@ -173,6 +255,7 @@ function runtypeFlueResourceAttributes(resource) {
173
255
  };
174
256
  }
175
257
  function createFlueProjection(options = {}) {
258
+ const content = resolveContentPolicy(options.content);
176
259
  const open = /* @__PURE__ */ new Map();
177
260
  const envelopes = /* @__PURE__ */ new Map();
178
261
  const envelopeByChain = /* @__PURE__ */ new Map();
@@ -257,7 +340,12 @@ function createFlueProjection(options = {}) {
257
340
  // any inbound id; it does not key the row, which stays derived from
258
341
  // the trace id.
259
342
  ...isEnvelope && event.submissionId ? { [RUNTYPE.executionId]: event.submissionId } : {},
260
- ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {}
343
+ ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {},
344
+ // WHY(README.md): the resource placement is the customer's to wire and is routinely skipped.
345
+ ...isEnvelope ? {
346
+ [RUNTYPE.adapterName]: ADAPTER_NAME,
347
+ [RUNTYPE.adapterVersion]: ADAPTER_VERSION
348
+ } : {}
261
349
  }
262
350
  }
263
351
  ];
@@ -343,7 +431,12 @@ function createFlueProjection(options = {}) {
343
431
  const ref = compactionKey(event);
344
432
  if (open.has(ref)) return [];
345
433
  const parentRef = resolveParentRef(event);
346
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
434
+ open.set(ref, {
435
+ ref,
436
+ ...parentRef ? { parentRef } : {},
437
+ chainKey: chainKey(event),
438
+ isTask: false
439
+ });
347
440
  const reason = readString(event, "reason");
348
441
  return [
349
442
  {
@@ -381,7 +474,12 @@ function createFlueProjection(options = {}) {
381
474
  const purpose = readString(event, "purpose") ?? "agent";
382
475
  const compactionRef = compactionKey(event);
383
476
  const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
384
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
477
+ open.set(ref, {
478
+ ref,
479
+ ...parentRef ? { parentRef } : {},
480
+ chainKey: chainKey(event),
481
+ isTask: false
482
+ });
385
483
  const envelopeRef = resolveEnvelopeRef(event);
386
484
  const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
387
485
  let position;
@@ -409,7 +507,8 @@ function createFlueProjection(options = {}) {
409
507
  if (position) turnPositions.set(ref, position);
410
508
  }
411
509
  if (envelope) {
412
- if (!envelope.modelPinned && purpose === "agent") envelope.requestModel = request.requestedModel;
510
+ if (!envelope.modelPinned && purpose === "agent")
511
+ envelope.requestModel = request.requestedModel;
413
512
  if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
414
513
  }
415
514
  return [
@@ -486,7 +585,12 @@ function createFlueProjection(options = {}) {
486
585
  const ref = toolKey(event);
487
586
  if (open.has(ref)) return [];
488
587
  const parentRef = resolveParentRef(event);
489
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
588
+ open.set(ref, {
589
+ ref,
590
+ ...parentRef ? { parentRef } : {},
591
+ chainKey: chainKey(event),
592
+ isTask: false
593
+ });
490
594
  const position = turnPositions.get(turnKey(event));
491
595
  if (isCallerShellCall(toolName, event)) {
492
596
  return [
@@ -510,6 +614,10 @@ function createFlueProjection(options = {}) {
510
614
  }
511
615
  const isDelegation = isTaskDelegationTool(toolName, event);
512
616
  const toolType = resolveToolType(toolName, event);
617
+ const args = content.toolArguments ? encodeContentValue(readUnknown(event, "args"), content, {
618
+ kind: "arguments",
619
+ toolName
620
+ }) : void 0;
513
621
  return [
514
622
  {
515
623
  kind: "open",
@@ -523,6 +631,7 @@ function createFlueProjection(options = {}) {
523
631
  [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
524
632
  [GEN_AI.toolName]: toolName,
525
633
  [GEN_AI.toolCallId]: toolCallId,
634
+ ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
526
635
  // The semconv `gen_ai.tool.type` domain is the transport-level one
527
636
  // (`function` / `extension` / `datastore`); a sub-agent delegation is
528
637
  // not a plain function call, so it is left unclaimed there while
@@ -541,9 +650,22 @@ function createFlueProjection(options = {}) {
541
650
  const ref = toolKey(event);
542
651
  if (!open.has(ref)) return [];
543
652
  const intents = sweepDescendants(ref, event.timestamp);
544
- intents.push(
545
- closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
546
- );
653
+ const isError = readBoolean(event, "isError") === true;
654
+ const toolName = readString(event, "toolName");
655
+ if (content.toolResults && !isError && toolName && !isCallerShellCall(toolName, event)) {
656
+ const result = encodeContentValue(readToolResult(event), content, {
657
+ kind: "result",
658
+ toolName
659
+ });
660
+ if (result !== void 0) {
661
+ intents.push({
662
+ kind: "update",
663
+ ref,
664
+ attributes: { [GEN_AI_CONTENT.toolCallResult]: result }
665
+ });
666
+ }
667
+ }
668
+ intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
547
669
  open.delete(ref);
548
670
  return intents;
549
671
  }
@@ -735,6 +857,13 @@ function readErrorInfo(event) {
735
857
  const detail = event.errorInfo;
736
858
  return detail !== null && typeof detail === "object" ? detail : void 0;
737
859
  }
860
+ function readUnknown(event, key) {
861
+ return event[key];
862
+ }
863
+ function readToolResult(event) {
864
+ const record = event;
865
+ return Object.prototype.hasOwnProperty.call(record, "effectiveResult") ? record.effectiveResult : record.result;
866
+ }
738
867
  function readString(event, key) {
739
868
  const value = event[key];
740
869
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -751,7 +880,12 @@ function positiveNumber(value) {
751
880
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
752
881
  }
753
882
  function chainKey(value) {
754
- return identityKey("chain", [value.instanceId, value.harness, value.conversationId, value.session]);
883
+ return identityKey("chain", [
884
+ value.instanceId,
885
+ value.harness,
886
+ value.conversationId,
887
+ value.session
888
+ ]);
755
889
  }
756
890
  function operationKey(value) {
757
891
  return identityKey("operation", [
@@ -940,7 +1074,10 @@ function readIdField(operation, key) {
940
1074
  0 && (module.exports = {
941
1075
  ADAPTER_NAME,
942
1076
  ADAPTER_VERSION,
1077
+ DEFAULT_CONTENT_MAX_CHARS,
943
1078
  GEN_AI,
1079
+ GEN_AI_CONTENT,
1080
+ INGEST_CONTENT_ATTRIBUTE_CEILING,
944
1081
  RUNTYPE,
945
1082
  RUNTYPE_FLUE_INSTRUMENTATION_KEY,
946
1083
  RUNTYPE_SCHEMA_VERSION,
package/dist/index.d.cts CHANGED
@@ -193,12 +193,14 @@ type FlueEventVariant = {
193
193
  type: 'tool_start';
194
194
  toolName: string;
195
195
  toolCallId: string;
196
+ args?: unknown;
196
197
  } | {
197
198
  type: 'tool';
198
199
  toolName: string;
199
200
  toolCallId: string;
200
201
  isError?: boolean;
201
202
  result?: unknown;
203
+ effectiveResult?: unknown;
202
204
  durationMs?: number;
203
205
  } | {
204
206
  type: 'submission_settled';
@@ -273,6 +275,117 @@ interface FlueInstrumentation {
273
275
  dispose(): void | Promise<void>;
274
276
  }
275
277
 
278
+ /**
279
+ * Opt-in tool content: `gen_ai.tool.call.arguments` and
280
+ * `gen_ai.tool.call.result` on `execute_tool` spans.
281
+ *
282
+ * PURE, like `projection.ts`: value in, attribute string out. The projection
283
+ * decides WHEN to call this (arguments when the tool span opens, the result
284
+ * just before it closes); this module decides WHAT reaches the wire and how
285
+ * much of it.
286
+ *
287
+ * ## Why opt-in, and why only tools
288
+ *
289
+ * The package's default contract is that no content leaves the process, and
290
+ * that stays the default: an instrumentation that lands inside a PHI-bearing
291
+ * process must not start shipping tool payloads because of a version bump.
292
+ * The `content` option is the explicit decision, and it is per kind, so a
293
+ * deployment can export the arguments it controls without the results a
294
+ * third-party API returns.
295
+ *
296
+ * Tool content is the one kind this release can emit honestly. Flue declares
297
+ * `tool_start.args` and `tool.result` stable, so both are on the same payloads
298
+ * the span lifecycle already reads. Prompts and completions are NOT here:
299
+ * they live on `AgentMessage`, which Flue marks unstable and this package
300
+ * refuses to read (see `flue-types.ts`).
301
+ *
302
+ * ## The read-side contract this honours
303
+ *
304
+ * Runtype's ingest reads exactly these two attributes off an `execute_tool`
305
+ * span (`packages/shared/src/otlp-runtype-semconv.ts`,
306
+ * `GEN_AI_CONTENT_ATTRIBUTES`) and projects them to the tool card's input and
307
+ * output. The value is a STRING: a JSON document is parsed back, anything
308
+ * that does not parse is kept as plain text. It is admitted under a budget
309
+ * (`packages/execution-ingest/src/ingest-content.ts`): a single attribute over
310
+ * `MAX_CONTENT_ATTRIBUTE_CHARS` (256 KiB) is dropped WHOLE, not cut, and one
311
+ * span may carry at most `MAX_CONTENT_SPAN_CHARS` (384 KiB) across every
312
+ * content attribute. Both ceilings are why `maxChars` exists and why its
313
+ * default sits where it does: arguments plus result at the default each fit
314
+ * inside the per-span ceiling with room to spare, and a value this module has
315
+ * already cut can never trip the per-attribute drop.
316
+ *
317
+ * The canonical `gen_ai.tool.call.*` names are emitted for every value,
318
+ * string or not. Stock `@flue/opentelemetry` routes a stringified non-object
319
+ * to `flue.tool.call.*` instead; ingest reads that alias only as a fallback,
320
+ * and the semconv attribute type is a string either way, so there is nothing
321
+ * to gain from the split.
322
+ *
323
+ * The two kinds are encoded differently because ingest projects them
324
+ * differently: a RESULT is any text (a string as-is, anything else as JSON,
325
+ * cut in place when over budget), while ARGUMENTS are always a JSON object,
326
+ * see {@link encodeArguments}.
327
+ */
328
+ /**
329
+ * What to emit, per tool span. Every field is optional and every default is
330
+ * OFF; passing `{}` changes nothing.
331
+ */
332
+ interface FlueContentOptions {
333
+ /**
334
+ * Emit the call's arguments as `gen_ai.tool.call.arguments` when the
335
+ * `execute_tool` span opens. Read from Flue's stable `tool_start.args`.
336
+ */
337
+ toolArguments?: boolean;
338
+ /**
339
+ * Emit the tool's return value as `gen_ai.tool.call.result` just before the
340
+ * `execute_tool` span closes. Read from Flue's stable `tool.result` (2.x's
341
+ * `effectiveResult` — what the model was actually shown — when present).
342
+ *
343
+ * A FAILED tool's result is never emitted: on both Flue lines it commonly
344
+ * IS the error payload, and error messages stay off the wire under every
345
+ * setting of this option. The span still closes with its error type and
346
+ * exception class name, as before.
347
+ */
348
+ toolResults?: boolean;
349
+ /**
350
+ * Ceiling on ONE emitted value, in UTF-16 code units, marker included.
351
+ * Defaults to {@link DEFAULT_CONTENT_MAX_CHARS}. A longer value is cut and
352
+ * ends in an explicit `…[truncated N chars]` marker, so a cut value never
353
+ * masquerades as complete.
354
+ *
355
+ * Clamped to {@link INGEST_CONTENT_ATTRIBUTE_CEILING}: above that, Runtype's
356
+ * ingest drops the attribute whole, which is strictly worse than a cut.
357
+ */
358
+ maxChars?: number;
359
+ /**
360
+ * Rewrite a value before it is encoded. Runs on the RAW value, before
361
+ * stringification and truncation, so a redaction can address fields by name.
362
+ * Return `undefined` to suppress the attribute for this call entirely.
363
+ *
364
+ * An exception thrown here suppresses the attribute and nothing else: the
365
+ * span still opens and closes normally.
366
+ */
367
+ redact?: (value: unknown, context: FlueContentRedactContext) => unknown;
368
+ }
369
+ interface FlueContentRedactContext {
370
+ kind: 'arguments' | 'result';
371
+ toolName: string;
372
+ }
373
+ /**
374
+ * The default per-value ceiling: 64 KiB of ASCII. Large enough for any
375
+ * realistic tool argument object and most results, and small enough that
376
+ * arguments plus result on one span sit well inside ingest's 384 KiB per-span
377
+ * ceiling — a customer who raises it should know that ceiling exists.
378
+ */
379
+ declare const DEFAULT_CONTENT_MAX_CHARS = 65536;
380
+ /**
381
+ * Runtype ingest's per-attribute ceiling (`MAX_CONTENT_ATTRIBUTE_CHARS` in
382
+ * `packages/execution-ingest/src/ingest-content.ts`), inlined for the same
383
+ * reason `semconv.ts` inlines the attribute names — that package is private
384
+ * and cannot resolve for a customer installing from npm. `tests/contract.test.ts`
385
+ * pins the two equal.
386
+ */
387
+ declare const INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
388
+
276
389
  /**
277
390
  * Flue observations → span intents. PURE: no `@opentelemetry/api`, no clock, no
278
391
  * I/O. `spans.ts` is the only module that touches a `Tracer`.
@@ -378,8 +491,8 @@ interface CloseSpanIntent {
378
491
  * Present when the span failed. Carries the error TYPE and the exception
379
492
  * class NAME only — never a message and never a stack. Both are content: a
380
493
  * provider error message routinely quotes the prompt back, and a stack
381
- * exposes filesystem paths and deployment layout. This release emits no
382
- * content at all, so neither is read.
494
+ * exposes filesystem paths and deployment layout. Neither is read under any
495
+ * setting; the `content` opt-in covers tool payloads only.
383
496
  */
384
497
  error?: {
385
498
  type: string;
@@ -402,6 +515,13 @@ interface FlueProjectionOptions {
402
515
  * runs inside the delegating agent's trace, and one trace is one execution.
403
516
  */
404
517
  agents?: Record<string, string>;
518
+ /**
519
+ * Opt in to tool content on `execute_tool` spans. Default: nothing. See
520
+ * {@link FlueContentOptions} for the per-kind switches, the size ceiling
521
+ * and the redaction hook, and `content.ts` for the read-side contract they
522
+ * honour.
523
+ */
524
+ content?: FlueContentOptions;
405
525
  }
406
526
  /**
407
527
  * Resource attributes for the customer's `Resource`. Exported rather than set
@@ -465,6 +585,18 @@ declare const GEN_AI: {
465
585
  readonly serverAddress: "server.address";
466
586
  readonly serverPort: "server.port";
467
587
  };
588
+ /**
589
+ * GenAI semconv CONTENT attribute names — the semconv Opt-In set. Kept apart
590
+ * from {@link GEN_AI} on purpose: that table is what this package emits by
591
+ * DEFAULT, and the contract test pins that no content name is in it. These
592
+ * two reach the wire only under the `content` option (`content.ts`), and only
593
+ * on `execute_tool` spans. Mirrors `GEN_AI_CONTENT_ATTRIBUTES` in
594
+ * `packages/shared/src/otlp-runtype-semconv.ts`, which is what ingest reads.
595
+ */
596
+ declare const GEN_AI_CONTENT: {
597
+ readonly toolCallArguments: "gen_ai.tool.call.arguments";
598
+ readonly toolCallResult: "gen_ai.tool.call.result";
599
+ };
468
600
  /**
469
601
  * Runtype's extension vocabulary. Placement is part of the contract and the
470
602
  * reader enforces it — see the `RUNTYPE_ATTRIBUTES` doc block in the shared
@@ -534,12 +666,13 @@ declare const ADAPTER_VERSION: string;
534
666
  * pipeline fights the one the customer already runs, and in a stack exporting
535
667
  * to two backends it silently wins one of those fights.
536
668
  *
537
- * It also emits **no content** in this release — no prompts, no completions, no
669
+ * It also emits **no content by default** — no prompts, no completions, no
538
670
  * tool arguments or results, no error messages, no stack traces. Only
539
- * identifiers, structure and metrics reach the wire. Content is a later,
540
- * explicitly opted-in increment; until then the safety story needs no
541
- * qualifiers, which is the right default for a package landing inside a
542
- * PHI-bearing process.
671
+ * identifiers, structure and metrics reach the wire, which is the right
672
+ * default for a package landing inside a PHI-bearing process. The one
673
+ * explicit opt-in is `content`: tool arguments and results on `execute_tool`
674
+ * spans, per kind, size-capped and redactable (`content.ts`). Prompts and
675
+ * completions remain off the table until Flue exposes a stable message shape.
543
676
  *
544
677
  * ## Composing with other instrumentations
545
678
  *
@@ -582,4 +715,4 @@ interface RuntypeFlueInstrumentationOptions extends FlueProjectionOptions {
582
715
  */
583
716
  declare function createRuntypeFlueInstrumentation(options?: RuntypeFlueInstrumentationOptions): FlueInstrumentation;
584
717
 
585
- export { ADAPTER_NAME, ADAPTER_VERSION, type FlueInstrumentation, type FlueProjectionOptions, GEN_AI, RUNTYPE, RUNTYPE_FLUE_INSTRUMENTATION_KEY, RUNTYPE_SCHEMA_VERSION, RUNTYPE_STOP_REASONS, RUNTYPE_TOOL_TYPES, type RuntypeFlueInstrumentationOptions, type RuntypeStopReason, type RuntypeToolType, type SpanAttributes, type SpanIntent, createRuntypeFlueInstrumentation, runtypeFlueResourceAttributes };
718
+ export { ADAPTER_NAME, ADAPTER_VERSION, DEFAULT_CONTENT_MAX_CHARS, type FlueContentOptions, type FlueContentRedactContext, type FlueInstrumentation, type FlueProjectionOptions, GEN_AI, GEN_AI_CONTENT, INGEST_CONTENT_ATTRIBUTE_CEILING, RUNTYPE, RUNTYPE_FLUE_INSTRUMENTATION_KEY, RUNTYPE_SCHEMA_VERSION, RUNTYPE_STOP_REASONS, RUNTYPE_TOOL_TYPES, type RuntypeFlueInstrumentationOptions, type RuntypeStopReason, type RuntypeToolType, type SpanAttributes, type SpanIntent, createRuntypeFlueInstrumentation, runtypeFlueResourceAttributes };
package/dist/index.d.ts CHANGED
@@ -193,12 +193,14 @@ type FlueEventVariant = {
193
193
  type: 'tool_start';
194
194
  toolName: string;
195
195
  toolCallId: string;
196
+ args?: unknown;
196
197
  } | {
197
198
  type: 'tool';
198
199
  toolName: string;
199
200
  toolCallId: string;
200
201
  isError?: boolean;
201
202
  result?: unknown;
203
+ effectiveResult?: unknown;
202
204
  durationMs?: number;
203
205
  } | {
204
206
  type: 'submission_settled';
@@ -273,6 +275,117 @@ interface FlueInstrumentation {
273
275
  dispose(): void | Promise<void>;
274
276
  }
275
277
 
278
+ /**
279
+ * Opt-in tool content: `gen_ai.tool.call.arguments` and
280
+ * `gen_ai.tool.call.result` on `execute_tool` spans.
281
+ *
282
+ * PURE, like `projection.ts`: value in, attribute string out. The projection
283
+ * decides WHEN to call this (arguments when the tool span opens, the result
284
+ * just before it closes); this module decides WHAT reaches the wire and how
285
+ * much of it.
286
+ *
287
+ * ## Why opt-in, and why only tools
288
+ *
289
+ * The package's default contract is that no content leaves the process, and
290
+ * that stays the default: an instrumentation that lands inside a PHI-bearing
291
+ * process must not start shipping tool payloads because of a version bump.
292
+ * The `content` option is the explicit decision, and it is per kind, so a
293
+ * deployment can export the arguments it controls without the results a
294
+ * third-party API returns.
295
+ *
296
+ * Tool content is the one kind this release can emit honestly. Flue declares
297
+ * `tool_start.args` and `tool.result` stable, so both are on the same payloads
298
+ * the span lifecycle already reads. Prompts and completions are NOT here:
299
+ * they live on `AgentMessage`, which Flue marks unstable and this package
300
+ * refuses to read (see `flue-types.ts`).
301
+ *
302
+ * ## The read-side contract this honours
303
+ *
304
+ * Runtype's ingest reads exactly these two attributes off an `execute_tool`
305
+ * span (`packages/shared/src/otlp-runtype-semconv.ts`,
306
+ * `GEN_AI_CONTENT_ATTRIBUTES`) and projects them to the tool card's input and
307
+ * output. The value is a STRING: a JSON document is parsed back, anything
308
+ * that does not parse is kept as plain text. It is admitted under a budget
309
+ * (`packages/execution-ingest/src/ingest-content.ts`): a single attribute over
310
+ * `MAX_CONTENT_ATTRIBUTE_CHARS` (256 KiB) is dropped WHOLE, not cut, and one
311
+ * span may carry at most `MAX_CONTENT_SPAN_CHARS` (384 KiB) across every
312
+ * content attribute. Both ceilings are why `maxChars` exists and why its
313
+ * default sits where it does: arguments plus result at the default each fit
314
+ * inside the per-span ceiling with room to spare, and a value this module has
315
+ * already cut can never trip the per-attribute drop.
316
+ *
317
+ * The canonical `gen_ai.tool.call.*` names are emitted for every value,
318
+ * string or not. Stock `@flue/opentelemetry` routes a stringified non-object
319
+ * to `flue.tool.call.*` instead; ingest reads that alias only as a fallback,
320
+ * and the semconv attribute type is a string either way, so there is nothing
321
+ * to gain from the split.
322
+ *
323
+ * The two kinds are encoded differently because ingest projects them
324
+ * differently: a RESULT is any text (a string as-is, anything else as JSON,
325
+ * cut in place when over budget), while ARGUMENTS are always a JSON object,
326
+ * see {@link encodeArguments}.
327
+ */
328
+ /**
329
+ * What to emit, per tool span. Every field is optional and every default is
330
+ * OFF; passing `{}` changes nothing.
331
+ */
332
+ interface FlueContentOptions {
333
+ /**
334
+ * Emit the call's arguments as `gen_ai.tool.call.arguments` when the
335
+ * `execute_tool` span opens. Read from Flue's stable `tool_start.args`.
336
+ */
337
+ toolArguments?: boolean;
338
+ /**
339
+ * Emit the tool's return value as `gen_ai.tool.call.result` just before the
340
+ * `execute_tool` span closes. Read from Flue's stable `tool.result` (2.x's
341
+ * `effectiveResult` — what the model was actually shown — when present).
342
+ *
343
+ * A FAILED tool's result is never emitted: on both Flue lines it commonly
344
+ * IS the error payload, and error messages stay off the wire under every
345
+ * setting of this option. The span still closes with its error type and
346
+ * exception class name, as before.
347
+ */
348
+ toolResults?: boolean;
349
+ /**
350
+ * Ceiling on ONE emitted value, in UTF-16 code units, marker included.
351
+ * Defaults to {@link DEFAULT_CONTENT_MAX_CHARS}. A longer value is cut and
352
+ * ends in an explicit `…[truncated N chars]` marker, so a cut value never
353
+ * masquerades as complete.
354
+ *
355
+ * Clamped to {@link INGEST_CONTENT_ATTRIBUTE_CEILING}: above that, Runtype's
356
+ * ingest drops the attribute whole, which is strictly worse than a cut.
357
+ */
358
+ maxChars?: number;
359
+ /**
360
+ * Rewrite a value before it is encoded. Runs on the RAW value, before
361
+ * stringification and truncation, so a redaction can address fields by name.
362
+ * Return `undefined` to suppress the attribute for this call entirely.
363
+ *
364
+ * An exception thrown here suppresses the attribute and nothing else: the
365
+ * span still opens and closes normally.
366
+ */
367
+ redact?: (value: unknown, context: FlueContentRedactContext) => unknown;
368
+ }
369
+ interface FlueContentRedactContext {
370
+ kind: 'arguments' | 'result';
371
+ toolName: string;
372
+ }
373
+ /**
374
+ * The default per-value ceiling: 64 KiB of ASCII. Large enough for any
375
+ * realistic tool argument object and most results, and small enough that
376
+ * arguments plus result on one span sit well inside ingest's 384 KiB per-span
377
+ * ceiling — a customer who raises it should know that ceiling exists.
378
+ */
379
+ declare const DEFAULT_CONTENT_MAX_CHARS = 65536;
380
+ /**
381
+ * Runtype ingest's per-attribute ceiling (`MAX_CONTENT_ATTRIBUTE_CHARS` in
382
+ * `packages/execution-ingest/src/ingest-content.ts`), inlined for the same
383
+ * reason `semconv.ts` inlines the attribute names — that package is private
384
+ * and cannot resolve for a customer installing from npm. `tests/contract.test.ts`
385
+ * pins the two equal.
386
+ */
387
+ declare const INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
388
+
276
389
  /**
277
390
  * Flue observations → span intents. PURE: no `@opentelemetry/api`, no clock, no
278
391
  * I/O. `spans.ts` is the only module that touches a `Tracer`.
@@ -378,8 +491,8 @@ interface CloseSpanIntent {
378
491
  * Present when the span failed. Carries the error TYPE and the exception
379
492
  * class NAME only — never a message and never a stack. Both are content: a
380
493
  * provider error message routinely quotes the prompt back, and a stack
381
- * exposes filesystem paths and deployment layout. This release emits no
382
- * content at all, so neither is read.
494
+ * exposes filesystem paths and deployment layout. Neither is read under any
495
+ * setting; the `content` opt-in covers tool payloads only.
383
496
  */
384
497
  error?: {
385
498
  type: string;
@@ -402,6 +515,13 @@ interface FlueProjectionOptions {
402
515
  * runs inside the delegating agent's trace, and one trace is one execution.
403
516
  */
404
517
  agents?: Record<string, string>;
518
+ /**
519
+ * Opt in to tool content on `execute_tool` spans. Default: nothing. See
520
+ * {@link FlueContentOptions} for the per-kind switches, the size ceiling
521
+ * and the redaction hook, and `content.ts` for the read-side contract they
522
+ * honour.
523
+ */
524
+ content?: FlueContentOptions;
405
525
  }
406
526
  /**
407
527
  * Resource attributes for the customer's `Resource`. Exported rather than set
@@ -465,6 +585,18 @@ declare const GEN_AI: {
465
585
  readonly serverAddress: "server.address";
466
586
  readonly serverPort: "server.port";
467
587
  };
588
+ /**
589
+ * GenAI semconv CONTENT attribute names — the semconv Opt-In set. Kept apart
590
+ * from {@link GEN_AI} on purpose: that table is what this package emits by
591
+ * DEFAULT, and the contract test pins that no content name is in it. These
592
+ * two reach the wire only under the `content` option (`content.ts`), and only
593
+ * on `execute_tool` spans. Mirrors `GEN_AI_CONTENT_ATTRIBUTES` in
594
+ * `packages/shared/src/otlp-runtype-semconv.ts`, which is what ingest reads.
595
+ */
596
+ declare const GEN_AI_CONTENT: {
597
+ readonly toolCallArguments: "gen_ai.tool.call.arguments";
598
+ readonly toolCallResult: "gen_ai.tool.call.result";
599
+ };
468
600
  /**
469
601
  * Runtype's extension vocabulary. Placement is part of the contract and the
470
602
  * reader enforces it — see the `RUNTYPE_ATTRIBUTES` doc block in the shared
@@ -534,12 +666,13 @@ declare const ADAPTER_VERSION: string;
534
666
  * pipeline fights the one the customer already runs, and in a stack exporting
535
667
  * to two backends it silently wins one of those fights.
536
668
  *
537
- * It also emits **no content** in this release — no prompts, no completions, no
669
+ * It also emits **no content by default** — no prompts, no completions, no
538
670
  * tool arguments or results, no error messages, no stack traces. Only
539
- * identifiers, structure and metrics reach the wire. Content is a later,
540
- * explicitly opted-in increment; until then the safety story needs no
541
- * qualifiers, which is the right default for a package landing inside a
542
- * PHI-bearing process.
671
+ * identifiers, structure and metrics reach the wire, which is the right
672
+ * default for a package landing inside a PHI-bearing process. The one
673
+ * explicit opt-in is `content`: tool arguments and results on `execute_tool`
674
+ * spans, per kind, size-capped and redactable (`content.ts`). Prompts and
675
+ * completions remain off the table until Flue exposes a stable message shape.
543
676
  *
544
677
  * ## Composing with other instrumentations
545
678
  *
@@ -582,4 +715,4 @@ interface RuntypeFlueInstrumentationOptions extends FlueProjectionOptions {
582
715
  */
583
716
  declare function createRuntypeFlueInstrumentation(options?: RuntypeFlueInstrumentationOptions): FlueInstrumentation;
584
717
 
585
- export { ADAPTER_NAME, ADAPTER_VERSION, type FlueInstrumentation, type FlueProjectionOptions, GEN_AI, RUNTYPE, RUNTYPE_FLUE_INSTRUMENTATION_KEY, RUNTYPE_SCHEMA_VERSION, RUNTYPE_STOP_REASONS, RUNTYPE_TOOL_TYPES, type RuntypeFlueInstrumentationOptions, type RuntypeStopReason, type RuntypeToolType, type SpanAttributes, type SpanIntent, createRuntypeFlueInstrumentation, runtypeFlueResourceAttributes };
718
+ export { ADAPTER_NAME, ADAPTER_VERSION, DEFAULT_CONTENT_MAX_CHARS, type FlueContentOptions, type FlueContentRedactContext, type FlueInstrumentation, type FlueProjectionOptions, GEN_AI, GEN_AI_CONTENT, INGEST_CONTENT_ATTRIBUTE_CEILING, RUNTYPE, RUNTYPE_FLUE_INSTRUMENTATION_KEY, RUNTYPE_SCHEMA_VERSION, RUNTYPE_STOP_REASONS, RUNTYPE_TOOL_TYPES, type RuntypeFlueInstrumentationOptions, type RuntypeStopReason, type RuntypeToolType, type SpanAttributes, type SpanIntent, createRuntypeFlueInstrumentation, runtypeFlueResourceAttributes };
package/dist/index.mjs CHANGED
@@ -42,8 +42,83 @@ function mapSettlementOutcome(outcome) {
42
42
  return void 0;
43
43
  }
44
44
 
45
+ // src/content.ts
46
+ var DEFAULT_CONTENT_MAX_CHARS = 65536;
47
+ var INGEST_CONTENT_ATTRIBUTE_CEILING = 262144;
48
+ function resolveContentPolicy(options) {
49
+ const requested = options?.maxChars;
50
+ const maxChars = typeof requested === "number" && Number.isFinite(requested) && requested > 0 ? Math.min(Math.floor(requested), INGEST_CONTENT_ATTRIBUTE_CEILING) : DEFAULT_CONTENT_MAX_CHARS;
51
+ return {
52
+ toolArguments: options?.toolArguments === true,
53
+ toolResults: options?.toolResults === true,
54
+ maxChars,
55
+ redact: options?.redact
56
+ };
57
+ }
58
+ function encodeContentValue(value, policy, context3) {
59
+ 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);
70
+ const encoded = stringify(redacted);
71
+ if (encoded === void 0) return void 0;
72
+ return truncate(encoded, policy.maxChars);
73
+ }
74
+ function encodeArguments(value, maxChars) {
75
+ const shaped = isPlainObject(value) ? value : { value };
76
+ const encoded = stringify(shaped);
77
+ if (encoded === void 0) return void 0;
78
+ 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)) });
86
+ }
87
+ var TRUNCATED_WRAPPER_OVERHEAD = 16;
88
+ function isPlainObject(value) {
89
+ return value !== null && typeof value === "object" && !Array.isArray(value);
90
+ }
91
+ function stringify(value) {
92
+ if (typeof value === "string") return value;
93
+ try {
94
+ const encoded = JSON.stringify(value);
95
+ return typeof encoded === "string" ? encoded : void 0;
96
+ } catch {
97
+ return void 0;
98
+ }
99
+ }
100
+ function truncate(value, maxChars) {
101
+ if (value.length <= maxChars) return value;
102
+ const upperBound = truncationMarker(value.length);
103
+ let keep = Math.max(0, maxChars - upperBound.length);
104
+ let marker = truncationMarker(value.length - keep);
105
+ keep = Math.max(0, maxChars - marker.length);
106
+ marker = truncationMarker(value.length - keep);
107
+ if (keep > 0 && isHighSurrogate(value.charCodeAt(keep - 1))) {
108
+ keep -= 1;
109
+ marker = truncationMarker(value.length - keep);
110
+ }
111
+ return value.slice(0, keep) + marker;
112
+ }
113
+ function truncationMarker(droppedChars) {
114
+ return `\u2026[truncated ${droppedChars} chars]`;
115
+ }
116
+ function isHighSurrogate(code) {
117
+ return code >= 55296 && code <= 56319;
118
+ }
119
+
45
120
  // package.json
46
- var version = "0.2.2";
121
+ var version = "0.4.0";
47
122
 
48
123
  // src/semconv.ts
49
124
  var GEN_AI = {
@@ -71,6 +146,10 @@ var GEN_AI = {
71
146
  serverAddress: "server.address",
72
147
  serverPort: "server.port"
73
148
  };
149
+ var GEN_AI_CONTENT = {
150
+ toolCallArguments: "gen_ai.tool.call.arguments",
151
+ toolCallResult: "gen_ai.tool.call.result"
152
+ };
74
153
  var GEN_AI_OPERATION_INVOKE_AGENT = "invoke_agent";
75
154
  var GEN_AI_OPERATION_CHAT = "chat";
76
155
  var GEN_AI_OPERATION_EXECUTE_TOOL = "execute_tool";
@@ -140,6 +219,7 @@ function runtypeFlueResourceAttributes(resource) {
140
219
  };
141
220
  }
142
221
  function createFlueProjection(options = {}) {
222
+ const content = resolveContentPolicy(options.content);
143
223
  const open = /* @__PURE__ */ new Map();
144
224
  const envelopes = /* @__PURE__ */ new Map();
145
225
  const envelopeByChain = /* @__PURE__ */ new Map();
@@ -224,7 +304,12 @@ function createFlueProjection(options = {}) {
224
304
  // any inbound id; it does not key the row, which stays derived from
225
305
  // the trace id.
226
306
  ...isEnvelope && event.submissionId ? { [RUNTYPE.executionId]: event.submissionId } : {},
227
- ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {}
307
+ ...isEnvelope && agentId ? { [RUNTYPE.agentId]: agentId } : {},
308
+ // WHY(README.md): the resource placement is the customer's to wire and is routinely skipped.
309
+ ...isEnvelope ? {
310
+ [RUNTYPE.adapterName]: ADAPTER_NAME,
311
+ [RUNTYPE.adapterVersion]: ADAPTER_VERSION
312
+ } : {}
228
313
  }
229
314
  }
230
315
  ];
@@ -310,7 +395,12 @@ function createFlueProjection(options = {}) {
310
395
  const ref = compactionKey(event);
311
396
  if (open.has(ref)) return [];
312
397
  const parentRef = resolveParentRef(event);
313
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
398
+ open.set(ref, {
399
+ ref,
400
+ ...parentRef ? { parentRef } : {},
401
+ chainKey: chainKey(event),
402
+ isTask: false
403
+ });
314
404
  const reason = readString(event, "reason");
315
405
  return [
316
406
  {
@@ -348,7 +438,12 @@ function createFlueProjection(options = {}) {
348
438
  const purpose = readString(event, "purpose") ?? "agent";
349
439
  const compactionRef = compactionKey(event);
350
440
  const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
351
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
441
+ open.set(ref, {
442
+ ref,
443
+ ...parentRef ? { parentRef } : {},
444
+ chainKey: chainKey(event),
445
+ isTask: false
446
+ });
352
447
  const envelopeRef = resolveEnvelopeRef(event);
353
448
  const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
354
449
  let position;
@@ -376,7 +471,8 @@ function createFlueProjection(options = {}) {
376
471
  if (position) turnPositions.set(ref, position);
377
472
  }
378
473
  if (envelope) {
379
- if (!envelope.modelPinned && purpose === "agent") envelope.requestModel = request.requestedModel;
474
+ if (!envelope.modelPinned && purpose === "agent")
475
+ envelope.requestModel = request.requestedModel;
380
476
  if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
381
477
  }
382
478
  return [
@@ -453,7 +549,12 @@ function createFlueProjection(options = {}) {
453
549
  const ref = toolKey(event);
454
550
  if (open.has(ref)) return [];
455
551
  const parentRef = resolveParentRef(event);
456
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
552
+ open.set(ref, {
553
+ ref,
554
+ ...parentRef ? { parentRef } : {},
555
+ chainKey: chainKey(event),
556
+ isTask: false
557
+ });
457
558
  const position = turnPositions.get(turnKey(event));
458
559
  if (isCallerShellCall(toolName, event)) {
459
560
  return [
@@ -477,6 +578,10 @@ function createFlueProjection(options = {}) {
477
578
  }
478
579
  const isDelegation = isTaskDelegationTool(toolName, event);
479
580
  const toolType = resolveToolType(toolName, event);
581
+ const args = content.toolArguments ? encodeContentValue(readUnknown(event, "args"), content, {
582
+ kind: "arguments",
583
+ toolName
584
+ }) : void 0;
480
585
  return [
481
586
  {
482
587
  kind: "open",
@@ -490,6 +595,7 @@ function createFlueProjection(options = {}) {
490
595
  [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
491
596
  [GEN_AI.toolName]: toolName,
492
597
  [GEN_AI.toolCallId]: toolCallId,
598
+ ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
493
599
  // The semconv `gen_ai.tool.type` domain is the transport-level one
494
600
  // (`function` / `extension` / `datastore`); a sub-agent delegation is
495
601
  // not a plain function call, so it is left unclaimed there while
@@ -508,9 +614,22 @@ function createFlueProjection(options = {}) {
508
614
  const ref = toolKey(event);
509
615
  if (!open.has(ref)) return [];
510
616
  const intents = sweepDescendants(ref, event.timestamp);
511
- intents.push(
512
- closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
513
- );
617
+ const isError = readBoolean(event, "isError") === true;
618
+ const toolName = readString(event, "toolName");
619
+ if (content.toolResults && !isError && toolName && !isCallerShellCall(toolName, event)) {
620
+ const result = encodeContentValue(readToolResult(event), content, {
621
+ kind: "result",
622
+ toolName
623
+ });
624
+ if (result !== void 0) {
625
+ intents.push({
626
+ kind: "update",
627
+ ref,
628
+ attributes: { [GEN_AI_CONTENT.toolCallResult]: result }
629
+ });
630
+ }
631
+ }
632
+ intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
514
633
  open.delete(ref);
515
634
  return intents;
516
635
  }
@@ -702,6 +821,13 @@ function readErrorInfo(event) {
702
821
  const detail = event.errorInfo;
703
822
  return detail !== null && typeof detail === "object" ? detail : void 0;
704
823
  }
824
+ function readUnknown(event, key) {
825
+ return event[key];
826
+ }
827
+ function readToolResult(event) {
828
+ const record = event;
829
+ return Object.prototype.hasOwnProperty.call(record, "effectiveResult") ? record.effectiveResult : record.result;
830
+ }
705
831
  function readString(event, key) {
706
832
  const value = event[key];
707
833
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -718,7 +844,12 @@ function positiveNumber(value) {
718
844
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
719
845
  }
720
846
  function chainKey(value) {
721
- return identityKey("chain", [value.instanceId, value.harness, value.conversationId, value.session]);
847
+ return identityKey("chain", [
848
+ value.instanceId,
849
+ value.harness,
850
+ value.conversationId,
851
+ value.session
852
+ ]);
722
853
  }
723
854
  function operationKey(value) {
724
855
  return identityKey("operation", [
@@ -912,7 +1043,10 @@ function readIdField(operation, key) {
912
1043
  export {
913
1044
  ADAPTER_NAME,
914
1045
  ADAPTER_VERSION,
1046
+ DEFAULT_CONTENT_MAX_CHARS,
915
1047
  GEN_AI,
1048
+ GEN_AI_CONTENT,
1049
+ INGEST_CONTENT_ATTRIBUTE_CEILING,
916
1050
  RUNTYPE,
917
1051
  RUNTYPE_FLUE_INSTRUMENTATION_KEY,
918
1052
  RUNTYPE_SCHEMA_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/flue-otel",
3
- "version": "0.2.2",
3
+ "version": "0.4.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",
@@ -31,10 +31,11 @@
31
31
  "@opentelemetry/api": "^1.9.0",
32
32
  "@opentelemetry/context-async-hooks": "^2.1.0",
33
33
  "@opentelemetry/sdk-trace-base": "^2.1.0",
34
+ "@runtypelabs/execution-ingest": "0.6.18",
35
+ "@runtypelabs/shared": "3.35.1",
34
36
  "tsup": "^8.0.2",
35
37
  "typescript": "^6.0.3",
36
- "vitest": "^4.1.0",
37
- "@runtypelabs/shared": "3.28.0"
38
+ "vitest": "^4.1.0"
38
39
  },
39
40
  "publishConfig": {
40
41
  "access": "public"