@telemetry-dev/bedrock 0.1.10 → 0.1.12

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  First-class AWS Bedrock instrumentation for the telemetry.dev TypeScript SDK.
4
4
 
5
+ Docs: https://telemetry.dev/docs/integrations/bedrock
6
+
5
7
  This package instruments AWS SDK for JavaScript v3 clients for:
6
8
 
7
9
  - `@aws-sdk/client-bedrock-runtime`
@@ -29,15 +31,18 @@ pnpm add @telemetry-dev/sdk @telemetry-dev/bedrock @aws-sdk/client-bedrock-runti
29
31
  pnpm add @aws-sdk/client-bedrock-agent-runtime
30
32
  ```
31
33
 
34
+ Requires `@aws-sdk/client-bedrock-runtime` 3.600.0 or a later 3.x release (and the same range for the optional `@aws-sdk/client-bedrock-agent-runtime`), and `@telemetry-dev/sdk`, which needs Node.js 20.19.0 or later.
35
+
32
36
  ## Quickstart
33
37
 
38
+ `init()` reads your project API key from the `TELEMETRY_DEV_API_KEY` environment variable, so set it before you start the app.
39
+
34
40
  ```ts
35
41
  import { BedrockRuntimeClient, ConverseCommand } from "@aws-sdk/client-bedrock-runtime";
36
42
  import { init, shutdown } from "@telemetry-dev/sdk";
37
43
  import { wrapBedrock } from "@telemetry-dev/bedrock";
38
44
 
39
45
  init({
40
- apiKey: process.env.TELEMETRY_DEV_API_KEY,
41
46
  serviceName: "bedrock-app",
42
47
  environment: "production",
43
48
  });
@@ -98,9 +103,22 @@ instrumentBedrockAgents({ captureAgentTrace: false });
98
103
 
99
104
  ## Options
100
105
 
101
- | Option | Default | Applies to | Notes |
102
- | ------------------- | ------- | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
103
- | `captureAgentTrace` | `false` | Agent Runtime streams | Aggregates trace usage and counts by default. When enabled, also attaches raw trace events as `td.metadata.agent_trace` after SDK masking/truncation. |
106
+ | Option | Default | Applies to | Notes |
107
+ | ------------------- | ------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
108
+ | `captureAgentTrace` | `false` | Agent Runtime streams | Aggregates trace usage and counts by default. When enabled, also attaches trace events as `td.metadata.agent_trace`, filtered as described below. |
109
+
110
+ `td.metadata.agent_trace` is truncated but never passed to your `mask`, so each trace event is rebuilt from an explicit allowlist instead:
111
+
112
+ - Envelope fields: `agentId`, `agentAliasId`, `agentVersion`, `sessionId`, `collaboratorName`, `eventTime`, and the alias ARNs in `callerChain`.
113
+ - Content the agent sends to a model, tool, or guardrail (`modelInvocationInput`, `invocationInput`, `inputAssessments`) is kept as received when `captureInput` is on.
114
+ - Content that comes back (`modelInvocationOutput`, `rationale`, `observation`, `outputAssessments`, custom orchestration `event`) is kept as received when `captureOutput` is on.
115
+ - A failure reason (`failureTrace.failureReason`, and `td.metadata.failure_reason`) is free-form text, so it is kept only when both are on. `td.metadata.failure` always records that the invocation failed, with `td.metadata.failure_code` when AWS sends one.
116
+ - When a `mask` is set, all of this content is withheld.
117
+ - Withheld content and the trace structure around it keep only trace ids, step types, the foundation model, guardrail action, failure code, and the usage, timings, and `clientRequestId` from AWS's trace `metadata`.
118
+
119
+ Any other field, including unknown trace types and the AWS SDK's `$unknown` union member, is omitted even when both capture options are on; a content part that is kept is exported whole, including any fields inside it that are not listed here. Errors are not covered by this filtering: AWS exceptions, including errors raised inside the stream, still record the provider's error message on the span.
120
+
121
+ Output file names in `td.metadata.output_files` follow `captureOutput` the same way, and are withheld when a `mask` is set; file types are always kept.
104
122
 
105
123
  ## Signal coverage
106
124
 
@@ -159,3 +177,7 @@ The following calls pass through unless a supported operation above is used:
159
177
  - `AgenticRetrieveStream`
160
178
 
161
179
  Provider-native `InvokeModel` usage is best-effort. Anthropic Claude, Amazon Titan/Nova, Meta Llama, and Titan embeddings expose known token fields. Cohere and Mistral text models do not consistently include usage in the JSON body; spans still capture request/response bodies and finish reasons where present.
180
+
181
+ ## License
182
+
183
+ MIT. See [LICENSE](https://github.com/telemetry-dev/telemetry/blob/main/packages/bedrock/LICENSE).
package/dist/agents.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { r as createInstrumentedSend, t as AGENT_HANDLERS } from "./internal-handlers-PftQAPH3.mjs";
1
+ import { r as createInstrumentedSend, t as AGENT_HANDLERS } from "./internal-handlers-CLP2wXDh.mjs";
2
2
  import { BedrockAgentRuntimeClient } from "@aws-sdk/client-bedrock-agent-runtime";
3
3
  //#region src/agents.ts
4
4
  const WRAPPED = /* @__PURE__ */ new WeakSet();
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as RUNTIME_HANDLERS, r as createInstrumentedSend } from "./internal-handlers-PftQAPH3.mjs";
1
+ import { n as RUNTIME_HANDLERS, r as createInstrumentedSend } from "./internal-handlers-CLP2wXDh.mjs";
2
2
  import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime";
3
3
  //#region src/index.ts
4
4
  const WRAPPED = /* @__PURE__ */ new WeakSet();
