Bedrock Converse API: Tracking Down "No Response from the 10th Call" — Pitfalls When Using Extended Thinking × Tool Use

Bedrock Converse API: Tracking Down "No Response from the 10th Call" — Pitfalls When Using Extended Thinking × Tool Use

# Bedrock Converse API: Extended Thinking × Tool Use Silent Failure Investigation ## Problem Overview When combining Extended Thinking with tool use in the Bedrock Converse API, responses silently stop after the 10th exchange. No error is displayed to the user — the system simply falls silent. --- ## System Architecture ``` User → API Gateway → Lambda → Bedrock Converse API ↓ DynamoDB (conversation history) ↓ CloudWatch Logs ↑ Model Invocation Logging (S3/CloudWatch) ``` --- ## Investigation Process ### Step 1: CloudWatch Log Analysis ```python # Suspicious log pattern { "timestamp": "2024-01-15T10:23:45Z", "requestId": "abc-123", "response": "", # Empty string — silent failure "statusCode": 200, # 200 returned — no apparent error "turnCount": 11 } ``` The Lambda function was returning HTTP 200 with an empty response body. The problem was completely invisible from the application layer. ### Step 2: Model Invocation Logging Deep Dive Enabling Model Invocation Logging revealed the true error: ```json { "schemaType": "ModelInvocationLog", "schemaVersion": "1.0", "errorCode": "ValidationException", "errorMessage": "messages: roles must alternate between user and assistant...", "inputTokenCount": 0, "outputTokenCount": 0 } ``` **Key insight**: The error was occurring inside Bedrock before any tokens were generated — meaning Lambda's try-catch was not catching it at all. ### Step 3: DynamoDB History Reconstruction Retrieving conversation history from DynamoDB and visualizing the structure: ``` Turn 1: User → Assistant (reasoning + text) Turn 2: User → Assistant (reasoning + text + tool_use) Turn 3: User(tool_result) → Assistant (reasoning + text) ... Turn 9: User(tool_result) → Assistant (reasoning + text + tool_use) Turn 10: User(tool_result) → [FAILURE] ``` The pattern became clear — failures occurred consistently after tool result exchanges. --- ## Root Cause Analysis ### The Core Mechanism The conversation history stored in DynamoDB had the following structure for an assistant turn that used tools: ```python # What was being saved to DynamoDB { "role": "assistant", "content": [ { "reasoningContent": { "reasoningText": { "text": "Let me think about this...", "signature": "ErUBCkQIARAA..." } } }, { "toolUse": { "toolUseId": "tool_001", "name": "search", "input": {"query": "..."} } } ] } ``` After receiving the tool result and building the next request, `toolUse` blocks were being stripped: ```python # Actual buggy code def build_next_message(history, tool_result): messages = [] for msg in history: if msg["role"] == "assistant": # Filter to "clean up" the message filtered_content = [ block for block in msg["content"] if block.get("type") != "tool_use" # ← Strip tool_use ] messages.append({ "role": "assistant", "content": filtered_content }) return messages ``` This produced: ```python # What was actually being sent to Bedrock { "role": "assistant", "content": [ # toolUse removed → only reasoningContent remains { "reasoningContent": { "reasoningText": { "text": "Let me think about this...", "signature": "ErUBCkQIARAA..." } } } # ← toolUse block is gone ] } ``` ### Why the ValidationException Occurs The Bedrock Converse API with Extended Thinking enforces a strict rule: ``` [Bedrock Validation Rule] When an assistant message contains reasoningContent, the following constraints apply: 1. reasoningContent MUST be followed by either: - A text block, OR - A toolUse block 2. reasoningContent blocks CANNOT appear consecutively 3. A message containing only reasoningContent is INVALID ``` What was being sent had two consecutive `reasoningContent` blocks across message boundaries — violating rule #2: ``` [Actual request structure sent] Turn N assistant: └─ reasoningContent (thinking) ← Block 1 (toolUse stripped here) Turn N+1 user: └─ toolResult Turn N+1 assistant: └─ reasoningContent (thinking) ← Block 2: consecutive reasoning! └─ text → ValidationException ``` ### Why It Only Failed After Turn 10 ```python # Frequency of tool use calls per turn (example data) turn_tool_usage = { 1: False, # No tool 2: False, 3: True, # First tool use → history contamination begins 4: False, 5: True, 6: False, 7: True, 8: False, 9: True, 10: True, # Contaminated history accumulates 11: "FAIL" # Threshold exceeded → ValidationException } ``` The issue was cumulative — each tool use turn added another corrupted block to the history, and validation only failed once enough had accumulated. --- ## The Cascade Failure Mechanism ``` [Cascade Failure Flow] Phase 1: Normal Operation (Turns 1-8) ┌─────────────────────────────────────┐ │ Tool use occurs → history saved │ │ toolUse block stripped on next call│ │ → reasoningContent left isolated │ │ But validation doesn't fail yet │ └─────────────────────────────────────┘ ↓ Phase 2: Contamination Accumulates (Turns 9-10) ┌─────────────────────────────────────┐ │ Multiple isolated reasoningContent │ │ blocks accumulate in history │ │ Approaching validation threshold │ └─────────────────────────────────────┘ ↓ Phase 3: ValidationException (Turn 11) ┌─────────────────────────────────────┐ │ Bedrock rejects the request │ │ Lambda catches exception │ │ But error handling returns "" │ │ → Silent failure to user │ └─────────────────────────────────────┘ ↓ Phase 4: History Corruption Persists ┌─────────────────────────────────────┐ │ Failed turn is still saved to DB │ │ Every subsequent turn also fails │ │ → Permanent silent failure │ └─────────────────────────────────────┘ ``` --- ## The Fix ### Correct Implementation ```python def build_messages_for_bedrock(history: list[dict]) -> list[dict]: """ Correctly preserve reasoning + tool pairs """ messages = [] for msg in history: if msg["role"] != "assistant": messages.append(msg) continue content_blocks = msg.get("content", []) # Identify reasoningContent + toolUse pairs cleaned_blocks = [] i = 0 while i < len(content_blocks): block = content_blocks[i] if "reasoningContent" in block: # Check what follows next_block = content_blocks[i + 1] if i + 1 < len(content_blocks) else None if next_block and "toolUse" in next_block: # Preserve the pair together — do NOT strip toolUse cleaned_blocks.append(block) # reasoningContent cleaned_blocks.append(next_block) # toolUse (must keep) i += 2 elif next_block and "text" in next_block: # reasoning + text is valid cleaned_blocks.append(block) cleaned_blocks.append(next_block) i += 2 else: # Isolated reasoningContent — this is the corrupted state # Log and skip to prevent cascade logger.warning( "Isolated reasoningContent detected", extra={"block_index": i, "msg_role": msg["role"]} ) i += 1 else: cleaned_blocks.append(block) i += 1 if cleaned_blocks: messages.append({ "role": "assistant", "content": cleaned_blocks }) return messages def validate_message_structure(messages: list[dict]) -> tuple[bool, str]: """ Pre-validate before sending to Bedrock """ for i, msg in enumerate(messages): if msg["role"] != "assistant": continue content = msg.get("content", []) for j, block in enumerate(content): if "reasoningContent" not in block: continue # Check what follows reasoningContent next_block = content[j + 1] if j + 1 < len(content) else None if next_block is None: return False, f"Turn {i}: reasoningContent has no following block" if "reasoningContent" in next_block: return False, f"Turn {i}: consecutive reasoningContent blocks" if "toolUse" not in next_block and "text" not in next_block: return False, f"Turn {i}: reasoningContent followed by invalid block type" return True, "OK" ``` ### Saving History Correctly ```python def save_turn_to_dynamodb( table, session_id: str, turn_number: int, user_message: dict, assistant_response: dict, tool_interactions: list[dict] | None = None ): """ Save conversation history with structural integrity guaranteed """ turn_data = { "sessionId": session_id, "turnNumber": turn_number, "userMessage": user_message, "assistantResponse": assistant_response, # Explicitly preserve tool interactions as a separate field "toolInteractions": tool_interactions or [], # Record structure metadata for debugging "contentBlockTypes": [ list(block.keys())[0] for block in assistant_response.get("content", []) ], "hasToolUse": any( "toolUse" in block for block in assistant_response.get("content", []) ), "timestamp": datetime.utcnow().isoformat() } table.put_item(Item=turn_data) ``` --- ## Monitoring and Alerting ### CloudWatch Metric Filter ```python # Detect ValidationException pattern METRIC_FILTER_PATTERN = """ { $.errorCode = "ValidationException" && $.errorMessage = "*reasoningContent*" } """ # Alarm configuration alarm_config = { "AlarmName": "BedrockExtendedThinking-ValidationException", "MetricName": "ReasoningValidationError", "Threshold": 1, "EvaluationPeriods": 1, "ComparisonOperator": "GreaterThanOrEqualToThreshold", "TreatMissingData": "notBreaching" } ``` ### Structured Logging for Tracing ```python import structlog logger = structlog.get_logger() def invoke_bedrock_with_tracing(messages, session_id, turn_number): # Validate before sending is_valid, validation_msg = validate_message_structure(messages) logger.info( "bedrock_invocation_start", session_id=session_id, turn_number=turn_number, message_count=len(messages), pre_validation=validation_msg, content_structure=[ { "role": m["role"], "block_types": [list(b.keys())[0] for b in m.get("content", [])] } for m in messages ] ) if not is_valid: logger.error( "bedrock_pre_validation_failed", session_id=session_id, turn_number=turn_number, reason=validation_msg ) raise ValueError(f"Message structure validation failed: {validation_msg}") try: response = bedrock_client.converse( modelId=MODEL_ID, messages=messages, # ... other params ) logger.info( "bedrock_invocation_success", session_id=session_id, turn_number=turn_number, input_tokens=response["usage"]["inputTokens"], output_tokens=response["usage"]["outputTokens"] ) return response except bedrock_client.exceptions.ValidationException as e: logger.error( "bedrock_validation_exception", session_id=session_id, turn_number=turn_number, error=str(e), # Dump full message structure for post-mortem message_dump=messages ) raise ``` --- ## Key Takeaways | Point | Detail | |---|---| | **Root cause** | `toolUse` blocks stripped from history, leaving `reasoningContent` isolated | | **Why silent** | Lambda caught the exception but returned `""` — user saw nothing | | **Why turn 10+** | Corrupted blocks accumulate until validation threshold is crossed | | **Detection method** | Model Invocation Logging (not CloudWatch alone) | | **Prevention** | Pre-validate message structure before every Bedrock call | ### The Three Rules for Extended Thinking + Tool Use ``` Rule 1: reasoningContent must ALWAYS be paired with what follows it → Never strip toolUse that comes after reasoningContent Rule 2: Validate message structure BEFORE sending to Bedrock → Catch issues at the application layer, not inside Bedrock Rule 3: Enable Model Invocation Logging → Standard CloudWatch logs will NOT show these errors ``` --- Extended Thinking is powerful, but its message structure constraints are strict. The combination with tool use creates subtle history management pitfalls that only surface after many turns — making Model Invocation Logging an essential debugging tool, not an optional one.
2026.06.28

This page has been translated by machine translation. View original

TL;DR — Notes on Using Extended Thinking × Tool Use

A summary for those who aren't interested in the investigation process and just want the conclusion.

Problem: When using Extended Thinking (reasoningContent) together with tool use in the Bedrock Converse API, a ValidationException occurs when reasoningContent blocks become consecutive due to manipulation of the conversation history.

Cause: reasoningContent blocks are given a cryptographic signature (signature). This signature proves the authenticity of the block (that it was generated by Claude) and is not a hash of the text content. The API structurally validates not only the authenticity of the signature, but also whether the consecutive pattern of reasoning blocks matches the model's original output. When toolUse/toolResult blocks are excluded from the conversation history, reasoningContent blocks that were originally non-consecutive become adjacent, creating a consecutive pattern that did not exist in the original output, causing structural validation to fail.

Model output:  [reasoning_A, toolUse, reasoning_B, text]
                              ↓ toolUse excluded
After filter:  [reasoning_A, reasoning_B, text]
               ^^^^^^^^^^^^^^^^^^^^^^^^
               Consecutive pattern not present in model output → ValidationException

Countermeasures:

  1. If reasoningContent blocks become consecutive after filtering, combine the text into a single block (since the signature does not validate text content, retaining either signature is sufficient)
  2. Exclude messages whose content becomes empty after filtering from the conversation history (to prevent cascade failures)

Reference: Anthropic Official Documentation — Extended thinking

the entire sequence of consecutive thinking blocks must match the outputs generated by the model during the original request

Introduction

"After asking the chat assistant about 10 questions, it stopped responding from the 11th question onward. No errors are displayed."

When I received this report, my first hypothesis was "context window exceeded." I figured it had hit the token limit after 10 exchanges and was producing an error about input being too long.

As it turned out, that hypothesis was wrong.

The actual cause was a violation of the message structure constraints in the Bedrock Converse API, and reaching that conclusion required an investigation spanning multiple data sources: CloudWatch, DynamoDB, Bedrock Model Invocation Logging, and direct API calls. This article walks through that investigation process.

Organizing the Symptoms

The application where the problem occurred is a chat assistant using the Bedrock Converse API. It operates with the following configuration:

  • Model: Claude Sonnet 4 (Extended Thinking enabled)
  • Tool use: Tools such as database queries via Function Calling
  • Conversation history: Stored in DynamoDB, with the full history sent to the API on every request

Reported symptoms:

  • Responds normally up to about 10 questions
  • From the 11th question onward, no error displayed, the next question can be sent immediately
  • No application crashes or error screens

The "no error displayed" aspect was tricky.

Chapter 1: Discovering the Silent Error

Nothing in CloudWatch

