Skip to content

Parallel tool calls causes Bedrock Converse ValidationException #664

Description

@alenaksu

Affected Model(s)

  • Claude (via us.anthropic.claude-sonnet-4-5-20250929-v1:0 cross-region inference profile)

Describe the bug

When a model returns more than one toolUse block in a single assistant turn, the message converter (toAwsBedrockMessage) emits one separate user message per tool result instead of combining them all into a single user message. This produces two violations:

  1. Missing toolResult ids in messages[2] — Bedrock Converse (for Claude) enforces the Anthropic Messages contract: all toolResult blocks answering a given assistant turn must appear together in the immediately following user message. The second result landing in messages[3] instead triggers a ValidationException (HTTP 400):

    Expected toolResult blocks at messages.2.content for the following Ids: tooluse_<id>
    
  2. Two consecutive user messages — independently violates Bedrock Converse's general user/assistant role-alternation requirement, regardless of model provider.

The failure is deterministic and reproduces on every retry since the message history is replayed unchanged.

To Reproduce

  1. Define a Genkit flow using genkitx-aws-bedrock with two or more tools and a structured JSON output schema.
  2. Prompt the model in a way that causes it to call two tools in a single turn (parallel tool calls).
  3. Both tools resolve successfully; Genkit sends the follow-up turn with both toolResponses in one role: "tool" message.
  4. The Converse API throws ValidationException: Expected toolResult blocks at messages.2.content ....

Root cause

In the case "tool": branch of toAwsBedrockMessage (lib/esm/aws_bedrock_llms.mjs ~L1224, lib/cjs/aws_bedrock_llms.js ~L1277), each tool response part is pushed as its own user message:

toolResponseParts.map((part) => {
    const toolresult = {
        role: "user",
        content: [{ toolResult: { toolUseId: part.toolResponse.ref, ... } }],
    };
    awsBedrockMsgs.push(toolresult); // one message per part
});

For an assistant turn with 2 toolUse blocks this produces:

  • messages[1] — assistant with 2 toolUse blocks
  • messages[2] — user with only the first toolResult
  • messages[3] — user with the second toolResult

Bedrock requires messages[2] to contain a toolResult for every toolUse id in messages[1]. Because the second id lands in messages[3], validation fails. Two consecutive user messages also violate Converse's strict role alternation.

Expected behavior

All toolResult blocks from a single Genkit role: "tool" message should be combined into one user message:

case "tool": {
    const toolResponseParts = msg.toolResponseParts();
    if (toolResponseParts.length > 0) {
        const content = toolResponseParts.map((part) => ({
            toolResult: {
                toolUseId: part.toolResponse.ref,
                content: [{ json: { result: part.toolResponse.output } }],
            },
        }));
        awsBedrockMsgs.push({ role: "user", content });
    }
    break;
}

This yields a single user message whose content holds a toolResult for each id, satisfying both the Converse role-alternation rule and the Anthropic Messages contract enforced by Converse for Claude models.

Screenshots

N/A

Plugin(s) version:

genkitx-aws-bedrock 1.19.1

Genkit version:

genkit 1.39.x

Additional context

  • Single tool-call turns are unaffected — only one result to convert, so the bug only surfaces once the model emits parallel tool calls.
  • Happy to open a PR with the fix above if useful.

References

The error is a Bedrock Converse ValidationException (HTTP 400). AWS documents the correct shape by example but does not state the grouping rule in plain language; the clearest explicit statement is in the Anthropic docs, because Converse proxies the Anthropic Messages contract for Claude models.

  • Bedrock Converse ValidationExceptionConverse API reference: confirms ValidationException is HTTP 400 and is raised when the input fails to satisfy Converse's constraints.

  • Message.content is an arrayMessage API reference: confirms content is an array of ContentBlock, meaning multiple toolResult blocks belong together in one message's content array.

  • Correct tool-result shape (by example)Client-side tool use (Converse): the official Converse example appends results as a single user message with an array of toolResult content blocks.

  • Explicit grouping rule + wrong/correct exampleAnthropic: Parallel tool use → Troubleshooting: states the rule plainly and shows the exact wrong pattern the plugin currently produces:

    Whichever strategy you use, return one tool_result for each tool_use block, all together in the next user message.

    // Wrong: separate user messages (what the plugin currently produces)
    [
      {"role": "assistant", "content": [tool_use_1, tool_use_2]},
      {"role": "user", "content": [tool_result_1]},
      {"role": "user", "content": [tool_result_2]}
    ]
    
    // Correct: one user message with all results
    [
      {"role": "assistant", "content": [tool_use_1, tool_use_2]},
      {"role": "user", "content": [tool_result_1, tool_result_2]}
    ]

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions