@runtypelabs/flue-otel 0.2.2 → 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
 
@@ -179,7 +185,7 @@ backends.
179
185
  | -------------------------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
180
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 |
181
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) |
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 |
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 |
183
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
@@ -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.2";
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";
@@ -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();
@@ -343,7 +426,12 @@ function createFlueProjection(options = {}) {
343
426
  const ref = compactionKey(event);
344
427
  if (open.has(ref)) return [];
345
428
  const parentRef = resolveParentRef(event);
346
- 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
+ });
347
435
  const reason = readString(event, "reason");
348
436
  return [
349
437
  {
@@ -381,7 +469,12 @@ function createFlueProjection(options = {}) {
381
469
  const purpose = readString(event, "purpose") ?? "agent";
382
470
  const compactionRef = compactionKey(event);
383
471
  const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
384
- 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
+ });
385
478
  const envelopeRef = resolveEnvelopeRef(event);
386
479
  const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
387
480
  let position;
@@ -409,7 +502,8 @@ function createFlueProjection(options = {}) {
409
502
  if (position) turnPositions.set(ref, position);
410
503
  }
411
504
  if (envelope) {
412
- if (!envelope.modelPinned && purpose === "agent") envelope.requestModel = request.requestedModel;
505
+ if (!envelope.modelPinned && purpose === "agent")
506
+ envelope.requestModel = request.requestedModel;
413
507
  if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
414
508
  }
415
509
  return [
@@ -486,7 +580,12 @@ function createFlueProjection(options = {}) {
486
580
  const ref = toolKey(event);
487
581
  if (open.has(ref)) return [];
488
582
  const parentRef = resolveParentRef(event);
489
- 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
+ });
490
589
  const position = turnPositions.get(turnKey(event));
491
590
  if (isCallerShellCall(toolName, event)) {
492
591
  return [
@@ -510,6 +609,10 @@ function createFlueProjection(options = {}) {
510
609
  }
511
610
  const isDelegation = isTaskDelegationTool(toolName, event);
512
611
  const toolType = resolveToolType(toolName, event);
612
+ const args = content.toolArguments ? encodeContentValue(readUnknown(event, "args"), content, {
613
+ kind: "arguments",
614
+ toolName
615
+ }) : void 0;
513
616
  return [
514
617
  {
515
618
  kind: "open",
@@ -523,6 +626,7 @@ function createFlueProjection(options = {}) {
523
626
  [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
524
627
  [GEN_AI.toolName]: toolName,
525
628
  [GEN_AI.toolCallId]: toolCallId,
629
+ ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
526
630
  // The semconv `gen_ai.tool.type` domain is the transport-level one
527
631
  // (`function` / `extension` / `datastore`); a sub-agent delegation is
528
632
  // not a plain function call, so it is left unclaimed there while
@@ -541,9 +645,22 @@ function createFlueProjection(options = {}) {
541
645
  const ref = toolKey(event);
542
646
  if (!open.has(ref)) return [];
543
647
  const intents = sweepDescendants(ref, event.timestamp);
544
- intents.push(
545
- closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
546
- );
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)));
547
664
  open.delete(ref);
548
665
  return intents;
549
666
  }
@@ -735,6 +852,13 @@ function readErrorInfo(event) {
735
852
  const detail = event.errorInfo;
736
853
  return detail !== null && typeof detail === "object" ? detail : void 0;
737
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
+ }
738
862
  function readString(event, key) {
739
863
  const value = event[key];
740
864
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -751,7 +875,12 @@ function positiveNumber(value) {
751
875
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
752
876
  }
753
877
  function chainKey(value) {
754
- 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
+ ]);
755
884
  }
756
885
  function operationKey(value) {
757
886
  return identityKey("operation", [
@@ -940,7 +1069,10 @@ function readIdField(operation, key) {
940
1069
  0 && (module.exports = {
941
1070
  ADAPTER_NAME,
942
1071
  ADAPTER_VERSION,
1072
+ DEFAULT_CONTENT_MAX_CHARS,
943
1073
  GEN_AI,
1074
+ GEN_AI_CONTENT,
1075
+ INGEST_CONTENT_ATTRIBUTE_CEILING,
944
1076
  RUNTYPE,
945
1077
  RUNTYPE_FLUE_INSTRUMENTATION_KEY,
946
1078
  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.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";
@@ -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();
@@ -310,7 +390,12 @@ function createFlueProjection(options = {}) {
310
390
  const ref = compactionKey(event);
311
391
  if (open.has(ref)) return [];
312
392
  const parentRef = resolveParentRef(event);
313
- 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
+ });
314
399
  const reason = readString(event, "reason");
315
400
  return [
316
401
  {
@@ -348,7 +433,12 @@ function createFlueProjection(options = {}) {
348
433
  const purpose = readString(event, "purpose") ?? "agent";
349
434
  const compactionRef = compactionKey(event);
350
435
  const parentRef = purpose !== "agent" && open.has(compactionRef) ? compactionRef : resolveParentRef(event);
351
- 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
+ });
352
442
  const envelopeRef = resolveEnvelopeRef(event);
353
443
  const envelope = envelopeRef ? envelopes.get(envelopeRef) : void 0;
354
444
  let position;
@@ -376,7 +466,8 @@ function createFlueProjection(options = {}) {
376
466
  if (position) turnPositions.set(ref, position);
377
467
  }
378
468
  if (envelope) {
379
- if (!envelope.modelPinned && purpose === "agent") envelope.requestModel = request.requestedModel;
469
+ if (!envelope.modelPinned && purpose === "agent")
470
+ envelope.requestModel = request.requestedModel;
380
471
  if (Array.isArray(request.input?.tools)) envelope.toolsReported = true;
381
472
  }
382
473
  return [
@@ -453,7 +544,12 @@ function createFlueProjection(options = {}) {
453
544
  const ref = toolKey(event);
454
545
  if (open.has(ref)) return [];
455
546
  const parentRef = resolveParentRef(event);
456
- 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
+ });
457
553
  const position = turnPositions.get(turnKey(event));
458
554
  if (isCallerShellCall(toolName, event)) {
459
555
  return [
@@ -477,6 +573,10 @@ function createFlueProjection(options = {}) {
477
573
  }
478
574
  const isDelegation = isTaskDelegationTool(toolName, event);
479
575
  const toolType = resolveToolType(toolName, event);
576
+ const args = content.toolArguments ? encodeContentValue(readUnknown(event, "args"), content, {
577
+ kind: "arguments",
578
+ toolName
579
+ }) : void 0;
480
580
  return [
481
581
  {
482
582
  kind: "open",
@@ -490,6 +590,7 @@ function createFlueProjection(options = {}) {
490
590
  [GEN_AI.operationName]: GEN_AI_OPERATION_EXECUTE_TOOL,
491
591
  [GEN_AI.toolName]: toolName,
492
592
  [GEN_AI.toolCallId]: toolCallId,
593
+ ...args !== void 0 ? { [GEN_AI_CONTENT.toolCallArguments]: args } : {},
493
594
  // The semconv `gen_ai.tool.type` domain is the transport-level one
494
595
  // (`function` / `extension` / `datastore`); a sub-agent delegation is
495
596
  // not a plain function call, so it is left unclaimed there while
@@ -508,9 +609,22 @@ function createFlueProjection(options = {}) {
508
609
  const ref = toolKey(event);
509
610
  if (!open.has(ref)) return [];
510
611
  const intents = sweepDescendants(ref, event.timestamp);
511
- intents.push(
512
- closeIntent(ref, event, readBoolean(event, "isError") === true, readErrorInfo(event))
513
- );
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)));
514
628
  open.delete(ref);
515
629
  return intents;
516
630
  }
@@ -702,6 +816,13 @@ function readErrorInfo(event) {
702
816
  const detail = event.errorInfo;
703
817
  return detail !== null && typeof detail === "object" ? detail : void 0;
704
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
+ }
705
826
  function readString(event, key) {
706
827
  const value = event[key];
707
828
  return typeof value === "string" && value.length > 0 ? value : void 0;
@@ -718,7 +839,12 @@ function positiveNumber(value) {
718
839
  return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
719
840
  }
720
841
  function chainKey(value) {
721
- 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
+ ]);
722
848
  }
723
849
  function operationKey(value) {
724
850
  return identityKey("operation", [
@@ -912,7 +1038,10 @@ function readIdField(operation, key) {
912
1038
  export {
913
1039
  ADAPTER_NAME,
914
1040
  ADAPTER_VERSION,
1041
+ DEFAULT_CONTENT_MAX_CHARS,
915
1042
  GEN_AI,
1043
+ GEN_AI_CONTENT,
1044
+ INGEST_CONTENT_ATTRIBUTE_CEILING,
916
1045
  RUNTYPE,
917
1046
  RUNTYPE_FLUE_INSTRUMENTATION_KEY,
918
1047
  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.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.28.0"
37
+ "@runtypelabs/execution-ingest": "0.6.12",
38
+ "@runtypelabs/shared": "3.32.0"
38
39
  },
39
40
  "publishConfig": {
40
41
  "access": "public"