First I checked CloudWatch Logs. I searched the application's log group for logs around the relevant time, but found no WARN (level 40) or higher logs at all.

fields @timestamp, @message
| filter level >= 40
| sort @timestamp desc
| limit 100

Result: 0 entries. All logs were INFO (level 30) only.

SCR-20260612-naan (1)

There Were Clues in DynamoDB

Next I checked the chat history table in DynamoDB. This application stores chat messages encoded with gzip compression + Base64. When decoded:

[
  { "type": "note", "key": "InternalServerError" },
  { "type": "note", "key": "InternalServerError" },
  { "type": "note", "key": "InternalServerError" }
]

The errors were being saved to DynamoDB, but were not being sent to the client.

Looking at the code, there was a problem with the error handling when an error occurred during streaming response processing. While error information was being saved to the DB, both the SSE transmission to the client and log output were missing.

This is a pitfall specific to streaming processing. With a normal request-response, you can return an error via HTTP status code, but errors that occur during SSE streaming happen "after you've already started returning a response," so dedicated processing to notify the error is required. This notification processing was missing, causing the catch block to swallow the error without reaching the outer error handler.

19db6137-ffd2-4bf7-9f8e-480e45a55bbb

Learnings at This Point

  • No visible error ≠ No error occurring: The existence of the error could only be confirmed by directly checking the data in the persistence layer
  • Silent catch blocks are dangerous: If you catch an error, you must always both log it and notify the user

Chapter 2: The True Nature of the ValidationException

When I found InternalServerError in DynamoDB, I still suspected "context window exceeded." However, estimating the token count of stored messages revealed that only about 15% of the 200K token limit was being used. I needed to look for a different cause.

Item Size
System prompt ~10,000 characters
Total text content ~26,000 characters
Total reasoning text ~7,000 characters
Total reasoning signatures ~19,000 characters

Totaling approximately 62,000 characters (≈ 20,000–30,000 tokens). The model in use was Claude Sonnet 4.6 (context window 200K tokens), revealing that only about 15% of the limit was being used.

The application logs contained no error details whatsoever, and only the code InternalServerError was stored in DynamoDB. To identify the actual error, I enabled Bedrock Model Invocation Logging.

Enabling Bedrock Model Invocation Logging

  1. Create a CloudWatch Logs log group: Retention period of 1 day (for temporary debugging)
    SCR-20260612-nojq

  2. Enable in Bedrock settings: Amazon Bedrock → Settings → Model invocation logging
    SCR-20260612-niuc

  3. Select logging destination: Select CloudWatch Logs only
    SCR-20260612-nnli

  1. Create an IAM role: A service role for Bedrock to write logs
    SCR-20260612-nnlx

  2. Check in CloudWatch: Select the log group created in CloudWatch and review the logs
    SCR-20260612-nuar (1)

