Skip to content

ConverseStream never delivers ContentBlockStopEvent for tool_use content blocks — stream hangs until timeout. Help needed. #7136

Description

@ady88

Describe the bug

Summary

When calling BedrockRuntimeAsyncClient.converseStream() with a tool configuration, the SDK never delivers a ContentBlockStopEvent for tool_use content blocks. After the last ContentBlockDeltaEvent for the tool input, the stream goes completely silent — MessageStopEvent also never arrives. The
connection holds open until the HTTP read/SSE timeout fires.

Text content blocks (type=text) receive their ContentBlockStopEvent correctly. Only tool_use blocks are affected.

SDK version

software.amazon.awssdk:bedrockruntime:2.47.4

Region / Model

  • Region: eu-central-1
  • Model: eu.anthropic.claude-sonnet-4-6

Observed event sequence

  MessageStartEvent
  ContentBlockDeltaEvent   (text, index 0)   ← multiple tokens
  ContentBlockDeltaEvent   (text, index 0)
  ContentBlockStopEvent    (index 0)          ← text block closes correctly ✓
  ContentBlockStartEvent   (index 1, tool_use)
  ContentBlockDeltaEvent   (index 1, tool_use, partial_json="")
                                              ← SILENCE — no ContentBlockStopEvent
                                              ← no MessageStopEvent
                                              ← stream holds open until timeout

Expected event sequence

  MessageStartEvent
  ContentBlockDeltaEvent   (text, index 0)
  ...
  ContentBlockStopEvent    (index 0)
  ContentBlockStartEvent   (index 1, tool_use)
  ContentBlockDeltaEvent   (index 1, tool_use)
  ContentBlockStopEvent    (index 1)          ← never arrives
  MessageStopEvent                            ← never arrives
  ConverseStreamMetadataEvent

Minimal reproduction

var request = ConverseStreamRequest.builder()
     .modelId("eu.anthropic.claude-sonnet-4-6")
     .messages(Message.builder()
         .role(ConversationRole.USER)
         .content(ContentBlock.builder()
             .text("Please analyse my company benefit data and give me a short summary.")
             .build())
         .build())
     .toolConfig(ToolConfiguration.builder()
         .tools(Tool.builder()
             .toolSpec(ts -> ts
                 .name("CompanyBenefitData")
                 .description("Return benefit health data for a company.")
                 .inputSchema(is -> is.json(Document.fromMap(Map.of(
                     "type", Document.fromString("object"),
                     "properties", Document.fromMap(Map.of()),
                     "required", Document.fromList(List.of()))))))
             .build())
         .build())
     .build();

 asyncClient.converseStream(request, ConverseStreamResponseHandler.builder()
     .subscriber(event -> System.out.println("Event: " + event.getClass().getSimpleName()))
     .onComplete(() -> System.out.println("Complete"))
     .onError(t -> System.out.println("Error: " + t))
     .build())
     .get(90, TimeUnit.SECONDS);
 // Never prints "Complete" or "Error" within 90s on a tool_use turn
 // ContentBlockStopEvent for index 1 never appears in subscriber

Additional observations

  • The ConverseStream raw HTTP/2 connection does receive DATA frames from Bedrock after the last tool_use_delta (observed via Netty's DefaultHttp2ConnectionDecoder logging). Those frames are not translated into ContentBlockStopEvent by the SDK.
  • The synchronous converse() API correctly returns the complete response including tool_use blocks — only the streaming path is affected.
  • This appears to be an SDK-level translation issue: the raw Bedrock response contains the stop events but they are not surfaced to the ConverseStreamResponseHandler subscriber.

Regression Issue

  • Select this option if this issue appears to be a regression.

Expected Behavior

MessageStartEvent
ContentBlockDeltaEvent (text, index 0)
...
ContentBlockStopEvent (index 0)
ContentBlockStartEvent (index 1, tool_use)
ContentBlockDeltaEvent (index 1, tool_use)
ContentBlockStopEvent (index 1) ← never arrives
MessageStopEvent ← never arrives
ConverseStreamMetadataEvent

Current Behavior

After the last ContentBlockDeltaEvent for the tool_use block, the stream goes completely silent. ContentBlockStopEvent for index=1 never arrives. MessageStopEvent never arrives. onComplete is never called. The CompletableFuture returned by converseStream() hangs until the caller's timeout fires.

Observed SDK event log (INFO-level instrumentation on the ConverseStreamResponseHandler subscriber):

[bedrock-stream] event received: DefaultMessageStart
[bedrock-stream] event received: DefaultContentBlockDelta
[bedrock-stream] event received: DefaultContentBlockDelta
[bedrock-stream] event received: DefaultContentBlockDelta
[bedrock-stream] event received: DefaultContentBlockDelta
[bedrock-stream] event received: DefaultContentBlockDelta
[bedrock-stream] event received: DefaultContentBlockStop ← index 0 (text), correct
[bedrock-stream] ContentBlockStopEvent index=0
[bedrock-stream] event received: DefaultContentBlockStart
[bedrock-stream] ContentBlockStartEvent index=1 type=tool_use
[bedrock-stream] event received: DefaultContentBlockDelta ← tool input delta
← SILENCE from this point
← no ContentBlockStopEvent
← no MessageStopEvent
← onComplete never called
← onError never called

After a 60 s wall-clock wait:
java.util.concurrent.TimeoutException
at java.base/java.util.concurrent.CompletableFuture.timedGet(CompletableFuture.java:1957)
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2031)
at ConverseStreamToolUseBug.main(ConverseStreamToolUseBug.java:XX)

