@telemetry-dev/bedrock 0.1.8 → 0.1.10
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 +4 -1
- package/dist/agents.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{internal-handlers-Byxp14OZ.mjs → internal-handlers-PftQAPH3.mjs} +113 -21
- package/package.json +3 -3
- package/src/agent-handlers.ts +51 -8
- package/src/converse.ts +6 -3
- package/src/internal-handlers.ts +2 -1
- package/src/internal.ts +111 -5
- package/src/invoke-model.ts +6 -2
package/README.md
CHANGED
|
@@ -138,8 +138,11 @@ InvokeModel captures provider-native JSON bodies verbatim. Non-JSON bodies are n
|
|
|
138
138
|
- Instrumentation is fail-open. Normalization, span updates, and span endings are guarded so telemetry failures do not break caller code.
|
|
139
139
|
- Requests are never modified. This is especially important for AWS SigV4 signing and for application-level request object reuse.
|
|
140
140
|
- Streams are pull-through. The wrapper does not pre-buffer chunks and therefore preserves backpressure.
|
|
141
|
-
- Streams end spans exactly once. Exhaustion records full output and finish reason; early `break`, `return()`, `close()`, abort, or stream errors record partial output.
|
|
141
|
+
- Streams end spans exactly once. Exhaustion records full output and finish reason; early `break`, `return()`, `close()`, abort, or stream errors record partial output and set `telemetry.dev.capture.truncated`.
|
|
142
|
+
- Stream reconstruction (output, Agent trace events, and Agent output-file metadata) retains up to 48 KiB and 1,000 items; events past the bound still pass through to the caller. The configured `maxAttributeLength` does not shrink this bound; the core applies it to the exported attribute after the mask runs.
|
|
143
|
+
- A stream cut at the bound, stopped early, or failed sets `telemetry.dev.capture.truncated`. With a mask configured, its incomplete reconstructed output is omitted because the mask cannot inspect the complete value. Agent trace events and output-file metadata share the bound, but dropping them only sets the attribute; Agent text is omitted only when a text chunk was dropped or the stream stopped early or failed. A normally exhausted Agent stream that returned control keeps its complete `returnControl` output even if earlier text or metadata passed the bound; an early stop or failure still omits the output when a mask is configured.
|
|
142
144
|
- `AbortSignal` passed to `send(command, { abortSignal })` is forwarded untouched. Pre-response aborts end the span with the thrown abort error. Mid-stream aborts end the span with partial output and the stream error.
|
|
145
|
+
- `Rerank` input and results are bounded to 48 KiB and 1,000 items. A value over the bound sets `telemetry.dev.capture.truncated`. Without a mask, the bounded part is kept. With a mask configured, that value is omitted because the mask cannot inspect the complete value. Input is omitted before the span starts, so a custom `sampler` sees the bounded input when no mask is configured; with an older `@telemetry-dev/sdk` that cannot report whether a mask is configured, cut input is omitted as if one were.
|
|
143
146
|
- Callback-style `send(command, callback)` and `send(command, options, callback)` calls are instrumented; callbacks receive the original data/error while the span is ended before callback invocation.
|
|
144
147
|
|
|
145
148
|
## Coverage gaps
|
package/dist/agents.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as createInstrumentedSend, t as AGENT_HANDLERS } from "./internal-handlers-
|
|
1
|
+
import { r as createInstrumentedSend, t as AGENT_HANDLERS } from "./internal-handlers-PftQAPH3.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-
|
|
1
|
+
import { n as RUNTIME_HANDLERS, r as createInstrumentedSend } from "./internal-handlers-PftQAPH3.mjs";
|
|
2
2
|
import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime";
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
const WRAPPED = /* @__PURE__ */ new WeakSet();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import * as telemetrySdk from "@telemetry-dev/sdk";
|
|
2
|
+
import { boundedCapture, boundedCaptureDetails, captureEnabled, startSpan } from "@telemetry-dev/sdk";
|
|
2
3
|
//#region src/internal.ts
|
|
3
4
|
const PROVIDER = "amazon-bedrock";
|
|
4
5
|
const STREAM_FINALIZER = typeof FinalizationRegistry === "function" ? new FinalizationRegistry((finish) => finish()) : void 0;
|
|
@@ -12,7 +13,7 @@ function createInstrumentedSend(originalSend, options, handlers) {
|
|
|
12
13
|
const input = commandInput(rawCommand);
|
|
13
14
|
const t0 = performance.now();
|
|
14
15
|
const spanType = safe(() => handler.spanType(input)) ?? "span";
|
|
15
|
-
const span =
|
|
16
|
+
const span = startRequestSpan(safe(() => handler.spanName(input)) ?? name ?? "bedrock", {
|
|
16
17
|
type: spanType,
|
|
17
18
|
...safe(() => handler.requestFields(input, options)) ?? {}
|
|
18
19
|
});
|
|
@@ -103,6 +104,33 @@ function withCacheInclusiveInput(fields) {
|
|
|
103
104
|
}
|
|
104
105
|
};
|
|
105
106
|
}
|
|
107
|
+
function capturesWithMask(span) {
|
|
108
|
+
return span.capturePolicy?.mask ?? true;
|
|
109
|
+
}
|
|
110
|
+
function isCut(fields) {
|
|
111
|
+
return fields?.attributes?.["telemetry.dev.capture.truncated"] === true;
|
|
112
|
+
}
|
|
113
|
+
function captureMaskedBeforeStart() {
|
|
114
|
+
const read = telemetrySdk.captureMasked;
|
|
115
|
+
try {
|
|
116
|
+
return typeof read === "function" ? read() : true;
|
|
117
|
+
} catch {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function startRequestSpan(name, options) {
|
|
122
|
+
return startSpan(name, options.input !== void 0 && isCut(options) && captureMaskedBeforeStart() ? {
|
|
123
|
+
...options,
|
|
124
|
+
input: void 0
|
|
125
|
+
} : options);
|
|
126
|
+
}
|
|
127
|
+
/** Ends a non-streamed span, withholding output cut to the retention bound from a mask. */
|
|
128
|
+
function endSpanWithBoundedOutput(span, fields) {
|
|
129
|
+
endSpan(span, fields.output !== void 0 && isCut(fields) && capturesWithMask(span) ? {
|
|
130
|
+
...fields,
|
|
131
|
+
output: void 0
|
|
132
|
+
} : fields);
|
|
133
|
+
}
|
|
106
134
|
function endSpan(span, fields) {
|
|
107
135
|
try {
|
|
108
136
|
span.end(withCacheInclusiveInput(fields));
|
|
@@ -209,6 +237,46 @@ function parseJson(value) {
|
|
|
209
237
|
return;
|
|
210
238
|
}
|
|
211
239
|
}
|
|
240
|
+
const STREAM_CAPTURE_MAX_BYTES = 48 * 1024;
|
|
241
|
+
const STREAM_CAPTURE_MAX_ITEMS = 1e3;
|
|
242
|
+
const CAPTURE_TRUNCATED_ATTRIBUTE = "telemetry.dev.capture.truncated";
|
|
243
|
+
const LEGACY_CAPTURE_POLICY = {
|
|
244
|
+
output: true,
|
|
245
|
+
mask: true,
|
|
246
|
+
maxAttributeLength: 65536
|
|
247
|
+
};
|
|
248
|
+
/** Tracks the fixed byte and item bound before a stream state retains a value. */
|
|
249
|
+
var StreamRetention = class {
|
|
250
|
+
bytes = 0;
|
|
251
|
+
items = 0;
|
|
252
|
+
truncated = false;
|
|
253
|
+
accept(value) {
|
|
254
|
+
if (this.truncated) return false;
|
|
255
|
+
const measured = boundedCaptureDetails(value, {
|
|
256
|
+
maxBytes: STREAM_CAPTURE_MAX_BYTES - this.bytes,
|
|
257
|
+
maxItems: STREAM_CAPTURE_MAX_ITEMS - this.items
|
|
258
|
+
});
|
|
259
|
+
if (measured.truncated) {
|
|
260
|
+
this.truncated = true;
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
this.bytes += measured.bytes;
|
|
264
|
+
this.items += measured.items;
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
function finishStream(state, span, partial) {
|
|
269
|
+
const fields = state.finish(partial);
|
|
270
|
+
const interrupted = partial || fields.error !== void 0;
|
|
271
|
+
if (!interrupted && !state.retention.truncated) return fields;
|
|
272
|
+
fields.attributes = {
|
|
273
|
+
...fields.attributes,
|
|
274
|
+
[CAPTURE_TRUNCATED_ATTRIBUTE]: true
|
|
275
|
+
};
|
|
276
|
+
const outputTruncated = state.outputTruncated?.() ?? state.retention.truncated;
|
|
277
|
+
if ((span.capturePolicy ?? LEGACY_CAPTURE_POLICY).mask && (interrupted || outputTruncated)) delete fields.output;
|
|
278
|
+
return fields;
|
|
279
|
+
}
|
|
212
280
|
function wrapAsyncIterable(iterable, state, span, t0, baseFields, trackOutputChunks = true) {
|
|
213
281
|
if (!iterable || !isAsyncIterable(iterable)) {
|
|
214
282
|
endSpan(span, baseFields);
|
|
@@ -238,15 +306,15 @@ function wrapAsyncIterable(iterable, state, span, t0, baseFields, trackOutputChu
|
|
|
238
306
|
}
|
|
239
307
|
completed = true;
|
|
240
308
|
} catch (error) {
|
|
241
|
-
endOnce(mergeFields(state
|
|
309
|
+
endOnce(mergeFields(finishStream(state, span, true), awsMetadataFields(metadataOf(error)), { error: asError(error) }));
|
|
242
310
|
throw error;
|
|
243
311
|
} finally {
|
|
244
|
-
if (!ended) endOnce(state
|
|
312
|
+
if (!ended) endOnce(finishStream(state, span, !completed));
|
|
245
313
|
}
|
|
246
314
|
};
|
|
247
315
|
const wrapped = {};
|
|
248
316
|
if (isRecord(iterable)) for (const key of Reflect.ownKeys(iterable)) wrapped[key] = iterable[key];
|
|
249
|
-
const finishAbandoned = () => endOnce(safe(() => state
|
|
317
|
+
const finishAbandoned = () => endOnce(safe(() => finishStream(state, span, true)));
|
|
250
318
|
wrapped[Symbol.asyncIterator] = () => {
|
|
251
319
|
const iterator = generator();
|
|
252
320
|
STREAM_FINALIZER?.register(iterator, finishAbandoned, unregisterToken);
|
|
@@ -340,12 +408,16 @@ function inlineAgentRequestFields(input) {
|
|
|
340
408
|
});
|
|
341
409
|
}
|
|
342
410
|
var AgentStreamState = class {
|
|
411
|
+
retention = new StreamRetention();
|
|
343
412
|
text = "";
|
|
413
|
+
textRejected = false;
|
|
344
414
|
usage = {};
|
|
345
415
|
traceEventCount = 0;
|
|
346
416
|
failureReason;
|
|
347
417
|
guardrailAction;
|
|
348
418
|
returnControl;
|
|
419
|
+
sawReturnControl = false;
|
|
420
|
+
returnControlRejected = false;
|
|
349
421
|
traces = [];
|
|
350
422
|
error;
|
|
351
423
|
errorFields;
|
|
@@ -365,17 +437,27 @@ var AgentStreamState = class {
|
|
|
365
437
|
}
|
|
366
438
|
const chunk = isRecord(raw.chunk) ? raw.chunk : void 0;
|
|
367
439
|
const bytes = chunk?.bytes;
|
|
368
|
-
|
|
440
|
+
const piece = bytes === void 0 ? void 0 : bytesToString(bytes);
|
|
441
|
+
if (piece) if (this.retention.accept(piece)) this.text += piece;
|
|
442
|
+
else this.textRejected = true;
|
|
369
443
|
const citations = arrayValue((isRecord(chunk?.attribution) ? chunk.attribution : void 0)?.citations);
|
|
370
444
|
if (citations) this.citationCount += citations.length;
|
|
371
|
-
const
|
|
372
|
-
if (
|
|
445
|
+
const sanitizedFiles = (isRecord(raw.files) ? arrayValue(raw.files.files) : void 0)?.map(sanitizeOutputFile);
|
|
446
|
+
if (sanitizedFiles && this.retention.accept(sanitizedFiles)) this.outputFiles.push(...sanitizedFiles);
|
|
373
447
|
const returnControl = isRecord(raw.returnControl) ? raw.returnControl : void 0;
|
|
374
|
-
if (returnControl)
|
|
448
|
+
if (returnControl) {
|
|
449
|
+
this.sawReturnControl = true;
|
|
450
|
+
if (new StreamRetention().accept(returnControl)) this.returnControl = returnControl;
|
|
451
|
+
else {
|
|
452
|
+
this.returnControl = void 0;
|
|
453
|
+
this.returnControlRejected = true;
|
|
454
|
+
this.retention.truncated = true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
375
457
|
const traceEvent = isRecord(raw.trace) ? raw.trace : void 0;
|
|
376
458
|
if (traceEvent) {
|
|
377
459
|
this.traceEventCount += 1;
|
|
378
|
-
if (this.options.captureAgentTrace) this.traces.push(traceEvent);
|
|
460
|
+
if (this.options.captureAgentTrace && this.retention.accept(traceEvent)) this.traces.push(traceEvent);
|
|
379
461
|
collectTrace(traceEvent, (usage) => this.addUsage(usage));
|
|
380
462
|
const failure = findKey(traceEvent, "failureTrace");
|
|
381
463
|
if (isRecord(failure)) this.failureReason = stringValue(failure.failureReason) ?? this.failureReason;
|
|
@@ -383,12 +465,16 @@ var AgentStreamState = class {
|
|
|
383
465
|
if (isRecord(guardrail)) this.guardrailAction = stringValue(guardrail.action) ?? this.guardrailAction;
|
|
384
466
|
}
|
|
385
467
|
}
|
|
468
|
+
outputTruncated() {
|
|
469
|
+
if (this.returnControl !== void 0) return false;
|
|
470
|
+
return this.textRejected || this.returnControlRejected;
|
|
471
|
+
}
|
|
386
472
|
finish() {
|
|
387
473
|
const metadata = omitUndefined({
|
|
388
474
|
trace_event_count: this.traceEventCount || void 0,
|
|
389
475
|
failure_reason: this.failureReason,
|
|
390
476
|
guardrail_action: this.guardrailAction,
|
|
391
|
-
return_control: this.
|
|
477
|
+
return_control: this.sawReturnControl ? true : void 0,
|
|
392
478
|
citation_count: this.citationCount || void 0,
|
|
393
479
|
output_files: this.outputFiles.length > 0 ? this.outputFiles : void 0,
|
|
394
480
|
agent_trace: this.options.captureAgentTrace && this.traces.length > 0 ? this.traces : void 0
|
|
@@ -533,6 +619,7 @@ function ragResponseFields(output) {
|
|
|
533
619
|
});
|
|
534
620
|
}
|
|
535
621
|
var RagStreamState = class {
|
|
622
|
+
retention = new StreamRetention();
|
|
536
623
|
text = "";
|
|
537
624
|
citationCount = 0;
|
|
538
625
|
guardrailAction;
|
|
@@ -547,8 +634,8 @@ var RagStreamState = class {
|
|
|
547
634
|
this.errorFields = streamError.fields;
|
|
548
635
|
return;
|
|
549
636
|
}
|
|
550
|
-
const
|
|
551
|
-
|
|
637
|
+
const text = stringValue((isRecord(raw.output) ? raw.output : void 0)?.text);
|
|
638
|
+
if (text && this.retention.accept(text)) this.text += text;
|
|
552
639
|
if (raw.citation) this.citationCount += 1;
|
|
553
640
|
const guardrail = isRecord(raw.guardrail) ? raw.guardrail : void 0;
|
|
554
641
|
this.guardrailAction = stringValue(guardrail?.action) ?? this.guardrailAction;
|
|
@@ -578,6 +665,7 @@ function flowRequestFields(input) {
|
|
|
578
665
|
});
|
|
579
666
|
}
|
|
580
667
|
var FlowStreamState = class {
|
|
668
|
+
retention = new StreamRetention();
|
|
581
669
|
outputs = [];
|
|
582
670
|
completionReason;
|
|
583
671
|
error;
|
|
@@ -592,9 +680,9 @@ var FlowStreamState = class {
|
|
|
592
680
|
return;
|
|
593
681
|
}
|
|
594
682
|
const output = isRecord(raw.flowOutputEvent) ? raw.flowOutputEvent : void 0;
|
|
595
|
-
if (output) this.outputs.push(output.content);
|
|
683
|
+
if (output && this.retention.accept(output.content)) this.outputs.push(output.content);
|
|
596
684
|
const inputRequest = isRecord(raw.flowMultiTurnInputRequestEvent) ? raw.flowMultiTurnInputRequestEvent : void 0;
|
|
597
|
-
if (inputRequest) this.outputs.push(inputRequest.content);
|
|
685
|
+
if (inputRequest && this.retention.accept(inputRequest.content)) this.outputs.push(inputRequest.content);
|
|
598
686
|
const completion = isRecord(raw.flowCompletionEvent) ? raw.flowCompletionEvent : void 0;
|
|
599
687
|
this.completionReason = stringValue(completion?.completionReason) ?? this.completionReason;
|
|
600
688
|
}
|
|
@@ -801,6 +889,7 @@ function s3Uri(value) {
|
|
|
801
889
|
return bucket && key ? `s3://${bucket}/${key}` : void 0;
|
|
802
890
|
}
|
|
803
891
|
var ConverseStreamState = class {
|
|
892
|
+
retention = new StreamRetention();
|
|
804
893
|
role = "assistant";
|
|
805
894
|
blocks = /* @__PURE__ */ new Map();
|
|
806
895
|
stopReason;
|
|
@@ -820,7 +909,7 @@ var ConverseStreamState = class {
|
|
|
820
909
|
const messageStart = isRecord(event.messageStart) ? event.messageStart : void 0;
|
|
821
910
|
if (messageStart) this.role = stringValue(messageStart.role) ?? this.role;
|
|
822
911
|
const blockStart = isRecord(event.contentBlockStart) ? event.contentBlockStart : void 0;
|
|
823
|
-
if (blockStart) {
|
|
912
|
+
if (blockStart && this.retention.accept(blockStart.start ?? {})) {
|
|
824
913
|
const index = numberValue(blockStart.contentBlockIndex) ?? this.blocks.size;
|
|
825
914
|
const start = isRecord(blockStart.start) ? blockStart.start : void 0;
|
|
826
915
|
const toolUse = isRecord(start?.toolUse) ? start?.toolUse : void 0;
|
|
@@ -847,9 +936,9 @@ var ConverseStreamState = class {
|
|
|
847
936
|
});
|
|
848
937
|
}
|
|
849
938
|
const deltaEvent = isRecord(event.contentBlockDelta) ? event.contentBlockDelta : void 0;
|
|
850
|
-
|
|
939
|
+
const delta = isRecord(deltaEvent?.delta) ? deltaEvent.delta : {};
|
|
940
|
+
if (deltaEvent && this.retention.accept(delta)) {
|
|
851
941
|
const index = numberValue(deltaEvent.contentBlockIndex) ?? 0;
|
|
852
|
-
const delta = isRecord(deltaEvent.delta) ? deltaEvent.delta : {};
|
|
853
942
|
const block = this.blocks.get(index) ?? {
|
|
854
943
|
kind: "text",
|
|
855
944
|
text: ""
|
|
@@ -1109,6 +1198,7 @@ function headerNumber(value) {
|
|
|
1109
1198
|
return numberValue(value);
|
|
1110
1199
|
}
|
|
1111
1200
|
var InvokeModelStreamState = class {
|
|
1201
|
+
retention = new StreamRetention();
|
|
1112
1202
|
chunks = [];
|
|
1113
1203
|
text = "";
|
|
1114
1204
|
usage;
|
|
@@ -1129,8 +1219,10 @@ var InvokeModelStreamState = class {
|
|
|
1129
1219
|
const raw = chunk?.bytes;
|
|
1130
1220
|
return (typeof raw === "string" || raw instanceof Uint8Array) && raw.length > 0;
|
|
1131
1221
|
}
|
|
1132
|
-
this.
|
|
1133
|
-
|
|
1222
|
+
if (this.retention.accept(parsed)) {
|
|
1223
|
+
this.chunks.push(parsed);
|
|
1224
|
+
this.text += textFromProviderChunk(parsed);
|
|
1225
|
+
}
|
|
1134
1226
|
if (isRecord(parsed)) {
|
|
1135
1227
|
const delta = isRecord(parsed.delta) ? parsed.delta : void 0;
|
|
1136
1228
|
const message = isRecord(parsed.message) ? parsed.message : void 0;
|
|
@@ -1278,7 +1370,7 @@ const AGENT_HANDLERS = {
|
|
|
1278
1370
|
spanType: () => "span",
|
|
1279
1371
|
requestFields: rerankRequestFields,
|
|
1280
1372
|
onResult(result, span) {
|
|
1281
|
-
|
|
1373
|
+
endSpanWithBoundedOutput(span, rerankResponseFields(result));
|
|
1282
1374
|
return result;
|
|
1283
1375
|
}
|
|
1284
1376
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-dev/bedrock",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -49,7 +49,7 @@
|
|
|
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.
|
|
52
|
+
"@telemetry-dev/sdk": "^0.1.8",
|
|
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.
|
|
62
|
+
"@telemetry-dev/sdk": "^0.1.8"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"@aws-sdk/client-bedrock-agent-runtime": {
|
package/src/agent-handlers.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
omitUndefined,
|
|
13
13
|
PROVIDER,
|
|
14
14
|
stringValue,
|
|
15
|
+
StreamRetention,
|
|
15
16
|
type BedrockInstrumentationOptions,
|
|
16
17
|
type JsonRecord,
|
|
17
18
|
type JsonValue,
|
|
@@ -48,12 +49,17 @@ export function inlineAgentRequestFields(input: JsonRecord): SpanFields {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
export class AgentStreamState implements StreamState {
|
|
52
|
+
readonly retention = new StreamRetention();
|
|
51
53
|
private text = "";
|
|
54
|
+
// Trace and output-file metadata share the budget but do not make the text incomplete.
|
|
55
|
+
private textRejected = false;
|
|
52
56
|
private usage: NonNullable<SpanFields["usage"]> = {};
|
|
53
57
|
private traceEventCount = 0;
|
|
54
58
|
private failureReason: string | undefined;
|
|
55
59
|
private guardrailAction: string | undefined;
|
|
56
60
|
private returnControl: unknown;
|
|
61
|
+
private sawReturnControl = false;
|
|
62
|
+
private returnControlRejected = false;
|
|
57
63
|
private readonly traces: unknown[] = [];
|
|
58
64
|
private error: Error | undefined;
|
|
59
65
|
private errorFields: SpanFields | undefined;
|
|
@@ -78,7 +84,12 @@ export class AgentStreamState implements StreamState {
|
|
|
78
84
|
const chunk = isRecord(raw.chunk) ? raw.chunk : undefined;
|
|
79
85
|
const bytes = chunk?.bytes;
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
const piece = bytes === undefined ? undefined : bytesToString(bytes);
|
|
88
|
+
|
|
89
|
+
if (piece) {
|
|
90
|
+
if (this.retention.accept(piece)) this.text += piece;
|
|
91
|
+
else this.textRejected = true;
|
|
92
|
+
}
|
|
82
93
|
const attribution = isRecord(chunk?.attribution) ? chunk.attribution : undefined;
|
|
83
94
|
const citations = arrayValue<unknown>(attribution?.citations);
|
|
84
95
|
|
|
@@ -86,18 +97,37 @@ export class AgentStreamState implements StreamState {
|
|
|
86
97
|
|
|
87
98
|
const files = isRecord(raw.files) ? arrayValue<JsonValue>(raw.files.files) : undefined;
|
|
88
99
|
|
|
89
|
-
|
|
100
|
+
const sanitizedFiles = files?.map(sanitizeOutputFile);
|
|
101
|
+
|
|
102
|
+
if (sanitizedFiles && this.retention.accept(sanitizedFiles)) {
|
|
103
|
+
this.outputFiles.push(...sanitizedFiles);
|
|
104
|
+
}
|
|
90
105
|
|
|
91
106
|
const returnControl = isRecord(raw.returnControl) ? raw.returnControl : undefined;
|
|
92
107
|
|
|
93
|
-
if (returnControl)
|
|
108
|
+
if (returnControl) {
|
|
109
|
+
this.sawReturnControl = true;
|
|
110
|
+
|
|
111
|
+
// Return control replaces the text output, so it gets its own fresh bound and survives
|
|
112
|
+
// text that already spent the shared budget; one that alone does not fit is dropped and
|
|
113
|
+
// the capture is marked incomplete.
|
|
114
|
+
if (new StreamRetention().accept(returnControl)) {
|
|
115
|
+
this.returnControl = returnControl;
|
|
116
|
+
} else {
|
|
117
|
+
this.returnControl = undefined;
|
|
118
|
+
this.returnControlRejected = true;
|
|
119
|
+
this.retention.truncated = true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
94
122
|
|
|
95
123
|
const traceEvent = isRecord(raw.trace) ? raw.trace : undefined;
|
|
96
124
|
|
|
97
125
|
if (traceEvent) {
|
|
98
126
|
this.traceEventCount += 1;
|
|
99
127
|
|
|
100
|
-
if (this.options.captureAgentTrace
|
|
128
|
+
if (this.options.captureAgentTrace && this.retention.accept(traceEvent)) {
|
|
129
|
+
this.traces.push(traceEvent);
|
|
130
|
+
}
|
|
101
131
|
collectTrace(traceEvent, (usage) => this.addUsage(usage));
|
|
102
132
|
const failure = findKey(traceEvent, "failureTrace");
|
|
103
133
|
|
|
@@ -110,12 +140,19 @@ export class AgentStreamState implements StreamState {
|
|
|
110
140
|
}
|
|
111
141
|
}
|
|
112
142
|
|
|
143
|
+
outputTruncated(): boolean {
|
|
144
|
+
// Return control replaces the text output and is retained whole when it fits its bound.
|
|
145
|
+
if (this.returnControl !== undefined) return false;
|
|
146
|
+
|
|
147
|
+
return this.textRejected || this.returnControlRejected;
|
|
148
|
+
}
|
|
149
|
+
|
|
113
150
|
finish(): SpanFields {
|
|
114
151
|
const metadata = omitUndefined({
|
|
115
152
|
trace_event_count: this.traceEventCount || undefined,
|
|
116
153
|
failure_reason: this.failureReason,
|
|
117
154
|
guardrail_action: this.guardrailAction,
|
|
118
|
-
return_control: this.
|
|
155
|
+
return_control: this.sawReturnControl ? true : undefined,
|
|
119
156
|
citation_count: this.citationCount || undefined,
|
|
120
157
|
output_files: this.outputFiles.length > 0 ? this.outputFiles : undefined,
|
|
121
158
|
agent_trace:
|
|
@@ -303,6 +340,7 @@ export function ragResponseFields<T>(output: T): SpanFields {
|
|
|
303
340
|
}
|
|
304
341
|
|
|
305
342
|
export class RagStreamState implements StreamState {
|
|
343
|
+
readonly retention = new StreamRetention();
|
|
306
344
|
private text = "";
|
|
307
345
|
private citationCount = 0;
|
|
308
346
|
private guardrailAction: string | undefined;
|
|
@@ -323,7 +361,9 @@ export class RagStreamState implements StreamState {
|
|
|
323
361
|
}
|
|
324
362
|
|
|
325
363
|
const output = isRecord(raw.output) ? raw.output : undefined;
|
|
326
|
-
|
|
364
|
+
const text = stringValue(output?.text);
|
|
365
|
+
|
|
366
|
+
if (text && this.retention.accept(text)) this.text += text;
|
|
327
367
|
|
|
328
368
|
if (raw.citation) this.citationCount += 1;
|
|
329
369
|
const guardrail = isRecord(raw.guardrail) ? raw.guardrail : undefined;
|
|
@@ -356,6 +396,7 @@ export function flowRequestFields(input: JsonRecord): SpanFields {
|
|
|
356
396
|
}
|
|
357
397
|
|
|
358
398
|
export class FlowStreamState implements StreamState {
|
|
399
|
+
readonly retention = new StreamRetention();
|
|
359
400
|
private outputs: unknown[] = [];
|
|
360
401
|
private completionReason: string | undefined;
|
|
361
402
|
private error: Error | undefined;
|
|
@@ -376,13 +417,15 @@ export class FlowStreamState implements StreamState {
|
|
|
376
417
|
|
|
377
418
|
const output = isRecord(raw.flowOutputEvent) ? raw.flowOutputEvent : undefined;
|
|
378
419
|
|
|
379
|
-
if (output) this.outputs.push(output.content);
|
|
420
|
+
if (output && this.retention.accept(output.content)) this.outputs.push(output.content);
|
|
380
421
|
|
|
381
422
|
const inputRequest = isRecord(raw.flowMultiTurnInputRequestEvent)
|
|
382
423
|
? raw.flowMultiTurnInputRequestEvent
|
|
383
424
|
: undefined;
|
|
384
425
|
|
|
385
|
-
if (inputRequest
|
|
426
|
+
if (inputRequest && this.retention.accept(inputRequest.content)) {
|
|
427
|
+
this.outputs.push(inputRequest.content);
|
|
428
|
+
}
|
|
386
429
|
const completion = isRecord(raw.flowCompletionEvent) ? raw.flowCompletionEvent : undefined;
|
|
387
430
|
this.completionReason = stringValue(completion?.completionReason) ?? this.completionReason;
|
|
388
431
|
}
|
package/src/converse.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
parseJson,
|
|
12
12
|
PROVIDER,
|
|
13
13
|
stringValue,
|
|
14
|
+
StreamRetention,
|
|
14
15
|
type JsonRecord,
|
|
15
16
|
type JsonValue,
|
|
16
17
|
type StreamState,
|
|
@@ -256,6 +257,7 @@ interface StreamBlock {
|
|
|
256
257
|
}
|
|
257
258
|
|
|
258
259
|
export class ConverseStreamState implements StreamState {
|
|
260
|
+
readonly retention = new StreamRetention();
|
|
259
261
|
private role = "assistant";
|
|
260
262
|
private readonly blocks = new Map<number, StreamBlock>();
|
|
261
263
|
private stopReason: string | undefined;
|
|
@@ -282,7 +284,7 @@ export class ConverseStreamState implements StreamState {
|
|
|
282
284
|
|
|
283
285
|
const blockStart = isRecord(event.contentBlockStart) ? event.contentBlockStart : undefined;
|
|
284
286
|
|
|
285
|
-
if (blockStart) {
|
|
287
|
+
if (blockStart && this.retention.accept(blockStart.start ?? {})) {
|
|
286
288
|
const index = numberValue(blockStart.contentBlockIndex) ?? this.blocks.size;
|
|
287
289
|
const start = isRecord(blockStart.start) ? blockStart.start : undefined;
|
|
288
290
|
const toolUse = isRecord(start?.toolUse) ? start?.toolUse : undefined;
|
|
@@ -316,9 +318,10 @@ export class ConverseStreamState implements StreamState {
|
|
|
316
318
|
|
|
317
319
|
const deltaEvent = isRecord(event.contentBlockDelta) ? event.contentBlockDelta : undefined;
|
|
318
320
|
|
|
319
|
-
|
|
321
|
+
const delta = isRecord(deltaEvent?.delta) ? deltaEvent.delta : {};
|
|
322
|
+
|
|
323
|
+
if (deltaEvent && this.retention.accept(delta)) {
|
|
320
324
|
const index = numberValue(deltaEvent.contentBlockIndex) ?? 0;
|
|
321
|
-
const delta = isRecord(deltaEvent.delta) ? deltaEvent.delta : {};
|
|
322
325
|
const block = this.blocks.get(index) ?? { kind: "text", text: "" };
|
|
323
326
|
const text = stringValue(delta.text);
|
|
324
327
|
|
package/src/internal-handlers.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
stringValue,
|
|
33
33
|
wrapAsyncIterable,
|
|
34
34
|
endSpan,
|
|
35
|
+
endSpanWithBoundedOutput,
|
|
35
36
|
type BedrockInstrumentationOptions,
|
|
36
37
|
} from "./internal.ts";
|
|
37
38
|
|
|
@@ -154,7 +155,7 @@ export const AGENT_HANDLERS = {
|
|
|
154
155
|
spanType: () => "span",
|
|
155
156
|
requestFields: rerankRequestFields,
|
|
156
157
|
onResult(result, span) {
|
|
157
|
-
|
|
158
|
+
endSpanWithBoundedOutput(span, rerankResponseFields(result));
|
|
158
159
|
|
|
159
160
|
return result;
|
|
160
161
|
},
|
package/src/internal.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
boundedCaptureDetails,
|
|
3
|
+
startSpan,
|
|
4
|
+
type SpanFields,
|
|
5
|
+
type SpanHandle,
|
|
6
|
+
type SpanType,
|
|
7
|
+
type StartSpanOptions,
|
|
8
|
+
} from "@telemetry-dev/sdk";
|
|
9
|
+
import * as telemetrySdk from "@telemetry-dev/sdk";
|
|
2
10
|
|
|
3
11
|
export const PROVIDER = "amazon-bedrock";
|
|
4
12
|
|
|
@@ -64,7 +72,7 @@ export function createInstrumentedSend(
|
|
|
64
72
|
const spanType = safe(() => handler.spanType(input)) ?? "span";
|
|
65
73
|
const spanName = safe(() => handler.spanName(input)) ?? name ?? "bedrock";
|
|
66
74
|
const fields = safe(() => handler.requestFields(input, options)) ?? {};
|
|
67
|
-
const span =
|
|
75
|
+
const span = startRequestSpan(spanName, { type: spanType, ...fields });
|
|
68
76
|
const callbackIndex = rawRest.findIndex((arg) => arg instanceof Function);
|
|
69
77
|
|
|
70
78
|
if (callbackIndex !== -1) {
|
|
@@ -150,8 +158,10 @@ function withCacheInclusiveInput(fields: SpanFields | undefined): SpanFields | u
|
|
|
150
158
|
const usage = fields?.usage;
|
|
151
159
|
|
|
152
160
|
if (usage?.inputTokens === undefined) return fields;
|
|
161
|
+
|
|
153
162
|
const inputTokens =
|
|
154
163
|
usage.inputTokens + (usage.cacheReadInputTokens ?? 0) + (usage.cacheCreationInputTokens ?? 0);
|
|
164
|
+
|
|
155
165
|
const totalTokens =
|
|
156
166
|
usage.totalTokens !== undefined && usage.outputTokens !== undefined
|
|
157
167
|
? inputTokens + usage.outputTokens
|
|
@@ -160,6 +170,42 @@ function withCacheInclusiveInput(fields: SpanFields | undefined): SpanFields | u
|
|
|
160
170
|
return { ...fields, usage: { ...usage, inputTokens, totalTokens } };
|
|
161
171
|
}
|
|
162
172
|
|
|
173
|
+
// A capture cut to the retention bound keeps its truncated flag, but a mask cannot be trusted to
|
|
174
|
+
// redact a value it only sees part of. Cores without a capture policy are treated as masked.
|
|
175
|
+
function capturesWithMask(span: SpanHandle): boolean {
|
|
176
|
+
return span.capturePolicy?.mask ?? true;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function isCut(fields: SpanFields | undefined): boolean {
|
|
180
|
+
return fields?.attributes?.["telemetry.dev.capture.truncated"] === true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Cores older than captureMasked expose no mask policy before a span starts; treat them as
|
|
184
|
+
// masked, like LEGACY_CAPTURE_POLICY. A namespace read keeps them loadable without the export.
|
|
185
|
+
function captureMaskedBeforeStart(): boolean {
|
|
186
|
+
const read = (telemetrySdk as { captureMasked?: () => boolean }).captureMasked;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
return typeof read === "function" ? read() : true;
|
|
190
|
+
} catch {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// The decision is made before the span starts so samplers see the same attributes as before.
|
|
196
|
+
function startRequestSpan(name: string, options: StartSpanOptions): SpanHandle {
|
|
197
|
+
const withheld = options.input !== undefined && isCut(options) && captureMaskedBeforeStart();
|
|
198
|
+
|
|
199
|
+
return startSpan(name, withheld ? { ...options, input: undefined } : options);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Ends a non-streamed span, withholding output cut to the retention bound from a mask. */
|
|
203
|
+
export function endSpanWithBoundedOutput(span: SpanHandle, fields: SpanFields): void {
|
|
204
|
+
const withheld = fields.output !== undefined && isCut(fields) && capturesWithMask(span);
|
|
205
|
+
|
|
206
|
+
endSpan(span, withheld ? { ...fields, output: undefined } : fields);
|
|
207
|
+
}
|
|
208
|
+
|
|
163
209
|
export function endSpan(span: SpanHandle, fields?: SpanFields): void {
|
|
164
210
|
try {
|
|
165
211
|
span.end(withCacheInclusiveInput(fields));
|
|
@@ -304,9 +350,69 @@ export function parseJson<T>(value: T): JsonValue {
|
|
|
304
350
|
}
|
|
305
351
|
}
|
|
306
352
|
|
|
353
|
+
// Retention uses fixed bounds; the core applies maxAttributeLength after the mask.
|
|
354
|
+
export const STREAM_CAPTURE_MAX_BYTES = 48 * 1024;
|
|
355
|
+
export const STREAM_CAPTURE_MAX_ITEMS = 1_000;
|
|
356
|
+
const CAPTURE_TRUNCATED_ATTRIBUTE = "telemetry.dev.capture.truncated";
|
|
357
|
+
|
|
358
|
+
// Older cores enforce captureOutput and masking when the span ends but expose no policy, so
|
|
359
|
+
// withhold incomplete output from their mask. Retention uses the fixed stream bounds either way;
|
|
360
|
+
// maxAttributeLength is only here to satisfy the policy type.
|
|
361
|
+
const LEGACY_CAPTURE_POLICY: NonNullable<SpanHandle["capturePolicy"]> = {
|
|
362
|
+
output: true,
|
|
363
|
+
mask: true,
|
|
364
|
+
maxAttributeLength: 65_536,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
/** Tracks the fixed byte and item bound before a stream state retains a value. */
|
|
368
|
+
export class StreamRetention {
|
|
369
|
+
private bytes = 0;
|
|
370
|
+
private items = 0;
|
|
371
|
+
truncated = false;
|
|
372
|
+
|
|
373
|
+
accept<T>(value: T): boolean {
|
|
374
|
+
if (this.truncated) return false;
|
|
375
|
+
|
|
376
|
+
const measured = boundedCaptureDetails(value, {
|
|
377
|
+
maxBytes: STREAM_CAPTURE_MAX_BYTES - this.bytes,
|
|
378
|
+
maxItems: STREAM_CAPTURE_MAX_ITEMS - this.items,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
if (measured.truncated) {
|
|
382
|
+
this.truncated = true;
|
|
383
|
+
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
this.bytes += measured.bytes;
|
|
388
|
+
this.items += measured.items;
|
|
389
|
+
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
307
394
|
export interface StreamState {
|
|
308
395
|
feed<T>(event: T): boolean | void;
|
|
309
396
|
finish(partial: boolean): SpanFields;
|
|
397
|
+
readonly retention: StreamRetention;
|
|
398
|
+
/** Whether the exported output itself was cut; defaults to the retention flag. */
|
|
399
|
+
outputTruncated?(): boolean;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function finishStream(state: StreamState, span: SpanHandle, partial: boolean): SpanFields {
|
|
403
|
+
const fields = state.finish(partial);
|
|
404
|
+
const interrupted = partial || fields.error !== undefined;
|
|
405
|
+
|
|
406
|
+
if (!interrupted && !state.retention.truncated) return fields;
|
|
407
|
+
|
|
408
|
+
fields.attributes = { ...fields.attributes, [CAPTURE_TRUNCATED_ATTRIBUTE]: true };
|
|
409
|
+
const outputTruncated = state.outputTruncated?.() ?? state.retention.truncated;
|
|
410
|
+
|
|
411
|
+
if ((span.capturePolicy ?? LEGACY_CAPTURE_POLICY).mask && (interrupted || outputTruncated)) {
|
|
412
|
+
delete fields.output;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return fields;
|
|
310
416
|
}
|
|
311
417
|
|
|
312
418
|
export function wrapAsyncIterable<T>(
|
|
@@ -358,13 +464,13 @@ export function wrapAsyncIterable<T>(
|
|
|
358
464
|
completed = true;
|
|
359
465
|
} catch (error) {
|
|
360
466
|
endOnce(
|
|
361
|
-
mergeFields(state
|
|
467
|
+
mergeFields(finishStream(state, span, true), awsMetadataFields(metadataOf(error)), {
|
|
362
468
|
error: asError(error),
|
|
363
469
|
}),
|
|
364
470
|
);
|
|
365
471
|
throw error;
|
|
366
472
|
} finally {
|
|
367
|
-
if (!ended) endOnce(state
|
|
473
|
+
if (!ended) endOnce(finishStream(state, span, !completed));
|
|
368
474
|
}
|
|
369
475
|
};
|
|
370
476
|
|
|
@@ -376,7 +482,7 @@ export function wrapAsyncIterable<T>(
|
|
|
376
482
|
}
|
|
377
483
|
}
|
|
378
484
|
|
|
379
|
-
const finishAbandoned = () => endOnce(safe(() => state
|
|
485
|
+
const finishAbandoned = () => endOnce(safe(() => finishStream(state, span, true)));
|
|
380
486
|
wrapped[Symbol.asyncIterator] = () => {
|
|
381
487
|
const iterator = generator();
|
|
382
488
|
STREAM_FINALIZER?.register(iterator, finishAbandoned, unregisterToken);
|
package/src/invoke-model.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
parseJson,
|
|
12
12
|
PROVIDER,
|
|
13
13
|
stringValue,
|
|
14
|
+
StreamRetention,
|
|
14
15
|
type JsonRecord,
|
|
15
16
|
type JsonValue,
|
|
16
17
|
type StreamState,
|
|
@@ -251,6 +252,7 @@ function headerNumber<T>(value: T): number | undefined {
|
|
|
251
252
|
}
|
|
252
253
|
|
|
253
254
|
export class InvokeModelStreamState implements StreamState {
|
|
255
|
+
readonly retention = new StreamRetention();
|
|
254
256
|
private readonly chunks: unknown[] = [];
|
|
255
257
|
private text = "";
|
|
256
258
|
private usage: SpanFields["usage"];
|
|
@@ -277,8 +279,10 @@ export class InvokeModelStreamState implements StreamState {
|
|
|
277
279
|
return (typeof raw === "string" || raw instanceof Uint8Array) && raw.length > 0;
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
this.
|
|
281
|
-
|
|
282
|
+
if (this.retention.accept(parsed)) {
|
|
283
|
+
this.chunks.push(parsed);
|
|
284
|
+
this.text += textFromProviderChunk(parsed);
|
|
285
|
+
}
|
|
282
286
|
|
|
283
287
|
if (isRecord(parsed)) {
|
|
284
288
|
const delta = isRecord(parsed.delta) ? parsed.delta : undefined;
|