The Error Type Identified

After reproducing the issue in the staging environment, checking the Invocation Log revealed:

{
  "operation": "ConverseStream",
  "modelId": "jp.anthropic.claude-sonnet-4-6",
  "errorCode": "ValidationException"
}

It was ValidationException, not InternalServerError. InternalServerError was the code assigned by the application-side catch block; the actual error type returned by the Bedrock API was ValidationException, indicating a request structure constraint violation.

Unfortunately, Bedrock Model Invocation Logging does not record the request body or detailed error messages when an error occurs. However, the body of the immediately preceding successful request is fully recorded, so I proceeded with the investigation using this as a clue.

Chapter 3: The Root Cause — Consecutive Reasoning Blocks

The Conversation History Filter Processing

First, some background. When sending conversation history to the Bedrock API, this application was selecting only the content types that need to be sent to the API (text, image, attachment, reasoning) using an allowlist approach.

This allowlist was designed when the application had no tool use functionality. At the time, only text and attachment existed, and an allowlist was sufficient. When reasoning (Extended Thinking) and image were subsequently added, they were added to the list, but tool blocks introduced later were left without being added to the allowlist.

e58acfeb-e45d-4853-a88a-e4b282c6b4d8

tool blocks are UI display metadata that holds the tool execution state within the application (tool name, parameters, results) and do not need to be sent to the Bedrock API, so there is normally no problem with them not being included in the allowlist.

However, this design had an unexpected side effect.

The Difference Between Successful Requests and Stored Data

By comparing successful requests (Invocation Log) with stored messages (DynamoDB), a decisive discovery was made.

Through the allowlist filter, in most cases this works without issue. However, when the model "reconsiders" between tool calls — for example, inserting reasoning before calling another tool after seeing a tool execution result — the tool block may be the only separator between reasoning blocks:

SCR-20260628-soxf

Verifying the Hypothesis via AWS CLI

The analysis up to this point led to the hypothesis that "consecutive reasoning blocks cause an error." However, the detailed error message could not be obtained from Bedrock Model Invocation Logging. To confirm the hypothesis, I sent test payloads directly to the Bedrock Converse API via AWS CLI.

As the base for testing, I used the payload from the last successful request obtained from Invocation Logging. Since this payload contains actual signed reasoning blocks, it can accurately verify the API's constraints.

aws bedrock-runtime converse \
  --region ap-northeast-1 \
  --model-id jp.anthropic.claude-sonnet-4-6 \
  --cli-input-json file://test-payload.json

Four tests were conducted, yielding the following results:

Test Payload Content Result
Baseline Successful request as-is (tool blocks excluded, reasoning non-consecutive) Success
Test 1 Make reasoning blocks consecutive (delete text between them) ValidationException
Test 2 Make reasoning consecutive in a past assistant message (not the latest) ValidationException
Test 3 Set assistant message content to an empty array ValidationException

Test 1 result:

An error occurred (ValidationException) when calling the Converse operation:
The model returned the following errors:
messages.1.content.1: `thinking` or `redacted_thinking` blocks in the
latest assistant message cannot be modified. These blocks must remain
as they were in the original response.

Comparing the baseline and Test 1 confirmed that the position (index) of blocks shifting itself is not a problem — an error occurs only when reasoning blocks are adjacent.

From Test 2's result, it became clear that the API validates not only the latest assistant message, but all assistant messages in the conversation history. This means that if even one message in the history has consecutive reasoning blocks, all subsequent requests will fail.

The True Nature of the Signature — Clarifying the API Validation Mechanism Through Experiments

From the error message These blocks must remain as they were in the original response, it is clear that signature-based validation is involved. However, what exactly the signature validates is not clear from documentation alone.

The official Anthropic documentation states:

the entire sequence of consecutive thinking blocks must match the outputs generated by the model during the original request; you cannot rearrange or modify the sequence of these blocks

To determine what this "sequence" refers to — whether it's the text content or the block structure — I conducted additional experiments to identify what the signature validates.

What Does the Signature Validate?

Using the earlier test payload (two consecutive reasoning blocks [reasoning_A, reasoning_B, text]), I conducted four additional tests related to signatures:

