@runtypelabs/flue-otel 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
@@ -105,11 +107,15 @@ subscriber. Its key is exported as `RUNTYPE_FLUE_INSTRUMENTATION_KEY`.
105
107
  - `runtypeFlueResourceAttributes({ agentId? })` builds the `runtype.*` resource
106
108
  attributes (`schema.version`, `adapter.name`, `adapter.version`, and
107
109
  `agent.id` when given). Spread it into your SDK `Resource`.
108
- - `GEN_AI` and `RUNTYPE`: the attribute-name constants this package emits.
110
+ - `GEN_AI` and `RUNTYPE`: the attribute-name constants this package emits by
111
+ default; `GEN_AI_CONTENT`: the two it emits under the `content` opt-in.
112
+ - `DEFAULT_CONTENT_MAX_CHARS` and `INGEST_CONTENT_ATTRIBUTE_CEILING`: the
113
+ content size defaults, see [Tool content](#tool-content).
109
114
  - `RUNTYPE_STOP_REASONS`, `RUNTYPE_TOOL_TYPES`, `RUNTYPE_SCHEMA_VERSION`,
110
115
  `ADAPTER_NAME`, `ADAPTER_VERSION`, plus the `RuntypeStopReason`,
111
116
  `RuntypeToolType`, `FlueInstrumentation`, `FlueProjectionOptions`,
112
- `SpanAttributes` and `SpanIntent` types.
117
+ `FlueContentOptions`, `FlueContentRedactContext`, `SpanAttributes` and
118
+ `SpanIntent` types.
113
119
 
114
120
  ## Attribution
115
121
 
@@ -175,12 +181,12 @@ backends.
175
181
 
176
182
  ## What it emits
177
183
 
178
- | Span | When | Carries |
179
- | -------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
180
- | `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
- | `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), and `runtype.turn.id` / `runtype.turn.index` / `runtype.iteration` |
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 |
183
- | `flue.task <agent>`, `flue.compaction`, `flue.operation shell` | delegation, compaction, host shell call | correlation ids only; framework structure, not agent invocations |
184
+ | Span | When | Carries |
185
+ | -------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
186
+ | `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 |
187
+ | `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) |
188
+ | `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 |
189
+ | `flue.task <agent>`, `flue.compaction`, `flue.operation shell` | delegation, compaction, host shell call | correlation ids only; framework structure, not agent invocations |
184
190
 
185
191
  Every span also carries Flue's own `flue.*` correlation attributes
186
192
  (`flue.instance.id`, `flue.submission.id`, `flue.agent.name`,
@@ -220,6 +226,73 @@ Rules the projection follows:
220
226
 
221
227
  An absent attribute costs one column. A wrong one renders as a measurement.
222
228
 
229
+ ## Tool content
230
+
231
+ By default an `execute_tool` span says which tool ran and how long it took, and
232
+ Runtype's trace view shows its card as "No tool content in this trace". To see
233
+ arguments and results on that card, opt in:
234
+
235
+ ```ts
236
+ createRuntypeFlueInstrumentation({
237
+ content: {
238
+ toolArguments: true, // gen_ai.tool.call.arguments, set when the span opens
239
+ toolResults: true, // gen_ai.tool.call.result, set just before the span ends
240
+ maxChars: 65_536, // per value, marker included; this is the default
241
+ redact: (value, { kind, toolName }) => value, // optional; return undefined to drop
242
+ },
243
+ })
244
+ ```
245
+
246
+ Each switch is independent and defaults to off, so `{}` changes nothing. The
247
+ values come from Flue's stable `tool_start.args` and `tool.result` payloads
248
+ (2.x's `effectiveResult`, what the model was actually shown, when present).
249
+
250
+ The attribute names are the GenAI semconv `gen_ai.tool.call.arguments` and
251
+ `gen_ai.tool.call.result` (exported as `GEN_AI_CONTENT`), which Runtype reads
252
+ onto the tool card's input and output. The two are encoded differently because
253
+ Runtype projects them differently:
254
+
255
+ - **Arguments are always a JSON object**, since that is the only shape the
256
+ reader projects. A non-object value (a bare string, an array) is carried as
257
+ `{ "value": ... }` rather than dropped. Over `maxChars`, the object is not
258
+ cut in place (a mid-document cut is not JSON and would be discarded); it is
259
+ replaced by `{ "truncated": "<head of the JSON>…[truncated N chars]" }`.
260
+ - **A result is text**: a string as-is, anything else as JSON. Over `maxChars`
261
+ it is cut and ends in `…[truncated N chars]`, so a cut value never looks
262
+ complete.
263
+
264
+ Ceilings worth knowing, because they decide what Runtype keeps:
265
+
266
+ - `maxChars` defaults to `DEFAULT_CONTENT_MAX_CHARS` (64 KiB) and is clamped to
267
+ `INGEST_CONTENT_ATTRIBUTE_CEILING` (256 KiB). Runtype drops a single content
268
+ attribute above that ceiling **whole** rather than cutting it, which is why
269
+ the package cuts first.
270
+ - One span may carry at most 384 KiB of content across every attribute, and one
271
+ export request at most 2 MiB of content. Arguments plus result at the default
272
+ fit inside the per-span ceiling; raise `maxChars` with that in mind.
273
+ - Content makes spans large. A `BatchSpanProcessor` at its default 512 spans per
274
+ request can push an export past Runtype's 8 MiB request bound, which rejects
275
+ the whole batch (envelope and usage included, not only the content). With
276
+ both switches on, set `maxExportBatchSize` to something like 64.
277
+
278
+ What is still never sent, whatever you set:
279
+
280
+ - **A failed tool's result.** On both Flue lines it is commonly the error
281
+ payload, and error messages stay off the wire. The span still closes with its
282
+ error type and exception class name.
283
+ - **The host's own shell call** (`session.shell()`), which is not a tool row.
284
+ - **Prompts and completions.** They live on `AgentMessage`, which Flue marks
285
+ unstable. This package will read them once Flue exposes a stable shape. One
286
+ edge to know: a delegation to a sub-agent is a tool call, and its
287
+ **arguments carry the prompt handed to that sub-agent**. If that matters for
288
+ your deployment, drop it with `redact` (return `undefined` when `toolName`
289
+ is the delegation tool).
290
+
291
+ `redact` runs on the raw value before encoding, so it can address fields by
292
+ name; it receives `{ kind: 'arguments' | 'result', toolName }`. Returning
293
+ `undefined` drops the attribute for that call. If it throws, the attribute is
294
+ dropped and the span is otherwise unaffected.
295
+
223
296
  ## Flue compatibility
224
297
 
225
298
  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.1";
157
+ var version = "0.3.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";
@@ -118,7 +200,9 @@ var RUNTYPE = {
118
200
  toolsReported: "runtype.tools.reported",
119
201
  toolType: "runtype.tool.type",
120
202
  turnId: "runtype.turn.id",
121
- turnIndex: "runtype.turn.index"
203
+ turnIndex: "runtype.turn.index",
204
+ providerFinishReason: "runtype.provider.finish_reason",
205
+ gatewayLogId: "runtype.gateway.log_id"
122
206
  };
123
207
  var FLUE = {
124
208
  instanceId: "flue.instance.id",
@@ -171,6 +255,7 @@ function runtypeFlueResourceAttributes(resource) {
171
255
  };
172
256
  }
173
257
  function createFlueProjection(options = {}) {
258
+ const content = resolveContentPolicy(options.content);
174
259
  const open = /* @__PURE__ */ new Map();
175
260
  const envelopes = /* @__PURE__ */ new Map();
176
261
  const envelopeByChain = /* @__PURE__ */ new Map();
@@ -341,7 +426,12 @@ function createFlueProjection(options = {}) {
341
426
  const ref = compactionKey(event);
342
427
  if (open.has(ref)) return [];
343
428
  const parentRef = resolveParentRef(event);
344
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
429
+ open.set(ref, {
430
+ ref,
431
+ ...parentRef ? { parentRef } : {},
432
+ chainKey: chainKey(event),
433
+ isTask: false
434
+ });
345
435
  const reason = readString(event, "reason");
346
436
  return [
347
437
  {
@@ -379,7 +469,12 @@ function createFlueProjection(options = {}) {
379
469
  const purpose = readString(event, "purpose") ?? "agent";
380
470
  const compactionRef = compactionKey(event);
381
471
  const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
382
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
472
+ open.set(ref, {
473
+ ref,
474
+ ...parentRef ? { parentRef } : {},
475
+ chainKey: chainKey(event),
476
+ isTask: false
477
+ });
383
478
  const envelopeRef = resolveEnvelopeRef(event);
384
479
  const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
385
480
  let position;
@@ -407,7 +502,8 @@ function createFlueProjection(options = {}) {
407
502
  if (position) turnPositions.set(ref, position);
408
503
  }
409
504
  if (envelope) {
410
- if (!envelope.modelPinned && purpose === "agent") envelope.requestModel = request.requestedModel;
505
+ if (!envelope.modelPinned && purpose === "agent")
506
+ envelope.requestModel = request.requestedModel;
411
507
  if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
412
508
  }
413
509
  return [
@@ -459,7 +555,17 @@ function createFlueProjection(options = {}) {
459
555
  ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
460
556
  ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
461
557
  ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
462
- ...usageAttributes(response.usage)
558
+ ...usageAttributes(response.usage),
559
+ // Provider-diagnostic pointers, emitted only when Flue actually provides
560
+ // them. Both are optional and provider-dependent (Workers AI attaches
561
+ // both today); an absent value costs one column, a synthesized one would
562
+ // render as a measurement. `gatewayLogId` is a pointer to the content
563
+ // without shipping the content — a customer can click through to that
564
+ // exact request in their own AI Gateway dashboard. `providerFinishReason`
565
+ // is the provider's exact finish value before normalization, which our
566
+ // `GEN_AI.finishReasons` above deliberately hides.
567
+ ...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
568
+ ...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
463
569
  };
464
570
  const intents = [];
465
571
  if (Object.keys(attributes).length > 0) intents.push({ kind: "update", ref, attributes });
@@ -474,7 +580,12 @@ function createFlueProjection(options = {}) {
474
580
  const ref = toolKey(event);
475
581
  if (open.has(ref)) return [];
476
582
  const parentRef = resolveParentRef(event);
477
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
583
+ open.set(ref, {
584
+ ref,
585
+ ...parentRef ? { parentRef } : {},
586
+ chainKey: chainKey(event),
587
+ isTask: false
588
+ });
478
589
  const position = turnPositions.get(turnKey(event));
479
590
  if (isCallerShellCall(toolName, event)) {
480
591
  return [
@@ -498,6 +609,10 @@ function createFlueProjection(options = {}) {
498
609
  }
499
610
  const isDelegation = isTaskDelegationTool(toolName, event);
500
611
  const toolType = resolveToolType(toolName, event);
612
+ const args = content.toolArguments ? encodeContentValue(readUnknown(event, "args"), content, {
613
+ kind: "arguments",
614
+ toolName
615
+ }) : void 0;
501
616
  return [
502
617
  {
503
618
  kind: "open",
@@ -511,6 +626,7 @@ function createFlueProjection(options = {}) {
511
626
  [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
512
627
  [GEN_AI.toolName]: toolName,
513
628
  [GEN_AI.toolCallId]: toolCallId,
629
+ ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
514
630
  // The semconv `gen_ai.tool.type` domain is the transport-level one
515
631
  // (`function` / `extension` / `datastore`); a sub-agent delegation is
516
632
  // not a plain function call, so it is left unclaimed there while
@@ -529,9 +645,22 @@ function createFlueProjection(options = {}) {
529
645
  const ref = toolKey(event);
530
646
  if (!open.has(ref)) return [];
531
647
  const intents = sweepDescendants(ref, event.timestamp);
532
- intents.push(
533
- closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
534
- );
648
+ const isError = readBoolean(event, "isError") === true;
649
+ const toolName = readString(event, "toolName");
650
+ if (content.toolResults && !isError && toolName && !isCallerShellCall(toolName, event)) {
651
+ const result = encodeContentValue(readToolResult(event), content, {
652
+ kind: "result",
653
+ toolName
654
+ });
655
+ if (result !== void 0) {
656
+ intents.push({
657
+ kind: "update",
658
+ ref,
659
+ attributes: { [GEN_AI_CONTENT.toolCallResult]: result }
660
+ });
661
+ }
662
+ }
663
+ intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
535
664
  open.delete(ref);
536
665
  return intents;
537
666
  }
@@ -723,6 +852,13 @@ function readErrorInfo(event) {
723
852
  const detail = event.errorInfo;
724
853
  return detail !== null && typeof detail === "object" ? detail : void 0;
725
854
  }
855
+ function readUnknown(event, key) {
856
+ return event[key];
857
+ }
858
+ function readToolResult(event) {
859
+ const record = event;
860
+ return Object.prototype.hasOwnProperty.call(record, "effectiveResult") ? record.effectiveResult : record.result;
861
+ }
726
862
  function readString(event, key) {
727
863
  const value = event[key];
728
864
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -739,7 +875,12 @@ function positiveNumber(value) {
739
875
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
740
876
  }
741
877
  function chainKey(value) {
742
- return identityKey("chain", [value.instanceId, value.harness, value.conversationId, value.session]);
878
+ return identityKey("chain", [
879
+ value.instanceId,
880
+ value.harness,
881
+ value.conversationId,
882
+ value.session
883
+ ]);
743
884
  }
744
885
  function operationKey(value) {
745
886
  return identityKey("operation", [
@@ -928,7 +1069,10 @@ function readIdField(operation, key) {
928
1069
  0 && (module.exports = {
929
1070
  ADAPTER_NAME,
930
1071
  ADAPTER_VERSION,
1072
+ DEFAULT_CONTENT_MAX_CHARS,
931
1073
  GEN_AI,
1074
+ GEN_AI_CONTENT,
1075
+ INGEST_CONTENT_ATTRIBUTE_CEILING,
932
1076
  RUNTYPE,
933
1077
  RUNTYPE_FLUE_INSTRUMENTATION_KEY,
934
1078
  RUNTYPE_SCHEMA_VERSION,
package/dist/index.d.cts CHANGED
@@ -100,6 +100,13 @@ interface FlueModelResponse {
100
100
  finishReason?: string;
101
101
  /** 2.x only. The provider's raw finish value before normalization. */
102
102
  providerFinishReason?: string;
103
+ /**
104
+ * The response's own gateway log id (e.g. Cloudflare AI Gateway's
105
+ * `cf-aig-log-id`), for correlating a specific turn with its entry in the
106
+ * gateway dashboard. Telemetry only — present only when the provider records
107
+ * one. The Workers AI provider attaches it today.
108
+ */
109
+ gatewayLogId?: string;
103
110
  error?: FlueErrorInfo;
104
111
  }
105
112
  /**
@@ -186,12 +193,14 @@ type FlueEventVariant = {
186
193
  type: 'tool_start';
187
194
  toolName: string;
188
195
  toolCallId: string;
196
+ args?: unknown;
189
197
  } | {
190
198
  type: 'tool';
191
199
  toolName: string;
192
200
  toolCallId: string;
193
201
  isError?: boolean;
194
202
  result?: unknown;
203
+ effectiveResult?: unknown;
195
204
  durationMs?: number;
196
205
  } | {
197
206
  type: 'submission_settled';
@@ -266,6 +275,117 @@ interface FlueInstrumentation {
266
275
  dispose(): void | Promise<void>;
267
276
  }
268
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
+
269
389
  /**
270
390
  * Flue observations → span intents. PURE: no `@opentelemetry/api`, no clock, no
271
391
  * I/O. `spans.ts` is the only module that touches a `Tracer`.
@@ -371,8 +491,8 @@ interface CloseSpanIntent {
371
491
  * Present when the span failed. Carries the error TYPE and the exception
372
492
  * class NAME only — never a message and never a stack. Both are content: a
373
493
  * provider error message routinely quotes the prompt back, and a stack
374
- * exposes filesystem paths and deployment layout. This release emits no
375
- * 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.
376
496
  */
377
497
  error?: {
378
498
  type: string;
@@ -395,6 +515,13 @@ interface FlueProjectionOptions {
395
515
  * runs inside the delegating agent's trace, and one trace is one execution.
396
516
  */
397
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;
398
525
  }
399
526
  /**
400
527
  * Resource attributes for the customer's `Resource`. Exported rather than set
@@ -458,6 +585,18 @@ declare const GEN_AI: {
458
585
  readonly serverAddress: "server.address";
459
586
  readonly serverPort: "server.port";
460
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
+ };
461
600
  /**
462
601
  * Runtype's extension vocabulary. Placement is part of the contract and the
463
602
  * reader enforces it — see the `RUNTYPE_ATTRIBUTES` doc block in the shared
@@ -475,6 +614,8 @@ declare const RUNTYPE: {
475
614
  readonly toolType: "runtype.tool.type";
476
615
  readonly turnId: "runtype.turn.id";
477
616
  readonly turnIndex: "runtype.turn.index";
617
+ readonly providerFinishReason: "runtype.provider.finish_reason";
618
+ readonly gatewayLogId: "runtype.gateway.log_id";
478
619
  };
479
620
  /**
480
621
  * `runtype.tool.type` values — the closed domain that drives display and
@@ -525,12 +666,13 @@ declare const ADAPTER_VERSION: string;
525
666
  * pipeline fights the one the customer already runs, and in a stack exporting
526
667
  * to two backends it silently wins one of those fights.
527
668
  *
528
- * 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
529
670
  * tool arguments or results, no error messages, no stack traces. Only
530
- * identifiers, structure and metrics reach the wire. Content is a later,
531
- * explicitly opted-in increment; until then the safety story needs no
532
- * qualifiers, which is the right default for a package landing inside a
533
- * 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.
534
676
  *
535
677
  * ## Composing with other instrumentations
536
678
  *
@@ -573,4 +715,4 @@ interface RuntypeFlueInstrumentationOptions extends FlueProjectionOptions {
573
715
  */
574
716
  declare function createRuntypeFlueInstrumentation(options?: RuntypeFlueInstrumentationOptions): FlueInstrumentation;
575
717
 
576
- 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
@@ -100,6 +100,13 @@ interface FlueModelResponse {
100
100
  finishReason?: string;
101
101
  /** 2.x only. The provider's raw finish value before normalization. */
102
102
  providerFinishReason?: string;
103
+ /**
104
+ * The response's own gateway log id (e.g. Cloudflare AI Gateway's
105
+ * `cf-aig-log-id`), for correlating a specific turn with its entry in the
106
+ * gateway dashboard. Telemetry only — present only when the provider records
107
+ * one. The Workers AI provider attaches it today.
108
+ */
109
+ gatewayLogId?: string;
103
110
  error?: FlueErrorInfo;
104
111
  }
105
112
  /**
@@ -186,12 +193,14 @@ type FlueEventVariant = {
186
193
  type: 'tool_start';
187
194
  toolName: string;
188
195
  toolCallId: string;
196
+ args?: unknown;
189
197
  } | {
190
198
  type: 'tool';
191
199
  toolName: string;
192
200
  toolCallId: string;
193
201
  isError?: boolean;
194
202
  result?: unknown;
203
+ effectiveResult?: unknown;
195
204
  durationMs?: number;
196
205
  } | {
197
206
  type: 'submission_settled';
@@ -266,6 +275,117 @@ interface FlueInstrumentation {
266
275
  dispose(): void | Promise<void>;
267
276
  }
268
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
+
269
389
  /**
270
390
  * Flue observations → span intents. PURE: no `@opentelemetry/api`, no clock, no
271
391
  * I/O. `spans.ts` is the only module that touches a `Tracer`.
@@ -371,8 +491,8 @@ interface CloseSpanIntent {
371
491
  * Present when the span failed. Carries the error TYPE and the exception
372
492
  * class NAME only — never a message and never a stack. Both are content: a
373
493
  * provider error message routinely quotes the prompt back, and a stack
374
- * exposes filesystem paths and deployment layout. This release emits no
375
- * 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.
376
496
  */
377
497
  error?: {
378
498
  type: string;
@@ -395,6 +515,13 @@ interface FlueProjectionOptions {
395
515
  * runs inside the delegating agent's trace, and one trace is one execution.
396
516
  */
397
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;
398
525
  }
399
526
  /**
400
527
  * Resource attributes for the customer's `Resource`. Exported rather than set
@@ -458,6 +585,18 @@ declare const GEN_AI: {
458
585
  readonly serverAddress: "server.address";
459
586
  readonly serverPort: "server.port";
460
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
+ };
461
600
  /**
462
601
  * Runtype's extension vocabulary. Placement is part of the contract and the
463
602
  * reader enforces it — see the `RUNTYPE_ATTRIBUTES` doc block in the shared
@@ -475,6 +614,8 @@ declare const RUNTYPE: {
475
614
  readonly toolType: "runtype.tool.type";
476
615
  readonly turnId: "runtype.turn.id";
477
616
  readonly turnIndex: "runtype.turn.index";
617
+ readonly providerFinishReason: "runtype.provider.finish_reason";
618
+ readonly gatewayLogId: "runtype.gateway.log_id";
478
619
  };
479
620
  /**
480
621
  * `runtype.tool.type` values — the closed domain that drives display and
@@ -525,12 +666,13 @@ declare const ADAPTER_VERSION: string;
525
666
  * pipeline fights the one the customer already runs, and in a stack exporting
526
667
  * to two backends it silently wins one of those fights.
527
668
  *
528
- * 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
529
670
  * tool arguments or results, no error messages, no stack traces. Only
530
- * identifiers, structure and metrics reach the wire. Content is a later,
531
- * explicitly opted-in increment; until then the safety story needs no
532
- * qualifiers, which is the right default for a package landing inside a
533
- * 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.
534
676
  *
535
677
  * ## Composing with other instrumentations
536
678
  *
@@ -573,4 +715,4 @@ interface RuntypeFlueInstrumentationOptions extends FlueProjectionOptions {
573
715
  */
574
716
  declare function createRuntypeFlueInstrumentation(options?: RuntypeFlueInstrumentationOptions): FlueInstrumentation;
575
717
 
576
- 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.1";
121
+ var version = "0.3.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";
@@ -85,7 +164,9 @@ var RUNTYPE = {
85
164
  toolsReported: "runtype.tools.reported",
86
165
  toolType: "runtype.tool.type",
87
166
  turnId: "runtype.turn.id",
88
- turnIndex: "runtype.turn.index"
167
+ turnIndex: "runtype.turn.index",
168
+ providerFinishReason: "runtype.provider.finish_reason",
169
+ gatewayLogId: "runtype.gateway.log_id"
89
170
  };
90
171
  var FLUE = {
91
172
  instanceId: "flue.instance.id",
@@ -138,6 +219,7 @@ function runtypeFlueResourceAttributes(resource) {
138
219
  };
139
220
  }
140
221
  function createFlueProjection(options = {}) {
222
+ const content = resolveContentPolicy(options.content);
141
223
  const open = /* @__PURE__ */ new Map();
142
224
  const envelopes = /* @__PURE__ */ new Map();
143
225
  const envelopeByChain = /* @__PURE__ */ new Map();
@@ -308,7 +390,12 @@ function createFlueProjection(options = {}) {
308
390
  const ref = compactionKey(event);
309
391
  if (open.has(ref)) return [];
310
392
  const parentRef = resolveParentRef(event);
311
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
393
+ open.set(ref, {
394
+ ref,
395
+ ...parentRef ? { parentRef } : {},
396
+ chainKey: chainKey(event),
397
+ isTask: false
398
+ });
312
399
  const reason = readString(event, "reason");
313
400
  return [
314
401
  {
@@ -346,7 +433,12 @@ function createFlueProjection(options = {}) {
346
433
  const purpose = readString(event, "purpose") ?? "agent";
347
434
  const compactionRef = compactionKey(event);
348
435
  const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
349
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
436
+ open.set(ref, {
437
+ ref,
438
+ ...parentRef ? { parentRef } : {},
439
+ chainKey: chainKey(event),
440
+ isTask: false
441
+ });
350
442
  const envelopeRef = resolveEnvelopeRef(event);
351
443
  const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
352
444
  let position;
@@ -374,7 +466,8 @@ function createFlueProjection(options = {}) {
374
466
  if (position) turnPositions.set(ref, position);
375
467
  }
376
468
  if (envelope) {
377
- if (!envelope.modelPinned && purpose === "agent") envelope.requestModel = request.requestedModel;
469
+ if (!envelope.modelPinned && purpose === "agent")
470
+ envelope.requestModel = request.requestedModel;
378
471
  if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
379
472
  }
380
473
  return [
@@ -426,7 +519,17 @@ function createFlueProjection(options = {}) {
426
519
  ...response.responseModel ? { [GEN_AI.responseModel]: response.responseModel } : {},
427
520
  ...response.responseId ? { [GEN_AI.responseId]: response.responseId } : {},
428
521
  ...response.finishReason ? { [GEN_AI.finishReasons]: [response.finishReason] } : {},
429
- ...usageAttributes(response.usage)
522
+ ...usageAttributes(response.usage),
523
+ // Provider-diagnostic pointers, emitted only when Flue actually provides
524
+ // them. Both are optional and provider-dependent (Workers AI attaches
525
+ // both today); an absent value costs one column, a synthesized one would
526
+ // render as a measurement. `gatewayLogId` is a pointer to the content
527
+ // without shipping the content — a customer can click through to that
528
+ // exact request in their own AI Gateway dashboard. `providerFinishReason`
529
+ // is the provider's exact finish value before normalization, which our
530
+ // `GEN_AI.finishReasons` above deliberately hides.
531
+ ...response.providerFinishReason ? { [RUNTYPE.providerFinishReason]: response.providerFinishReason } : {},
532
+ ...response.gatewayLogId ? { [RUNTYPE.gatewayLogId]: response.gatewayLogId } : {}
430
533
  };
431
534
  const intents = [];
432
535
  if (Object.keys(attributes).length > 0) intents.push({ kind: "update", ref, attributes });
@@ -441,7 +544,12 @@ function createFlueProjection(options = {}) {
441
544
  const ref = toolKey(event);
442
545
  if (open.has(ref)) return [];
443
546
  const parentRef = resolveParentRef(event);
444
- open.set(ref, { ref, ...parentRef ? { parentRef } : {}, chainKey: chainKey(event), isTask: false });
547
+ open.set(ref, {
548
+ ref,
549
+ ...parentRef ? { parentRef } : {},
550
+ chainKey: chainKey(event),
551
+ isTask: false
552
+ });
445
553
  const position = turnPositions.get(turnKey(event));
446
554
  if (isCallerShellCall(toolName, event)) {
447
555
  return [
@@ -465,6 +573,10 @@ function createFlueProjection(options = {}) {
465
573
  }
466
574
  const isDelegation = isTaskDelegationTool(toolName, event);
467
575
  const toolType = resolveToolType(toolName, event);
576
+ const args = content.toolArguments ? encodeContentValue(readUnknown(event, "args"), content, {
577
+ kind: "arguments",
578
+ toolName
579
+ }) : void 0;
468
580
  return [
469
581
  {
470
582
  kind: "open",
@@ -478,6 +590,7 @@ function createFlueProjection(options = {}) {
478
590
  [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
479
591
  [GEN_AI.toolName]: toolName,
480
592
  [GEN_AI.toolCallId]: toolCallId,
593
+ ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
481
594
  // The semconv `gen_ai.tool.type` domain is the transport-level one
482
595
  // (`function` / `extension` / `datastore`); a sub-agent delegation is
483
596
  // not a plain function call, so it is left unclaimed there while
@@ -496,9 +609,22 @@ function createFlueProjection(options = {}) {
496
609
  const ref = toolKey(event);
497
610
  if (!open.has(ref)) return [];
498
611
  const intents = sweepDescendants(ref, event.timestamp);
499
- intents.push(
500
- closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
501
- );
612
+ const isError = readBoolean(event, "isError") === true;
613
+ const toolName = readString(event, "toolName");
614
+ if (content.toolResults && !isError && toolName && !isCallerShellCall(toolName, event)) {
615
+ const result = encodeContentValue(readToolResult(event), content, {
616
+ kind: "result",
617
+ toolName
618
+ });
619
+ if (result !== void 0) {
620
+ intents.push({
621
+ kind: "update",
622
+ ref,
623
+ attributes: { [GEN_AI_CONTENT.toolCallResult]: result }
624
+ });
625
+ }
626
+ }
627
+ intents.push(closeIntent(ref, event, isError, readErrorInfo(event)));
502
628
  open.delete(ref);
503
629
  return intents;
504
630
  }
@@ -690,6 +816,13 @@ function readErrorInfo(event) {
690
816
  const detail = event.errorInfo;
691
817
  return detail !== null && typeof detail === "object" ? detail : void 0;
692
818
  }
819
+ function readUnknown(event, key) {
820
+ return event[key];
821
+ }
822
+ function readToolResult(event) {
823
+ const record = event;
824
+ return Object.prototype.hasOwnProperty.call(record, "effectiveResult") ? record.effectiveResult : record.result;
825
+ }
693
826
  function readString(event, key) {
694
827
  const value = event[key];
695
828
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -706,7 +839,12 @@ function positiveNumber(value) {
706
839
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
707
840
  }
708
841
  function chainKey(value) {
709
- return identityKey("chain", [value.instanceId, value.harness, value.conversationId, value.session]);
842
+ return identityKey("chain", [
843
+ value.instanceId,
844
+ value.harness,
845
+ value.conversationId,
846
+ value.session
847
+ ]);
710
848
  }
711
849
  function operationKey(value) {
712
850
  return identityKey("operation", [
@@ -900,7 +1038,10 @@ function readIdField(operation, key) {
900
1038
  export {
901
1039
  ADAPTER_NAME,
902
1040
  ADAPTER_VERSION,
1041
+ DEFAULT_CONTENT_MAX_CHARS,
903
1042
  GEN_AI,
1043
+ GEN_AI_CONTENT,
1044
+ INGEST_CONTENT_ATTRIBUTE_CEILING,
904
1045
  RUNTYPE,
905
1046
  RUNTYPE_FLUE_INSTRUMENTATION_KEY,
906
1047
  RUNTYPE_SCHEMA_VERSION,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@runtypelabs/flue-otel",
3
- "version": "0.2.1",
3
+ "version": "0.3.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",
@@ -34,7 +34,8 @@
34
34
  "tsup": "^8.0.2",
35
35
  "typescript": "^6.0.3",
36
36
  "vitest": "^4.1.0",
37
- "@runtypelabs/shared": "3.27.2"
37
+ "@runtypelabs/execution-ingest": "0.6.12",
38
+ "@runtypelabs/shared": "3.32.0"
38
39
  },
39
40
  "publishConfig": {
40
41
  "access": "public"