Summary
The Bedrock ConverseStream instrumentation in TeeingSubscriber only accumulates delta.text from contentBlockDelta events. When the model returns tool use content blocks during streaming (a documented and supported Bedrock feature), the tool call data is silently dropped from the span output.
Non-streaming Converse calls handle tool use correctly — normalizeBedrockMessage() in InstrumentationSemConv already recognizes toolUse, toolResult, and image content block types and adds appropriate type annotations. The gap is exclusively in the streaming path.
What is missing
1. TeeingSubscriber only parses text deltas (lines 273–289)
case "contentBlockDelta" -> {
String t = parseDeltaText(payload); // only looks for delta.text
if (t != null) {
text.append(t);
// ...
}
}
parseDeltaText() (lines 322–339) searches exclusively for delta.text. Bedrock ConverseStream sends delta.toolUse for tool use blocks (containing incremental input JSON), which is silently ignored.
2. contentBlockStart events are not handled
The TeeingSubscriber switch (lines 273–289) has no case for contentBlockStart. This event carries toolUse.toolUseId and toolUse.name — metadata needed to reconstruct the tool call. Without it, even if tool use deltas were captured, the tool name and ID would be lost.
3. buildConverseJson always produces a single text block (lines 385–410)
gen.writeArrayFieldStart("content");
gen.writeStartObject();
gen.writeStringField("text", text); // always one text block
gen.writeEndObject();
gen.writeEndArray();
The synthetic response JSON always contains exactly one {"text": "..."} content block, regardless of what the model actually returned. When the model requests a tool call, the span output will show empty text instead of the tool invocation.
Impact
When a streaming Bedrock call triggers tool use (common in agentic workflows):
- The span's
output_json contains [{"role":"assistant","content":[{"text":"","type":"text"}]}] — as if the model returned nothing
- The
stopReason is captured as "tool_use", contradicting the empty output
- Tool call names, IDs, and argument JSON are permanently lost from the trace
- Non-streaming
Converse calls with tool use are correctly captured
Braintrust docs status
Upstream sources
Local files inspected
braintrust-sdk/instrumentation/aws_bedrock_2_30_0/src/main/java/dev/braintrust/instrumentation/awsbedrock/v2_30_0/BraintrustBedrockInterceptor.java — lines 273–289 (TeeingSubscriber.onNext: only contentBlockDelta→text, messageStop, metadata handled; no contentBlockStart), lines 322–339 (parseDeltaText: only delta.text), lines 385–410 (buildConverseJson: hardcoded single text block)
braintrust-sdk/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java — lines 400–434 (normalizeBedrockMessage: non-streaming path correctly handles toolUse, toolResult, image content block types)
braintrust-sdk/instrumentation/aws_bedrock_2_30_0/src/test/java/dev/braintrust/instrumentation/awsbedrock/v2_30_0/BraintrustAWSBedrockTest.java — no streaming test exercises tool use responses
Summary
The Bedrock
ConverseStreaminstrumentation inTeeingSubscriberonly accumulatesdelta.textfromcontentBlockDeltaevents. When the model returns tool use content blocks during streaming (a documented and supported Bedrock feature), the tool call data is silently dropped from the span output.Non-streaming
Conversecalls handle tool use correctly —normalizeBedrockMessage()inInstrumentationSemConvalready recognizestoolUse,toolResult, andimagecontent block types and adds appropriatetypeannotations. The gap is exclusively in the streaming path.What is missing
1.
TeeingSubscriberonly parses text deltas (lines 273–289)parseDeltaText()(lines 322–339) searches exclusively fordelta.text. Bedrock ConverseStream sendsdelta.toolUsefor tool use blocks (containing incrementalinputJSON), which is silently ignored.2.
contentBlockStartevents are not handledThe
TeeingSubscriberswitch (lines 273–289) has no case forcontentBlockStart. This event carriestoolUse.toolUseIdandtoolUse.name— metadata needed to reconstruct the tool call. Without it, even if tool use deltas were captured, the tool name and ID would be lost.3.
buildConverseJsonalways produces a single text block (lines 385–410)The synthetic response JSON always contains exactly one
{"text": "..."}content block, regardless of what the model actually returned. When the model requests a tool call, the span output will show empty text instead of the tool invocation.Impact
When a streaming Bedrock call triggers tool use (common in agentic workflows):
output_jsoncontains[{"role":"assistant","content":[{"text":"","type":"text"}]}]— as if the model returned nothingstopReasonis captured as"tool_use", contradicting the empty outputConversecalls with tool use are correctly capturedBraintrust docs status
Upstream sources
toolConfigparameter and streaming tool use events (contentBlockStartwithtoolUse,contentBlockDeltawithtoolUseinput)contentBlockStart(carriesstart.toolUsewithtoolUseIdandname),contentBlockDelta(carriesdelta.toolUsewithinputJSON fragments), andcontentBlockStopLocal files inspected
braintrust-sdk/instrumentation/aws_bedrock_2_30_0/src/main/java/dev/braintrust/instrumentation/awsbedrock/v2_30_0/BraintrustBedrockInterceptor.java— lines 273–289 (TeeingSubscriber.onNext: onlycontentBlockDelta→text,messageStop,metadatahandled; nocontentBlockStart), lines 322–339 (parseDeltaText: onlydelta.text), lines 385–410 (buildConverseJson: hardcoded single text block)braintrust-sdk/src/main/java/dev/braintrust/instrumentation/InstrumentationSemConv.java— lines 400–434 (normalizeBedrockMessage: non-streaming path correctly handlestoolUse,toolResult,imagecontent block types)braintrust-sdk/instrumentation/aws_bedrock_2_30_0/src/test/java/dev/braintrust/instrumentation/awsbedrock/v2_30_0/BraintrustAWSBedrockTest.java— no streaming test exercises tool use responses