Test Operation Result
Test 4 Rewrite reasoning_A's text to completely different content, signature unchanged Success
Test 5 Swap the signatures of reasoning_A and reasoning_B (text unchanged) Success
Test 6 Combine the text of two reasoning blocks into one block, using either signature Success
Test 7 Use a completely forged signature string ValidationException

Test 7 error message:

messages.1.content.0: Invalid `signature` in `thinking` block

From these results, the role of the signature became clear:

1. The signature is not a hash of the text content

In Test 4, completely rewriting the text still succeeded, and in Test 5, swapping signatures also succeeded. The signature is not tied to the content of reasoningText.

2. The signature is a proof of authenticity that "Claude generated this"

Only Test 7 with a forged signature failed. The role of the signature is to prove that the block was generated by the Claude API (an authenticity proof). Conceptually it is similar to a JWT (JSON Web Token), signed with the server's private key and verified with the same key.

7e5afa3f-21d2-47b6-9464-831c34e767b6

3. The API is stateless — the signature encapsulates the "state"

LLM APIs are inherently stateless. Without storing conversation history on the server side, how can it verify "whether it matches the original output"? The answer is that the signature itself encapsulates the information needed for verification. It is the same mechanism by which JWT can verify token authenticity without a server-side session store.

aef38f7e-fa68-4db2-8ce2-42ecd263f7f4

4. Consecutive pattern validation is a separate structural check from the signature

The signature validates "whether the block was generated by Claude" and the consecutive pattern validation validates "whether the structural pattern matches the model's original output." These are two separate layers of validation:

  • Signature validation (confirmed in Test 7): Whether the block was generated by the Claude API
  • Structural validation (confirmed in Test 1): Whether the consecutive pattern of reasoning blocks matches the model's output

d17f4b04-6cde-4f17-a9c8-870258666ce0

What the Test Results Mean

This finding directly affects countermeasures. Since the signature does not validate text content, it is possible to combine the text of consecutive reasoning blocks into a single block (confirmed in Test 6). This is a superior countermeasure compared to simply deleting blocks, as it resolves the consecutive pattern without losing the model's thinking context.

Note that the same documentation permits entirely omitting thinking blocks from previous turns (except when using tools). What becomes a problem is "creating a consecutive pattern that did not exist in the original output."

Test 3 result:

An error occurred (ValidationException) when calling the Converse operation:
The content field in the Message object at messages.1 is empty.
Add a ContentBlock object to the content field and try again.

This also confirmed the cascade failure mechanism (details in Chapter 4).

bedrock-converse-extended-thinking-tool-use-validation-exception-reasoning-consecutive

Why It Occurs at a Specific Number of Exchanges

This problem does not occur with every exchange. The trigger is a pattern where only a reasoning block is inserted between tool calls.

For example, when a tool call fails and the model retries:

  1. reasoning (thinking about query) → text → tool (executed, failed)
  2. reasoning (thinking about correction) → tool (re-executed, failed)
  3. reasoning (thinking about further correction) → tool (succeeded) → text (explaining results)

In step 2, there is no text block between reasoning and tool, so excluding the tool results in consecutive reasoning blocks.

In early exchanges, text blocks often exist before and after tool blocks, so reasoning does not become consecutive after filtering. As the number of exchanges increases, tool retries and compound calls occur, raising the probability that this pattern will appear. The reproducibility of "around the 10th question" is the reason for this.

bb436225-2b4b-419e-9db8-75be4ab3ce5c

Chapter 4: Cascade Failure — Once It Breaks, It Stays Broken Permanently

In addition to the root cause, a cascade failure occurs where once a failure happens, all subsequent requests fail permanently. This made the problem even more serious.

This failure pattern is not limited to this case — it can occur in any chat application that persists conversation history and resends it each time. If an incomplete assistant message is saved when an error occurs, that broken message will continue to be included in all subsequent requests.

ce58319a-7144-4655-b45e-ba93707709f6

The mechanism in this case:

  1. On the initial failure, the assistant message is saved in an incomplete state (with no valid content blocks)
  2. On the next request, all blocks of this message are excluded by the filter, sending an assistant message with empty content to Bedrock
  3. Empty content also causes ValidationException → permanent failure loop
// After filtering, content becomes empty
{
  "role": "assistant",
  "content": []  // Violates Bedrock API constraints
}

As confirmed in Test 3 from the previous chapter, an empty content array also returns a ValidationException.

In other words, even if the root cause (consecutive reasoning blocks) is fixed, chats that failed in the past remain permanently broken. Unless the processing to skip empty-content messages is also addressed, existing broken chats cannot be recovered.

bedrock-converse-extended-thinking-tool-use-validation-exception-cascade-failure

Fixes and Countermeasures

Fix ①: Resolving Silent Errors

Log output and client notification were added to the error handling during streaming processing. Errors during SSE streaming need to be notified through a different path than normal HTTP error responses, making this an easy point to overlook.

Fix ②: Resolving the Root Cause

When sending conversation history to the Bedrock API, the following two points need to be addressed in the filter processing.

1. Resolving consecutive reasoning blocks

When reasoning blocks become consecutive after excluding tool blocks, combine the consecutive reasoning blocks into one. As noted above, since the signature does not validate text content, combining the text and retaining either signature will pass validation. This allows the consecutive pattern to be resolved while preserving the model's thinking context.

// Example of resolving consecutive reasoning after excluding tool blocks
function sanitizeContentBlocks(blocks: ContentBlock[]): ContentBlock[] {
  const filtered = blocks.filter(b => b.type !== 'toolUse' && b.type !== 'toolResult');

  // Combine consecutive reasoning blocks' text into a single block
  const result: ContentBlock[] = [];
  for (const block of filtered) {
    const prev = result[result.length - 1];
    if (prev?.type === 'reasoning' && block.type === 'reasoning') {
      prev.reasoningText += '\n\n' + block.reasoningText;
      // Since the signature is not tied to text content, keep the first block's signature as-is
    } else {
      result.push({ ...block });
    }
  }
  return result;
}

2. Skipping empty-content messages

Messages whose content becomes empty after filtering are excluded from the conversation history. This prevents cascade failures from chats that failed in the past.

// Example of skipping messages with empty content
const messages = history
  .map(msg => ({ ...msg, content: sanitizeContentBlocks(msg.content) }))
  .filter(msg => msg.content.length > 0);

Reflection on the Investigation Process

Here is a summary of the methods used in this investigation and the effectiveness of each.

Method What It Revealed Limitations
CloudWatch Logs The fact that no logs were being output was itself a clue No direct information since errors were caught and not logged
DynamoDB Error codes, overall message structure Detailed error messages were not saved
Bedrock Model Invocation Logging Actual error type (ValidationException), successful request payload Request body and error messages at failure time are not recorded
Cross-referencing DynamoDB × code × Invocation Log Identification of root cause
Direct API calls via AWS CLI (7 patterns) Exact wording of error messages, confirmation of hypothesis, that all messages are validated, identification of what the signature validates

bedrock-converse-extended-thinking-tool-use-validation-exception-investigation-flow

The most effective approach was "cross-referencing multiple data sources." A single log source did not reveal the full picture; the cause could only be identified by combining the stored data in DynamoDB × the successful payload from the Invocation Log × static analysis of the code.

Summary

For Bedrock Converse API Users

  • When combining Extended Thinking with tool use, be careful to ensure that reasoningContent blocks do not become consecutive when reconstructing conversation history
  • When filtering specific content types from conversation history, verify that the block order after filtering satisfies the API's constraints
  • The signature of a reasoning block is not a hash of the text content, but a proof of authenticity that Claude generated it. Combining or rewriting text is permitted, but forged signatures or consecutive patterns that did not exist in the original output will be rejected

As a Debugging Methodology

  • Suspect silent errors: Even when no error is visible to the user, error information may remain in the persistence layer
  • Reject hypotheses quickly: Rather than being fixated on the assumption of "context window exceeded," the token count should have been measured and rejected early
  • Make use of Bedrock Model Invocation Logging: The most direct means of verifying the reality of API calls. Since it can be temporarily enabled and immediately disabled, it should be actively used during debugging
  • Cross-reference multiple data sources: When a single log source is insufficient, analyze across stored data, application logs, and service logs

Share this article

AWSのお困り事はクラスメソッドへ