@@ -413,6 +413,8 @@ var AgentStreamState = class {
413
413
  textRejected = false;
414
414
  usage = {};
415
415
  traceEventCount = 0;
416
+ failed = false;
417
+ failureCode;
416
418
  failureReason;
417
419
  guardrailAction;
418
420
  returnControl;
@@ -423,6 +425,10 @@ var AgentStreamState = class {
423
425
  errorFields;
424
426
  citationCount = 0;
425
427
  outputFiles = [];
428
+ contentCapture = {
429
+ input: captureEnabled("input") && !captureMaskedBeforeStart(),
430
+ output: captureEnabled("output") && !captureMaskedBeforeStart()
431
+ };
426
432
  constructor(options) {
427
433
  this.options = options;
428
434
  }
@@ -442,7 +448,7 @@ var AgentStreamState = class {
442
448
  else this.textRejected = true;
443
449
  const citations = arrayValue((isRecord(chunk?.attribution) ? chunk.attribution : void 0)?.citations);
444
450
  if (citations) this.citationCount += citations.length;
445
- const sanitizedFiles = (isRecord(raw.files) ? arrayValue(raw.files.files) : void 0)?.map(sanitizeOutputFile);
451
+ const sanitizedFiles = (isRecord(raw.files) ? arrayValue(raw.files.files) : void 0)?.map((file) => sanitizeOutputFile(file, this.contentCapture.output));
446
452
  if (sanitizedFiles && this.retention.accept(sanitizedFiles)) this.outputFiles.push(...sanitizedFiles);
447
453
  const returnControl = isRecord(raw.returnControl) ? raw.returnControl : void 0;
448
454
  if (returnControl) {
@@ -457,11 +463,23 @@ var AgentStreamState = class {
457
463
  const traceEvent = isRecord(raw.trace) ? raw.trace : void 0;
458
464
  if (traceEvent) {
459
465
  this.traceEventCount += 1;
460
- if (this.options.captureAgentTrace && this.retention.accept(traceEvent)) this.traces.push(traceEvent);
461
- collectTrace(traceEvent, (usage) => this.addUsage(usage));
462
- const failure = findKey(traceEvent, "failureTrace");
463
- if (isRecord(failure)) this.failureReason = stringValue(failure.failureReason) ?? this.failureReason;
464
- const guardrail = findKey(traceEvent, "guardrailTrace");
466
+ if (this.options.captureAgentTrace) {
467
+ const retained = projectTrace(traceEvent, TRACE_PART, this.contentCapture);
468
+ if (this.retention.accept(retained)) this.traces.push(retained);
469
+ }
470
+ const trace = isRecord(traceEvent.trace) ? traceEvent.trace : {};
471
+ for (const step of MODEL_INVOCATION_STEPS) {
472
+ const member = isRecord(trace[step]) ? trace[step] : void 0;
473
+ const output = isRecord(member?.modelInvocationOutput) ? member.modelInvocationOutput : {};
474
+ this.addUsage(isRecord(output.metadata) ? output.metadata.usage : void 0);
475
+ }
476
+ const failure = trace.failureTrace;
477
+ if (isRecord(failure)) {
478
+ this.failed = true;
479
+ this.failureCode = numberValue(failure.failureCode) ?? this.failureCode;
480
+ if (this.contentCapture.input && this.contentCapture.output) this.failureReason = stringValue(failure.failureReason) ?? this.failureReason;
481
+ }
482
+ const guardrail = trace.guardrailTrace;
465
483
  if (isRecord(guardrail)) this.guardrailAction = stringValue(guardrail.action) ?? this.guardrailAction;
466
484
  }
467
485
  }
@@ -472,6 +490,8 @@ var AgentStreamState = class {
472
490
  finish() {
473
491
  const metadata = omitUndefined({
474
492
  trace_event_count: this.traceEventCount || void 0,
493
+ failure: this.failed ? true : void 0,
494
+ failure_code: this.failureCode,
475
495
  failure_reason: this.failureReason,
476
496
  guardrail_action: this.guardrailAction,
477
497
  return_control: this.sawReturnControl ? true : void 0,
@@ -504,11 +524,11 @@ var AgentStreamState = class {
504
524
  else if (inputTokens !== void 0 || outputTokens !== void 0) this.usage.totalTokens = (this.usage.inputTokens ?? 0) + (this.usage.outputTokens ?? 0);
505
525
  }
506
526
  };
507
- function sanitizeOutputFile(value) {
527
+ function sanitizeOutputFile(value, keepName) {
508
528
  const raw = value;
509
529
  if (!isRecord(raw)) return void 0;
510
530
  return omitUndefined({
511
- name: stringValue(raw.name),
531
+ name: keepName ? stringValue(raw.name) : void 0,
512
532
  type: stringValue(raw.type)
513
533
  });
514
534
  }
@@ -700,22 +720,111 @@ function flowName(input) {
700
720
  function ragName(input) {
701
721
  return `retrieve_and_generate ${basenameArn(ragModel(input)) ?? "knowledge-base"}`;
702
722
  }
703
- function collectTrace(value, onUsage) {
704
- const raw = value;
705
- if (!isRecord(raw)) return;
706
- const output = isRecord(raw.modelInvocationOutput) ? raw.modelInvocationOutput : void 0;
707
- if (output) onUsage((isRecord(output.metadata) ? output.metadata : void 0)?.usage);
708
- for (const child of Object.values(raw)) if (isRecord(child)) collectTrace(child, onUsage);
709
- else if (Array.isArray(child)) child.forEach((item) => collectTrace(item, onUsage));
710
- }
711
- function findKey(value, key) {
712
- const raw = value;
713
- if (!isRecord(raw)) return void 0;
714
- if (raw[key] !== void 0) return raw[key];
715
- for (const child of Object.values(raw)) {
716
- const found = findKey(child, key);
717
- if (found !== void 0) return found;
723
+ /** Content kept as received when every listed capture is on; otherwise reduced to `withheld`. */
724
+ var TraceContent = class {
725
+ constructor(needs, withheld) {
726
+ this.needs = needs;
727
+ this.withheld = withheld;
718
728
  }
729
+ };
730
+ const SCALAR = "scalar";
731
+ const TRACE_METADATA = {
732
+ usage: {
733
+ inputTokens: SCALAR,
734
+ outputTokens: SCALAR
735
+ },
736
+ startTime: SCALAR,
737
+ endTime: SCALAR,
738
+ totalTimeMs: SCALAR,
739
+ operationTotalTimeMs: SCALAR,
740
+ clientRequestId: SCALAR
741
+ };
742
+ const MODEL_INVOCATION_INPUT = new TraceContent(["input"], {
743
+ traceId: SCALAR,
744
+ type: SCALAR,
745
+ foundationModel: SCALAR
746
+ });
747
+ const INVOCATION_INPUT = new TraceContent(["input"], {
748
+ traceId: SCALAR,
749
+ invocationType: SCALAR
750
+ });
751
+ const MODEL_INVOCATION_OUTPUT = new TraceContent(["output"], {
752
+ traceId: SCALAR,
753
+ metadata: TRACE_METADATA
754
+ });
755
+ const OBSERVATION = new TraceContent(["output"], {
756
+ traceId: SCALAR,
757
+ type: SCALAR
758
+ });
759
+ const MODEL_INVOCATION_STEPS = [
760
+ "preProcessingTrace",
761
+ "orchestrationTrace",
762
+ "postProcessingTrace",
763
+ "routingClassifierTrace"
764
+ ];
765
+ const PROCESSING_TRACE = {
766
+ modelInvocationInput: MODEL_INVOCATION_INPUT,
767
+ modelInvocationOutput: MODEL_INVOCATION_OUTPUT
768
+ };
769
+ const TRACE_PART = {
770
+ agentId: SCALAR,
771
+ agentAliasId: SCALAR,
772
+ agentVersion: SCALAR,
773
+ sessionId: SCALAR,
774
+ collaboratorName: SCALAR,
775
+ eventTime: SCALAR,
776
+ callerChain: [{ agentAliasArn: SCALAR }],
777
+ trace: {
778
+ preProcessingTrace: PROCESSING_TRACE,
779
+ postProcessingTrace: PROCESSING_TRACE,
780
+ orchestrationTrace: {
781
+ modelInvocationInput: MODEL_INVOCATION_INPUT,
782
+ invocationInput: INVOCATION_INPUT,
783
+ modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
784
+ rationale: new TraceContent(["output"], { traceId: SCALAR }),
785
+ observation: OBSERVATION
786
+ },
787
+ routingClassifierTrace: {
788
+ modelInvocationInput: MODEL_INVOCATION_INPUT,
789
+ invocationInput: INVOCATION_INPUT,
790
+ modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
791
+ observation: OBSERVATION
792
+ },
793
+ customOrchestrationTrace: {
794
+ traceId: SCALAR,
795
+ event: new TraceContent(["output"])
796
+ },
797
+ guardrailTrace: {
798
+ traceId: SCALAR,
799
+ action: SCALAR,
800
+ metadata: TRACE_METADATA,
801
+ inputAssessments: new TraceContent(["input"]),
802
+ outputAssessments: new TraceContent(["output"])
803
+ },
804
+ failureTrace: {
805
+ traceId: SCALAR,
806
+ failureCode: SCALAR,
807
+ metadata: TRACE_METADATA,
808
+ failureReason: new TraceContent(["input", "output"])
809
+ }
810
+ }
811
+ };
812
+ function projectTrace(value, shape, capture) {
813
+ if (shape instanceof TraceContent) {
814
+ if (shape.needs.every((kind) => capture[kind])) return value ?? void 0;
815
+ return shape.withheld === void 0 ? void 0 : projectTrace(value, shape.withheld, capture);
816
+ }
817
+ if (shape === SCALAR) return [
818
+ "string",
819
+ "number",
820
+ "boolean"
821
+ ].includes(typeof value) || value instanceof Date ? value : void 0;
822
+ if (Array.isArray(shape)) {
823
+ if (!Array.isArray(value)) return void 0;
824
+ return value.map((item) => projectTrace(item, shape[0], capture)).filter((item) => item !== void 0);
825
+ }
826
+ if (!isRecord(value)) return void 0;
827
+ return omitUndefined(Object.fromEntries(Object.entries(shape).map(([field, child]) => [field, projectTrace(value[field], child, capture)])));
719
828
  }
720
829
  //#endregion
721
830
  //#region src/converse.ts
@@ -1147,12 +1256,7 @@ function nativeResponseFields(model, body, metadata) {
1147
1256
  return withHeaderUsage({
1148
1257
  output: body,
1149
1258
  finishReason: stringValue(body.stop_reason),
1150
- usage: omitUndefined({
1151
- inputTokens: numberValue(usage.input_tokens),
1152
- outputTokens: numberValue(usage.output_tokens),
1153
- cacheReadInputTokens: numberValue(usage.cache_read_input_tokens),
1154
- cacheCreationInputTokens: numberValue(usage.cache_creation_input_tokens)
1155
- })
1259
+ ...claudeUsageFields(usage)
1156
1260
  }, metadata);
1157
1261
  }
1158
1262
  if (lower.includes("meta.llama")) return omitUndefined({
@@ -1177,6 +1281,35 @@ function nativeResponseFields(model, body, metadata) {
1177
1281
  }, metadata);
1178
1282
  return withHeaderUsage({ output: body }, metadata);
1179
1283
  }
1284
+ const CLAUDE_USAGE_KEYS = {
1285
+ inputTokens: "input_tokens",
1286
+ outputTokens: "output_tokens",
1287
+ cacheReadInputTokens: "cache_read_input_tokens",
1288
+ cacheCreationInputTokens: "cache_creation_input_tokens"
1289
+ };
1290
+ function claudeUsageFields(record) {
1291
+ const usage = {};
1292
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
1293
+ const value = numberValue(record[source]);
1294
+ if (value !== void 0) usage[key] = value;
1295
+ }
1296
+ const iterations = record.iterations;
1297
+ if (!Array.isArray(iterations)) return { usage };
1298
+ const first = Math.max(0, iterations.length - STREAM_CAPTURE_MAX_ITEMS);
1299
+ for (let index = first; index < iterations.length; index += 1) {
1300
+ const entry = iterations[index];
1301
+ if (!isRecord(entry) || entry.type !== "compaction") continue;
1302
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
1303
+ const value = numberValue(entry[source]);
1304
+ const target = key;
1305
+ if (value !== void 0) usage[target] = (usage[target] ?? 0) + value;
1306
+ }
1307
+ }
1308
+ return {
1309
+ usage,
1310
+ ...first > 0 ? { attributes: { "telemetry.dev.capture.truncated": true } } : {}
1311
+ };
1312
+ }
1180
1313
  function withHeaderUsage(fields, metadata) {
1181
1314
  if (fields.usage && Object.keys(fields.usage).length > 0) return fields;
1182
1315
  return mergeFields(fields, headerUsageFields(metadata));
@@ -1202,6 +1335,9 @@ var InvokeModelStreamState = class {
1202
1335
  chunks = [];
1203
1336
  text = "";
1204
1337
  usage;
1338
+ finalUsage = {};
1339
+ startUsage = {};
1340
+ compaction = {};
1205
1341
  finishReason;
1206
1342
  error;
1207
1343
  errorFields;
@@ -1228,21 +1364,44 @@ var InvokeModelStreamState = class {
1228
1364
  const message = isRecord(parsed.message) ? parsed.message : void 0;
1229
1365
  const messageUsage = isRecord(message?.usage) ? message?.usage : void 0;
1230
1366
  const usage = isRecord(parsed.usage) ? parsed.usage : void 0;
1367
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
1368
+ const value = numberValue(usage?.[source]);
1369
+ if (value !== void 0) this.finalUsage[key] = value;
1370
+ }
1371
+ this.startUsage = omitUndefined({
1372
+ ...this.startUsage,
1373
+ inputTokens: numberValue(messageUsage?.input_tokens) ?? this.startUsage.inputTokens,
1374
+ outputTokens: numberValue(messageUsage?.output_tokens) ?? this.startUsage.outputTokens,
1375
+ cacheReadInputTokens: numberValue(messageUsage?.cache_read_input_tokens) ?? this.startUsage.cacheReadInputTokens,
1376
+ cacheCreationInputTokens: numberValue(messageUsage?.cache_creation_input_tokens) ?? this.startUsage.cacheCreationInputTokens
1377
+ });
1378
+ const iterations = Array.isArray(usage?.iterations) ? usage.iterations : messageUsage?.iterations;
1379
+ if (Array.isArray(iterations)) this.compaction = claudeUsageFields({ iterations });
1231
1380
  const metrics = isRecord(parsed["amazon-bedrock-invocationMetrics"]) ? parsed["amazon-bedrock-invocationMetrics"] : void 0;
1232
1381
  const outputs = Array.isArray(parsed.outputs) && isRecord(parsed.outputs[0]) ? parsed.outputs[0] : void 0;
1233
1382
  const generation = Array.isArray(parsed.generations) && isRecord(parsed.generations[0]) ? parsed.generations[0] : void 0;
1234
1383
  this.finishReason = stringValue(delta?.stop_reason) ?? stringValue(parsed.stop_reason) ?? stringValue(outputs?.stop_reason) ?? stringValue(generation?.finish_reason) ?? stringValue(parsed.completionReason) ?? this.finishReason;
1235
1384
  this.usage = omitUndefined({
1236
1385
  ...this.usage,
1237
- inputTokens: numberValue(messageUsage?.input_tokens) ?? numberValue(metrics?.inputTokenCount) ?? numberValue(parsed.inputTextTokenCount) ?? this.usage?.inputTokens,
1386
+ inputTokens: numberValue(usage?.input_tokens) ?? numberValue(metrics?.inputTokenCount) ?? numberValue(parsed.inputTextTokenCount) ?? this.usage?.inputTokens,
1238
1387
  outputTokens: numberValue(usage?.output_tokens) ?? numberValue(metrics?.outputTokenCount) ?? numberValue(parsed.totalOutputTextTokenCount) ?? this.usage?.outputTokens,
1239
- cacheReadInputTokens: numberValue(messageUsage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
1240
- cacheCreationInputTokens: numberValue(messageUsage?.cache_creation_input_tokens) ?? this.usage?.cacheCreationInputTokens
1388
+ cacheReadInputTokens: numberValue(usage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
1389
+ cacheCreationInputTokens: numberValue(usage?.cache_creation_input_tokens) ?? this.usage?.cacheCreationInputTokens
1241
1390
  });
1242
1391
  }
1243
1392
  return providerChunkHasOutput(parsed);
1244
1393
  }
1245
1394
  finish() {
1395
+ const usage = {
1396
+ ...this.startUsage,
1397
+ ...this.usage,
1398
+ ...this.finalUsage
1399
+ };
1400
+ for (const key of Object.keys(CLAUDE_USAGE_KEYS)) {
1401
+ const value = this.compaction.usage?.[key];
1402
+ const baseKey = key === "outputTokens" ? "outputTokens" : "inputTokens";
1403
+ if (value !== void 0 && (this.finalUsage[baseKey] !== void 0 || this.usage?.[baseKey] === void 0)) usage[key] = (usage[key] ?? 0) + value;
1404
+ }
1246
1405
  return mergeFields(omitUndefined({
1247
1406
  output: this.text ? [{
1248
1407
  role: "assistant",
@@ -1253,7 +1412,8 @@ var InvokeModelStreamState = class {
1253
1412
  finish_reason: this.finishReason
1254
1413
  }] : this.chunks,
1255
1414
  finishReason: this.finishReason,
1256
- usage: this.usage,
1415
+ usage,
1416
+ attributes: this.compaction.attributes,
1257
1417
  error: this.error
1258
1418
  }), this.errorFields);
1259
1419
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telemetry-dev/bedrock",
3
- "version": "0.1.10",
3
+ "version": "0.1.12",
4
4
  "description": "AWS Bedrock instrumentation for telemetry.dev: Converse, InvokeModel, and Bedrock Agents spans for AWS SDK v3 clients.",
5
5
  "keywords": [
6
6
  "aws",
@@ -44,12 +44,12 @@
44
44
  "@aws-sdk/client-bedrock-agent-runtime": "3.1135.0",
45
45
  "@aws-sdk/client-bedrock-runtime": "3.1135.0",
46
46
  "@opentelemetry/api": "^1.9.1",
47
- "@opentelemetry/sdk-logs": "^0.218.0",
48
- "@opentelemetry/sdk-trace-base": "^2.7.1",
47
+ "@opentelemetry/sdk-logs": "^0.223.0",
48
+ "@opentelemetry/sdk-trace-base": "^2.12.0",
49
49
  "@smithy/eventstream-codec": "^4.2.5",
50
50
  "@smithy/protocol-http": "^5.3.10",
51
51
  "@smithy/types": "^4.12.0",
52
- "@telemetry-dev/sdk": "^0.1.8",
52
+ "@telemetry-dev/sdk": "^0.1.10",
53
53
  "@types/node": "^25.5.0",
54
54
  "@typescript/native-preview": "7.0.0-dev.20260328.1",
55
55
  "typescript": "^6.0.2",
@@ -59,7 +59,7 @@
59
59
  "peerDependencies": {
60
60
  "@aws-sdk/client-bedrock-agent-runtime": ">=3.600.0 <4",
61
61
  "@aws-sdk/client-bedrock-runtime": ">=3.600.0 <4",
62
- "@telemetry-dev/sdk": "^0.1.8"
62
+ "@telemetry-dev/sdk": "^0.1.10"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "@aws-sdk/client-bedrock-agent-runtime": {
@@ -5,6 +5,7 @@ import {
5
5
  awsMetadataFields,
6
6
  basenameArn,
7
7
  bytesToString,
8
+ captureMaskedBeforeStart,
8
9
  isRecord,
9
10
  mergeFields,
10
11
  modeledStreamError,
@@ -55,6 +56,8 @@ export class AgentStreamState implements StreamState {
55
56
  private textRejected = false;
56
57
  private usage: NonNullable<SpanFields["usage"]> = {};
57
58
  private traceEventCount = 0;
59
+ private failed = false;
60
+ private failureCode: number | undefined;
58
61
  private failureReason: string | undefined;
59
62
  private guardrailAction: string | undefined;
60
63
  private returnControl: unknown;
@@ -65,6 +68,12 @@ export class AgentStreamState implements StreamState {
65
68
  private errorFields: SpanFields | undefined;
66
69
  private citationCount = 0;
67
70
  private readonly outputFiles: unknown[] = [];
71
+ // Metadata bypasses the capture flags and the mask, so trace and file content is filtered here,
72
+ // and a mask that cannot see it withholds it.
73
+ private readonly contentCapture: AgentTraceCapture = {
74
+ input: captureEnabled("input") && !captureMaskedBeforeStart(),
75
+ output: captureEnabled("output") && !captureMaskedBeforeStart(),
76
+ };
68
77
 
69
78
  constructor(private readonly options: BedrockInstrumentationOptions) {}
70
79
 
@@ -90,6 +99,7 @@ export class AgentStreamState implements StreamState {
90
99
  if (this.retention.accept(piece)) this.text += piece;
91
100
  else this.textRejected = true;
92
101
  }
102
+
93
103
  const attribution = isRecord(chunk?.attribution) ? chunk.attribution : undefined;
94
104
  const citations = arrayValue<unknown>(attribution?.citations);
95
105
 
@@ -97,7 +107,9 @@ export class AgentStreamState implements StreamState {
97
107
 
98
108
  const files = isRecord(raw.files) ? arrayValue<JsonValue>(raw.files.files) : undefined;
99
109
 
100
- const sanitizedFiles = files?.map(sanitizeOutputFile);
110
+ const sanitizedFiles = files?.map((file) =>
111
+ sanitizeOutputFile(file, this.contentCapture.output),
112
+ );
101
113
 
102
114
  if (sanitizedFiles && this.retention.accept(sanitizedFiles)) {
103
115
  this.outputFiles.push(...sanitizedFiles);
@@ -125,15 +137,35 @@ export class AgentStreamState implements StreamState {
125
137
  if (traceEvent) {
126
138
  this.traceEventCount += 1;
127
139
 
128
- if (this.options.captureAgentTrace && this.retention.accept(traceEvent)) {
129
- this.traces.push(traceEvent);
140
+ if (this.options.captureAgentTrace) {
141
+ const retained = projectTrace(traceEvent, TRACE_PART, this.contentCapture);
142
+
143
+ if (this.retention.accept(retained)) this.traces.push(retained);
144
+ }
145
+
146
+ // Summaries are read only where the API puts them, so content and unknown branches can't
147
+ // feed them.
148
+ const trace = isRecord(traceEvent.trace) ? traceEvent.trace : {};
149
+
150
+ for (const step of MODEL_INVOCATION_STEPS) {
151
+ const member = isRecord(trace[step]) ? trace[step] : undefined;
152
+ const output = isRecord(member?.modelInvocationOutput) ? member.modelInvocationOutput : {};
153
+ this.addUsage(isRecord(output.metadata) ? output.metadata.usage : undefined);
130
154
  }
131
- collectTrace(traceEvent, (usage) => this.addUsage(usage));
132
- const failure = findKey(traceEvent, "failureTrace");
133
155
 
134
- if (isRecord(failure))
135
- this.failureReason = stringValue(failure.failureReason) ?? this.failureReason;
136
- const guardrail = findKey(traceEvent, "guardrailTrace");
156
+ const failure = trace.failureTrace;
157
+
158
+ if (isRecord(failure)) {
159
+ this.failed = true;
160
+ this.failureCode = numberValue(failure.failureCode) ?? this.failureCode;
161
+
162
+ // The reason is free-form text that can quote the prompt or a tool result.
163
+ if (this.contentCapture.input && this.contentCapture.output) {
164
+ this.failureReason = stringValue(failure.failureReason) ?? this.failureReason;
165
+ }
166
+ }
167
+
168
+ const guardrail = trace.guardrailTrace;
137
169
 
138
170
  if (isRecord(guardrail))
139
171
  this.guardrailAction = stringValue(guardrail.action) ?? this.guardrailAction;
@@ -150,6 +182,8 @@ export class AgentStreamState implements StreamState {
150
182
  finish(): SpanFields {
151
183
  const metadata = omitUndefined({
152
184
  trace_event_count: this.traceEventCount || undefined,
185
+ failure: this.failed ? true : undefined,
186
+ failure_code: this.failureCode,
153
187
  failure_reason: this.failureReason,
154
188
  guardrail_action: this.guardrailAction,
155
189
  return_control: this.sawReturnControl ? true : undefined,
@@ -195,13 +229,13 @@ export class AgentStreamState implements StreamState {
195
229
  }
196
230
  }
197
231
 
198
- function sanitizeOutputFile<T>(value: T): JsonValue {
232
+ function sanitizeOutputFile<T>(value: T, keepName: boolean): JsonValue {
199
233
  const raw: unknown = value;
200
234
 
201
235
  if (!isRecord(raw)) return undefined;
202
236
 
203
237
  return omitUndefined({
204
- name: stringValue(raw.name),
238
+ name: keepName ? stringValue(raw.name) : undefined,
205
239
  type: stringValue(raw.type),
206
240
  });
207
241
  }
@@ -426,6 +460,7 @@ export class FlowStreamState implements StreamState {
426
460
  if (inputRequest && this.retention.accept(inputRequest.content)) {
427
461
  this.outputs.push(inputRequest.content);
428
462
  }
463
+
429
464
  const completion = isRecord(raw.flowCompletionEvent) ? raw.flowCompletionEvent : undefined;
430
465
  this.completionReason = stringValue(completion?.completionReason) ?? this.completionReason;
431
466
  }
@@ -450,35 +485,135 @@ export function ragName(input: JsonRecord): string {
450
485
  return `retrieve_and_generate ${basenameArn(ragModel(input)) ?? "knowledge-base"}`;
451
486
  }
452
487
 
453
- function collectTrace<T>(value: T, onUsage: <TUsage>(usage: TUsage) => void): void {
454
- const raw: unknown = value;
488
+ type AgentTraceCapture = Record<"input" | "output", boolean>;
455
489
 
456
- if (!isRecord(raw)) return;
457
- const output = isRecord(raw.modelInvocationOutput) ? raw.modelInvocationOutput : undefined;
490
+ /** Content kept as received when every listed capture is on; otherwise reduced to `withheld`. */
491
+ class TraceContent {
492
+ constructor(
493
+ readonly needs: ReadonlyArray<keyof AgentTraceCapture>,
494
+ readonly withheld?: TraceShape,
495
+ ) {}
496
+ }
458
497
 
459
- if (output) {
460
- const metadata = isRecord(output.metadata) ? output.metadata : undefined;
461
- onUsage(metadata?.usage);
462
- }
498
+ const SCALAR = "scalar";
499
+
500
+ // A record shape lists the only fields kept; a one-item array shape is a list of that shape.
501
+ type TraceShape =
502
+ | typeof SCALAR
503
+ | TraceContent
504
+ | readonly [TraceShape]
505
+ | { readonly [field: string]: TraceShape };
506
+
507
+ const TRACE_METADATA: TraceShape = {
508
+ usage: { inputTokens: SCALAR, outputTokens: SCALAR },
509
+ startTime: SCALAR,
510
+ endTime: SCALAR,
511
+ totalTimeMs: SCALAR,
512
+ operationTotalTimeMs: SCALAR,
513
+ clientRequestId: SCALAR,
514
+ };
515
+
516
+ // What the agent sends to a model, tool, or guardrail is input content; what comes back is output.
517
+ const MODEL_INVOCATION_INPUT = new TraceContent(["input"], {
518
+ traceId: SCALAR,
519
+ type: SCALAR,
520
+ foundationModel: SCALAR,
521
+ });
522
+
523
+ const INVOCATION_INPUT = new TraceContent(["input"], { traceId: SCALAR, invocationType: SCALAR });
524
+
525
+ const MODEL_INVOCATION_OUTPUT = new TraceContent(["output"], {
526
+ traceId: SCALAR,
527
+ metadata: TRACE_METADATA,
528
+ });
529
+
530
+ const OBSERVATION = new TraceContent(["output"], { traceId: SCALAR, type: SCALAR });
531
+
532
+ const MODEL_INVOCATION_STEPS = [
533
+ "preProcessingTrace",
534
+ "orchestrationTrace",
535
+ "postProcessingTrace",
536
+ "routingClassifierTrace",
537
+ ];
538
+
539
+ const PROCESSING_TRACE: TraceShape = {
540
+ modelInvocationInput: MODEL_INVOCATION_INPUT,
541
+ modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
542
+ };
543
+
544
+ // Mirrors TracePart and the Trace union in the Bedrock Agent Runtime API; anything else, including
545
+ // the AWS SDK's `$unknown` union member, is dropped because nothing says what it holds.
546
+ const TRACE_PART: TraceShape = {
547
+ agentId: SCALAR,
548
+ agentAliasId: SCALAR,
549
+ agentVersion: SCALAR,
550
+ sessionId: SCALAR,
551
+ collaboratorName: SCALAR,
552
+ eventTime: SCALAR,
553
+ callerChain: [{ agentAliasArn: SCALAR }],
554
+ trace: {
555
+ preProcessingTrace: PROCESSING_TRACE,
556
+ postProcessingTrace: PROCESSING_TRACE,
557
+ orchestrationTrace: {
558
+ modelInvocationInput: MODEL_INVOCATION_INPUT,
559
+ invocationInput: INVOCATION_INPUT,
560
+ modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
561
+ rationale: new TraceContent(["output"], { traceId: SCALAR }),
562
+ observation: OBSERVATION,
563
+ },
564
+ routingClassifierTrace: {
565
+ modelInvocationInput: MODEL_INVOCATION_INPUT,
566
+ invocationInput: INVOCATION_INPUT,
567
+ modelInvocationOutput: MODEL_INVOCATION_OUTPUT,
568
+ observation: OBSERVATION,
569
+ },
570
+ customOrchestrationTrace: { traceId: SCALAR, event: new TraceContent(["output"]) },
571
+ guardrailTrace: {
572
+ traceId: SCALAR,
573
+ action: SCALAR,
574
+ metadata: TRACE_METADATA,
575
+ inputAssessments: new TraceContent(["input"]),
576
+ outputAssessments: new TraceContent(["output"]),
577
+ },
578
+ failureTrace: {
579
+ traceId: SCALAR,
580
+ failureCode: SCALAR,
581
+ metadata: TRACE_METADATA,
582
+ // Free-form text that can quote either the prompt or a tool result.
583
+ failureReason: new TraceContent(["input", "output"]),
584
+ },
585
+ },
586
+ };
463
587
 
464
- for (const child of Object.values(raw)) {
465
- if (isRecord(child)) collectTrace(child, onUsage);
466
- else if (Array.isArray(child)) child.forEach((item) => collectTrace(item, onUsage));
467
- }
468
- }
588
+ function projectTrace(value: JsonValue, shape: TraceShape, capture: AgentTraceCapture): JsonValue {
589
+ if (shape instanceof TraceContent) {
590
+ if (shape.needs.every((kind) => capture[kind])) return value ?? undefined;
469
591
 
470
- function findKey<T>(value: T, key: string): JsonValue {
471
- const raw: unknown = value;
592
+ return shape.withheld === undefined ? undefined : projectTrace(value, shape.withheld, capture);
593
+ }
472
594
 
473
- if (!isRecord(raw)) return undefined;
595
+ if (shape === SCALAR) {
596
+ const scalar = ["string", "number", "boolean"].includes(typeof value) || value instanceof Date;
474
597
 
475
- if (raw[key] !== undefined) return raw[key];
598
+ return scalar ? value : undefined;
599
+ }
476
600
 
477
- for (const child of Object.values(raw)) {
478
- const found = findKey(child, key);
601
+ if (Array.isArray(shape)) {
602
+ if (!Array.isArray(value)) return undefined;
479
603
 
480
- if (found !== undefined) return found;
604
+ return value
605
+ .map((item) => projectTrace(item, shape[0], capture))
606
+ .filter((item) => item !== undefined);
481
607
  }
482
608
 
483
- return undefined;
609
+ if (!isRecord(value)) return undefined;
610
+
611
+ return omitUndefined(
612
+ Object.fromEntries(
613
+ Object.entries(shape).map(([field, child]) => [
614
+ field,
615
+ projectTrace(value[field], child, capture),
616
+ ]),
617
+ ),
618
+ );
484
619
  }
package/src/internal.ts CHANGED
@@ -182,7 +182,7 @@ function isCut(fields: SpanFields | undefined): boolean {
182
182
 
183
183
  // Cores older than captureMasked expose no mask policy before a span starts; treat them as
184
184
  // masked, like LEGACY_CAPTURE_POLICY. A namespace read keeps them loadable without the export.
185
- function captureMaskedBeforeStart(): boolean {
185
+ export function captureMaskedBeforeStart(): boolean {
186
186
  const read = (telemetrySdk as { captureMasked?: () => boolean }).captureMasked;
187
187
 
188
188
  try {
@@ -351,7 +351,7 @@ export function parseJson<T>(value: T): JsonValue {
351
351
  }
352
352
 
353
353
  // Retention uses fixed bounds; the core applies maxAttributeLength after the mask.
354
- export const STREAM_CAPTURE_MAX_BYTES = 48 * 1024;
354
+ const STREAM_CAPTURE_MAX_BYTES = 48 * 1024;
355
355
  export const STREAM_CAPTURE_MAX_ITEMS = 1_000;
356
356
  const CAPTURE_TRUNCATED_ATTRIBUTE = "telemetry.dev.capture.truncated";
357
357
 
@@ -10,6 +10,7 @@ import {
10
10
  omitUndefined,
11
11
  parseJson,
12
12
  PROVIDER,
13
+ STREAM_CAPTURE_MAX_ITEMS,
13
14
  stringValue,
14
15
  StreamRetention,
15
16
  type JsonRecord,
@@ -165,12 +166,7 @@ function nativeResponseFields(
165
166
  {
166
167
  output: body,
167
168
  finishReason: stringValue(body.stop_reason),
168
- usage: omitUndefined({
169
- inputTokens: numberValue(usage.input_tokens),
170
- outputTokens: numberValue(usage.output_tokens),
171
- cacheReadInputTokens: numberValue(usage.cache_read_input_tokens),
172
- cacheCreationInputTokens: numberValue(usage.cache_creation_input_tokens),
173
- }),
169
+ ...claudeUsageFields(usage),
174
170
  },
175
171
  metadata,
176
172
  );
@@ -216,6 +212,39 @@ function nativeResponseFields(
216
212
  return withHeaderUsage({ output: body }, metadata);
217
213
  }
218
214
 
215
+ const CLAUDE_USAGE_KEYS = {
216
+ inputTokens: "input_tokens",
217
+ outputTokens: "output_tokens",
218
+ cacheReadInputTokens: "cache_read_input_tokens",
219
+ cacheCreationInputTokens: "cache_creation_input_tokens",
220
+ } as const;
221
+
222
+ function claudeUsageFields(record: JsonRecord): SpanFields {
223
+ const usage: NonNullable<SpanFields["usage"]> = {};
224
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
225
+ const value = numberValue(record[source]);
226
+ if (value !== undefined) usage[key as keyof typeof CLAUDE_USAGE_KEYS] = value;
227
+ }
228
+ const iterations = record.iterations;
229
+ if (!Array.isArray(iterations)) return { usage };
230
+
231
+ // Executor iterations are already in top-level usage; only compaction is additive.
232
+ const first = Math.max(0, iterations.length - STREAM_CAPTURE_MAX_ITEMS);
233
+ for (let index = first; index < iterations.length; index += 1) {
234
+ const entry = iterations[index];
235
+ if (!isRecord(entry) || entry.type !== "compaction") continue;
236
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
237
+ const value = numberValue(entry[source]);
238
+ const target = key as keyof typeof CLAUDE_USAGE_KEYS;
239
+ if (value !== undefined) usage[target] = (usage[target] ?? 0) + value;
240
+ }
241
+ }
242
+ return {
243
+ usage,
244
+ ...(first > 0 ? { attributes: { "telemetry.dev.capture.truncated": true } } : {}),
245
+ };
246
+ }
247
+
219
248
  function withHeaderUsage<T>(fields: SpanFields, metadata: T): SpanFields {
220
249
  if (fields.usage && Object.keys(fields.usage).length > 0) return fields;
221
250
 
@@ -256,6 +285,9 @@ export class InvokeModelStreamState implements StreamState {
256
285
  private readonly chunks: unknown[] = [];
257
286
  private text = "";
258
287
  private usage: SpanFields["usage"];
288
+ private finalUsage: NonNullable<SpanFields["usage"]> = {};
289
+ private startUsage: NonNullable<SpanFields["usage"]> = {};
290
+ private compaction: SpanFields = {};
259
291
  private finishReason: string | undefined;
260
292
  private error: Error | undefined;
261
293
  private errorFields: SpanFields | undefined;
@@ -290,6 +322,28 @@ export class InvokeModelStreamState implements StreamState {
290
322
  const messageUsage = isRecord(message?.usage) ? message?.usage : undefined;
291
323
  const usage = isRecord(parsed.usage) ? parsed.usage : undefined;
292
324
 
325
+ for (const [key, source] of Object.entries(CLAUDE_USAGE_KEYS)) {
326
+ const value = numberValue(usage?.[source]);
327
+ if (value !== undefined) this.finalUsage[key as keyof typeof CLAUDE_USAGE_KEYS] = value;
328
+ }
329
+ // message_start counts exclude compaction like message_delta counts; metrics may not.
330
+ this.startUsage = omitUndefined({
331
+ ...this.startUsage,
332
+ inputTokens: numberValue(messageUsage?.input_tokens) ?? this.startUsage.inputTokens,
333
+ outputTokens: numberValue(messageUsage?.output_tokens) ?? this.startUsage.outputTokens,
334
+ cacheReadInputTokens:
335
+ numberValue(messageUsage?.cache_read_input_tokens) ??
336
+ this.startUsage.cacheReadInputTokens,
337
+ cacheCreationInputTokens:
338
+ numberValue(messageUsage?.cache_creation_input_tokens) ??
339
+ this.startUsage.cacheCreationInputTokens,
340
+ });
341
+ // An interrupted stream keeps message_start's iterations until a cumulative delta replaces them.
342
+ const iterations = Array.isArray(usage?.iterations)
343
+ ? usage.iterations
344
+ : messageUsage?.iterations;
345
+ if (Array.isArray(iterations)) this.compaction = claudeUsageFields({ iterations });
346
+
293
347
  const metrics = isRecord(parsed["amazon-bedrock-invocationMetrics"])
294
348
  ? parsed["amazon-bedrock-invocationMetrics"]
295
349
  : undefined;
@@ -314,7 +368,7 @@ export class InvokeModelStreamState implements StreamState {
314
368
  this.usage = omitUndefined({
315
369
  ...this.usage,
316
370
  inputTokens:
317
- numberValue(messageUsage?.input_tokens) ??
371
+ numberValue(usage?.input_tokens) ??
318
372
  numberValue(metrics?.inputTokenCount) ??
319
373
  numberValue(parsed.inputTextTokenCount) ??
320
374
  this.usage?.inputTokens,
@@ -324,10 +378,9 @@ export class InvokeModelStreamState implements StreamState {
324
378
  numberValue(parsed.totalOutputTextTokenCount) ??
325
379
  this.usage?.outputTokens,
326
380
  cacheReadInputTokens:
327
- numberValue(messageUsage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
381
+ numberValue(usage?.cache_read_input_tokens) ?? this.usage?.cacheReadInputTokens,
328
382
  cacheCreationInputTokens:
329
- numberValue(messageUsage?.cache_creation_input_tokens) ??
330
- this.usage?.cacheCreationInputTokens,
383
+ numberValue(usage?.cache_creation_input_tokens) ?? this.usage?.cacheCreationInputTokens,
331
384
  });
332
385
  }
333
386
 
@@ -335,6 +388,18 @@ export class InvokeModelStreamState implements StreamState {
335
388
  }
336
389
 
337
390
  finish(): SpanFields {
391
+ const usage = { ...this.startUsage, ...this.usage, ...this.finalUsage };
392
+ for (const key of Object.keys(CLAUDE_USAGE_KEYS) as (keyof typeof CLAUDE_USAGE_KEYS)[]) {
393
+ const value = this.compaction.usage?.[key];
394
+ const baseKey = key === "outputTokens" ? "outputTokens" : "inputTokens";
395
+ // Invocation metrics have no documented compaction exclusion; never add to that base.
396
+ if (
397
+ value !== undefined &&
398
+ (this.finalUsage[baseKey] !== undefined || this.usage?.[baseKey] === undefined)
399
+ ) {
400
+ usage[key] = (usage[key] ?? 0) + value;
401
+ }
402
+ }
338
403
  return mergeFields(
339
404
  omitUndefined({
340
405
  output: this.text
@@ -347,7 +412,8 @@ export class InvokeModelStreamState implements StreamState {
347
412
  ]
348
413
  : this.chunks,
349
414
  finishReason: this.finishReason,
350
- usage: this.usage,
415
+ usage,
416
+ attributes: this.compaction.attributes,
351
417
  error: this.error,
352
418
  }),
353
419
  this.errorFields,