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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions