@telemetry-dev/bedrock 0.1.9 → 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 +30 -5
- package/dist/agents.mjs +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{internal-handlers-Byxp14OZ.mjs → internal-handlers-CLP2wXDh.mjs} +304 -52
- package/package.json +5 -5
- package/src/agent-handlers.ts +214 -36
- package/src/converse.ts +6 -3
- package/src/internal-handlers.ts +2 -1
- package/src/internal.ts +109 -5
- package/src/invoke-model.ts +83 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telemetry-dev/bedrock",
|
|
3
|
-
"version": "0.1.
|
|
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.
|
|
48
|
-
"@opentelemetry/sdk-trace-base": "^2.
|
|
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.
|
|
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.
|
|
62
|
+
"@telemetry-dev/sdk": "^0.1.10"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"@aws-sdk/client-bedrock-agent-runtime": {
|
package/src/agent-handlers.ts
CHANGED
|
@@ -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,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
omitUndefined,
|
|
13
14
|
PROVIDER,
|
|
14
15
|
stringValue,
|
|
16
|
+
StreamRetention,
|
|
15
17
|
type BedrockInstrumentationOptions,
|
|
16
18
|
type JsonRecord,
|
|
17
19
|
type JsonValue,
|
|
@@ -48,17 +50,30 @@ export function inlineAgentRequestFields(input: JsonRecord): SpanFields {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export class AgentStreamState implements StreamState {
|
|
53
|
+
readonly retention = new StreamRetention();
|
|
51
54
|
private text = "";
|
|
55
|
+
// Trace and output-file metadata share the budget but do not make the text incomplete.
|
|
56
|
+
private textRejected = false;
|
|
52
57
|
private usage: NonNullable<SpanFields["usage"]> = {};
|
|
53
58
|
private traceEventCount = 0;
|
|
59
|
+
private failed = false;
|
|
60
|
+
private failureCode: number | undefined;
|
|
54
61
|
private failureReason: string | undefined;
|
|
55
62
|
private guardrailAction: string | undefined;
|
|
56
63
|
private returnControl: unknown;
|
|
64
|
+
private sawReturnControl = false;
|
|
65
|
+
private returnControlRejected = false;
|
|
57
66
|
private readonly traces: unknown[] = [];
|
|
58
67
|
private error: Error | undefined;
|
|
59
68
|
private errorFields: SpanFields | undefined;
|
|
60
69
|
private citationCount = 0;
|
|
61
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
|
+
};
|
|
62
77
|
|
|
63
78
|
constructor(private readonly options: BedrockInstrumentationOptions) {}
|
|
64
79
|
|
|
@@ -78,7 +93,13 @@ export class AgentStreamState implements StreamState {
|
|
|
78
93
|
const chunk = isRecord(raw.chunk) ? raw.chunk : undefined;
|
|
79
94
|
const bytes = chunk?.bytes;
|
|
80
95
|
|
|
81
|
-
|
|
96
|
+
const piece = bytes === undefined ? undefined : bytesToString(bytes);
|
|
97
|
+
|
|
98
|
+
if (piece) {
|
|
99
|
+
if (this.retention.accept(piece)) this.text += piece;
|
|
100
|
+
else this.textRejected = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
82
103
|
const attribution = isRecord(chunk?.attribution) ? chunk.attribution : undefined;
|
|
83
104
|
const citations = arrayValue<unknown>(attribution?.citations);
|
|
84
105
|
|
|
@@ -86,36 +107,86 @@ export class AgentStreamState implements StreamState {
|
|
|
86
107
|
|
|
87
108
|
const files = isRecord(raw.files) ? arrayValue<JsonValue>(raw.files.files) : undefined;
|
|
88
109
|
|
|
89
|
-
|
|
110
|
+
const sanitizedFiles = files?.map((file) =>
|
|
111
|
+
sanitizeOutputFile(file, this.contentCapture.output),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
if (sanitizedFiles && this.retention.accept(sanitizedFiles)) {
|
|
115
|
+
this.outputFiles.push(...sanitizedFiles);
|
|
116
|
+
}
|
|
90
117
|
|
|
91
118
|
const returnControl = isRecord(raw.returnControl) ? raw.returnControl : undefined;
|
|
92
119
|
|
|
93
|
-
if (returnControl)
|
|
120
|
+
if (returnControl) {
|
|
121
|
+
this.sawReturnControl = true;
|
|
122
|
+
|
|
123
|
+
// Return control replaces the text output, so it gets its own fresh bound and survives
|
|
124
|
+
// text that already spent the shared budget; one that alone does not fit is dropped and
|
|
125
|
+
// the capture is marked incomplete.
|
|
126
|
+
if (new StreamRetention().accept(returnControl)) {
|
|
127
|
+
this.returnControl = returnControl;
|
|
128
|
+
} else {
|
|
129
|
+
this.returnControl = undefined;
|
|
130
|
+
this.returnControlRejected = true;
|
|
131
|
+
this.retention.truncated = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
94
134
|
|
|
95
135
|
const traceEvent = isRecord(raw.trace) ? raw.trace : undefined;
|
|
96
136
|
|
|
97
137
|
if (traceEvent) {
|
|
98
138
|
this.traceEventCount += 1;
|
|
99
139
|
|
|
100
|
-
if (this.options.captureAgentTrace)
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
}
|
|
103
145
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
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);
|
|
154
|
+
}
|
|
155
|
+
|
|
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;
|
|
107
169
|
|
|
108
170
|
if (isRecord(guardrail))
|
|
109
171
|
this.guardrailAction = stringValue(guardrail.action) ?? this.guardrailAction;
|
|
110
172
|
}
|
|
111
173
|
}
|
|
112
174
|
|
|
175
|
+
outputTruncated(): boolean {
|
|
176
|
+
// Return control replaces the text output and is retained whole when it fits its bound.
|
|
177
|
+
if (this.returnControl !== undefined) return false;
|
|
178
|
+
|
|
179
|
+
return this.textRejected || this.returnControlRejected;
|
|
180
|
+
}
|
|
181
|
+
|
|
113
182
|
finish(): SpanFields {
|
|
114
183
|
const metadata = omitUndefined({
|
|
115
184
|
trace_event_count: this.traceEventCount || undefined,
|
|
185
|
+
failure: this.failed ? true : undefined,
|
|
186
|
+
failure_code: this.failureCode,
|
|
116
187
|
failure_reason: this.failureReason,
|
|
117
188
|
guardrail_action: this.guardrailAction,
|
|
118
|
-
return_control: this.
|
|
189
|
+
return_control: this.sawReturnControl ? true : undefined,
|
|
119
190
|
citation_count: this.citationCount || undefined,
|
|
120
191
|
output_files: this.outputFiles.length > 0 ? this.outputFiles : undefined,
|
|
121
192
|
agent_trace:
|
|
@@ -158,13 +229,13 @@ export class AgentStreamState implements StreamState {
|
|
|
158
229
|
}
|
|
159
230
|
}
|
|
160
231
|
|
|
161
|
-
function sanitizeOutputFile<T>(value: T): JsonValue {
|
|
232
|
+
function sanitizeOutputFile<T>(value: T, keepName: boolean): JsonValue {
|
|
162
233
|
const raw: unknown = value;
|
|
163
234
|
|
|
164
235
|
if (!isRecord(raw)) return undefined;
|
|
165
236
|
|
|
166
237
|
return omitUndefined({
|
|
167
|
-
name: stringValue(raw.name),
|
|
238
|
+
name: keepName ? stringValue(raw.name) : undefined,
|
|
168
239
|
type: stringValue(raw.type),
|
|
169
240
|
});
|
|
170
241
|
}
|
|
@@ -303,6 +374,7 @@ export function ragResponseFields<T>(output: T): SpanFields {
|
|
|
303
374
|
}
|
|
304
375
|
|
|
305
376
|
export class RagStreamState implements StreamState {
|
|
377
|
+
readonly retention = new StreamRetention();
|
|
306
378
|
private text = "";
|
|
307
379
|
private citationCount = 0;
|
|
308
380
|
private guardrailAction: string | undefined;
|
|
@@ -323,7 +395,9 @@ export class RagStreamState implements StreamState {
|
|
|
323
395
|
}
|
|
324
396
|
|
|
325
397
|
const output = isRecord(raw.output) ? raw.output : undefined;
|
|
326
|
-
|
|
398
|
+
const text = stringValue(output?.text);
|
|
399
|
+
|
|
400
|
+
if (text && this.retention.accept(text)) this.text += text;
|
|
327
401
|
|
|
328
402
|
if (raw.citation) this.citationCount += 1;
|
|
329
403
|
const guardrail = isRecord(raw.guardrail) ? raw.guardrail : undefined;
|
|
@@ -356,6 +430,7 @@ export function flowRequestFields(input: JsonRecord): SpanFields {
|
|
|
356
430
|
}
|
|
357
431
|
|
|
358
432
|
export class FlowStreamState implements StreamState {
|
|
433
|
+
readonly retention = new StreamRetention();
|
|
359
434
|
private outputs: unknown[] = [];
|
|
360
435
|
private completionReason: string | undefined;
|
|
361
436
|
private error: Error | undefined;
|
|
@@ -376,13 +451,16 @@ export class FlowStreamState implements StreamState {
|
|
|
376
451
|
|
|
377
452
|
const output = isRecord(raw.flowOutputEvent) ? raw.flowOutputEvent : undefined;
|
|
378
453
|
|
|
379
|
-
if (output) this.outputs.push(output.content);
|
|
454
|
+
if (output && this.retention.accept(output.content)) this.outputs.push(output.content);
|
|
380
455
|
|
|
381
456
|
const inputRequest = isRecord(raw.flowMultiTurnInputRequestEvent)
|
|
382
457
|
? raw.flowMultiTurnInputRequestEvent
|
|
383
458
|
: undefined;
|
|
384
459
|
|
|
385
|
-
if (inputRequest
|
|
460
|
+
if (inputRequest && this.retention.accept(inputRequest.content)) {
|
|
461
|
+
this.outputs.push(inputRequest.content);
|
|
462
|
+
}
|
|
463
|
+
|
|
386
464
|
const completion = isRecord(raw.flowCompletionEvent) ? raw.flowCompletionEvent : undefined;
|
|
387
465
|
this.completionReason = stringValue(completion?.completionReason) ?? this.completionReason;
|
|
388
466
|
}
|
|
@@ -407,35 +485,135 @@ export function ragName(input: JsonRecord): string {
|
|
|
407
485
|
return `retrieve_and_generate ${basenameArn(ragModel(input)) ?? "knowledge-base"}`;
|
|
408
486
|
}
|
|
409
487
|
|
|
410
|
-
|
|
411
|
-
const raw: unknown = value;
|
|
488
|
+
type AgentTraceCapture = Record<"input" | "output", boolean>;
|
|
412
489
|
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
}
|
|
415
497
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
+
};
|
|
420
587
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
}
|
|
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;
|
|
426
591
|
|
|
427
|
-
|
|
428
|
-
|
|
592
|
+
return shape.withheld === undefined ? undefined : projectTrace(value, shape.withheld, capture);
|
|
593
|
+
}
|
|
429
594
|
|
|
430
|
-
if (
|
|
595
|
+
if (shape === SCALAR) {
|
|
596
|
+
const scalar = ["string", "number", "boolean"].includes(typeof value) || value instanceof Date;
|
|
431
597
|
|
|
432
|
-
|
|
598
|
+
return scalar ? value : undefined;
|
|
599
|
+
}
|
|
433
600
|
|
|
434
|
-
|
|
435
|
-
|
|
601
|
+
if (Array.isArray(shape)) {
|
|
602
|
+
if (!Array.isArray(value)) return undefined;
|
|
436
603
|
|
|
437
|
-
|
|
604
|
+
return value
|
|
605
|
+
.map((item) => projectTrace(item, shape[0], capture))
|
|
606
|
+
.filter((item) => item !== undefined);
|
|
438
607
|
}
|
|
439
608
|
|
|
440
|
-
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
|
+
);
|
|
441
619
|
}
|
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) {
|
|
@@ -162,6 +170,42 @@ function withCacheInclusiveInput(fields: SpanFields | undefined): SpanFields | u
|
|
|
162
170
|
return { ...fields, usage: { ...usage, inputTokens, totalTokens } };
|
|
163
171
|
}
|
|
164
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
|
+
export 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
|
+
|
|
165
209
|
export function endSpan(span: SpanHandle, fields?: SpanFields): void {
|
|
166
210
|
try {
|
|
167
211
|
span.end(withCacheInclusiveInput(fields));
|
|
@@ -306,9 +350,69 @@ export function parseJson<T>(value: T): JsonValue {
|
|
|
306
350
|
}
|
|
307
351
|
}
|
|
308
352
|
|
|
353
|
+
// Retention uses fixed bounds; the core applies maxAttributeLength after the mask.
|
|
354
|
+
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
|
+
|
|
309
394
|
export interface StreamState {
|
|
310
395
|
feed<T>(event: T): boolean | void;
|
|
311
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;
|
|
312
416
|
}
|
|
313
417
|
|
|
314
418
|
export function wrapAsyncIterable<T>(
|
|
@@ -360,13 +464,13 @@ export function wrapAsyncIterable<T>(
|
|
|
360
464
|
completed = true;
|
|
361
465
|
} catch (error) {
|
|
362
466
|
endOnce(
|
|
363
|
-
mergeFields(state
|
|
467
|
+
mergeFields(finishStream(state, span, true), awsMetadataFields(metadataOf(error)), {
|
|
364
468
|
error: asError(error),
|
|
365
469
|
}),
|
|
366
470
|
);
|
|
367
471
|
throw error;
|
|
368
472
|
} finally {
|
|
369
|
-
if (!ended) endOnce(state
|
|
473
|
+
if (!ended) endOnce(finishStream(state, span, !completed));
|
|
370
474
|
}
|
|
371
475
|
};
|
|
372
476
|
|
|
@@ -378,7 +482,7 @@ export function wrapAsyncIterable<T>(
|
|
|
378
482
|
}
|
|
379
483
|
}
|
|
380
484
|
|
|
381
|
-
const finishAbandoned = () => endOnce(safe(() => state
|
|
485
|
+
const finishAbandoned = () => endOnce(safe(() => finishStream(state, span, true)));
|
|
382
486
|
wrapped[Symbol.asyncIterator] = () => {
|
|
383
487
|
const iterator = generator();
|
|
384
488
|
STREAM_FINALIZER?.register(iterator, finishAbandoned, unregisterToken);
|