Reproduction Steps


  import java.util.List;
  import java.util.Map;
  import java.util.concurrent.TimeUnit;

  import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
  import software.amazon.awssdk.core.document.Document;
  import software.amazon.awssdk.regions.Region;
  import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeAsyncClient;
  import software.amazon.awssdk.services.bedrockruntime.model.ContentBlock;
  import software.amazon.awssdk.services.bedrockruntime.model.ConversationRole;
  import software.amazon.awssdk.services.bedrockruntime.model.ConverseStreamRequest;
  import software.amazon.awssdk.services.bedrockruntime.model.ConverseStreamResponseHandler;
  import software.amazon.awssdk.services.bedrockruntime.model.Message;
  import software.amazon.awssdk.services.bedrockruntime.model.Tool;
  import software.amazon.awssdk.services.bedrockruntime.model.ToolConfiguration;

  /**
   * Reproduces: ContentBlockStopEvent never delivered for tool_use blocks in ConverseStream.
   *
   * Dependencies (Gradle):
   *   implementation platform("software.amazon.awssdk:bom:2.47.4")
   *   implementation "software.amazon.awssdk:bedrockruntime"
   *   implementation "software.amazon.awssdk:netty-nio-client"
   *
   * Prerequisites:
   *   AWS credentials available via the default credentials chain
   *   (env vars, ~/.aws/credentials, or IAM role).
   *
   * Expected: subscriber prints ContentBlockStopEvent for index 1, then MessageStopEvent, then "onComplete".
   * Actual:   subscriber stops after ContentBlockDeltaEvent for index 1 — no ContentBlockStopEvent,
   *           no MessageStopEvent, no "onComplete" within the 60s timeout.
   */
  public class ConverseStreamToolUseBug {

      public static void main(String[] args) throws Exception {
          var client = BedrockRuntimeAsyncClient.builder()
                  .region(Region.EU_CENTRAL_1)
                  .credentialsProvider(DefaultCredentialsProvider.create())
                  .build();

          var request = ConverseStreamRequest.builder()
                  .modelId("eu.anthropic.claude-sonnet-4-6")
                  .messages(Message.builder()
                          .role(ConversationRole.USER)
                          .content(ContentBlock.builder()
                                  .build())
                          .build())
                  .toolConfig(ToolConfiguration.builder()
                          .tools(Tool.builder()
                                  .toolSpec(ts -> ts
                                          .name("CompanyBenefitData")
                                          .description("Returns benefit data for the user's company.")
                                          .inputSchema(is -> is.json(
                                                  Document.fromMap(Map.of(
                                                          "type", Document.fromString("object"),
                                                          "properties", Document.fromMap(Map.of()),
                                                          "required", Document.fromList(List.of()))))))
                                  .build())               
                          .build())
                  .build();
                  
          var handler = ConverseStreamResponseHandler.builder()
                  .subscriber(event -> System.out.println(
                          "[" + System.currentTimeMillis() + "] event: " + event.getClass().getSimpleName()))
                  .onComplete(() -> System.out.println("onComplete — stream finished normally"))
                  .onError(t -> System.out.println("onError — " + t.getClass().getSimpleName() + ": " + t.getMessage()))
                  .build();
                  
          System.out.println("Starting converseStream...");
          client.converseStream(request, handler).get(60, TimeUnit.SECONDS);
          client.close();
      }
  }

Observed output (actual):
Starting converseStream...
[..] event: DefaultMessageStart
[..] event: DefaultContentBlockDelta
[..] event: DefaultContentBlockDelta
[..] event: DefaultContentBlockStop ← index 0 (text) closes correctly
[..] event: DefaultContentBlockStart ← index 1 (tool_use) opens
[..] event: DefaultContentBlockDelta ← tool input delta
← silence until TimeoutException after 60s
← no DefaultContentBlockStop for index 1
← no DefaultMessageStop
← onComplete never called

Expected output:
[..] event: DefaultContentBlockStop ← index 1 (tool_use) should close here
[..] event: DefaultMessageStop
[..] event: DefaultConverseStreamMetadata
onComplete — stream finished normally


Possible Solution

I have done workarounds by implementing watchdog solution but it has downsides, don't have an ideea how could this be fixed.

Additional Information/Context

The synchronous BedrockRuntimeClient.converse() API with the identical request payload completes successfully and returns the full response including the tool_use content block — only the converseStream() path is affected.

AWS Java SDK version used

software.amazon.awssdk:bedrockruntime:2.47.4

JDK version used

java 25.0.1 Oracle

Operating System and version

MacOs 15.7.4

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugThis issue is a bug.needs-triageThis issue or PR still needs to be triaged.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions