Skip to content

fix(bedrock): coerce replayed toolUse.name to the Converse name pattern - #122253

Open
JiaDe-Wu wants to merge 1 commit into
NousResearch:mainfrom
JiaDe-Wu:fix/bedrock-replayed-tool-name
Open

JiaDe-Wu wants to merge 1 commit into
NousResearch:mainfrom
JiaDe-Wu:fix/bedrock-replayed-tool-name

Conversation

@JiaDe-Wu

@JiaDe-Wu JiaDe-Wu commented Sep 25, 2026 •

Copy link
Copy Markdown

Summary

Fixes #90008. Converse validates toolUse.name against [a-zA-Z0-9_-]{1,64} and revalidates the whole message history on every request, so one malformed name recorded in a past assistant turn makes every later request in that session fail with a 400 — a plain hi included. The session cannot recover; today the only way out is to abandon it.

This coerces the name only where a past tool call is replayed onto the wire, at the two outbound sites in convert_messages_to_converse (the tool_calls path in _assistant_blocks, and the ordered bedrock_content_blocks sidecar path in _replay_ordered_blocks). convert_tools_to_converse is left alone on purpose — see Scope below.

agent/bedrock_adapter.py +32/-2, tests/agent/test_bedrock_adapter.py +107. Based on main 2ef41d2b58.

Live before/after

Same history, same model (zai.glm-4.7-flash, us-east-2), driven through this module's own convert_messages_to_converse. The recorded name is the one from the report, brain</arg_value>; the last message is a plain hi:

tree wire toolUse.name result
main 2ef41d2b58 'brain</arg_value>' 400 ValidationException at messages.2.member.content.2.member.toolUse.name — RequestId b615b12a-d85c-46ec-8e19-0151a29e0a65
with this patch 'brain__arg_value_' OK, stopReason=end_turn, model answers the hi — RequestId 10393a11-7688-4363-acfe-ffe91da40728

That also removes the needs-repro label's question: the report reproduces exactly as written.

Four measured facts that shaped the fix

All against zai.glm-4.7-flash in us-east-2:

  1. The pattern is enforced on messages[].content[].toolUse.name, not only on toolConfig — RequestId c7ba8744-619a-49e5-90c1-3321ebd3465a.
  2. The length cap is 64 and reports separately (length less than or equal to 64) — RequestId 57594bc1-3671-4ef6-8193-b0e25ef360a9.
  3. The hyphen is accepted, exactly as the pattern says. So brain-v2 is replayed verbatim rather than folded — RequestId b837dffa-02ee-4f7a-889e-7134fef982cf. A test pins this, because folding it would rename working tools for no reason.
  4. A replayed toolUse.name that appears in no toolConfig.tools entry is accepted and the turn completes normally — RequestIds 85adb3e8-971b-490c-8e78-e31881e415e4 and e7e0d12f-aaa7-48a5-8abd-a586b72aae3b. This is the fact the whole approach rests on: toolResult pairs to toolUse by toolUseId, not by name, so a replayed name has no reader left and can be rewritten safely. In the second of those runs the model's next call came back as {"name": "brain", ...} — under the toolSpec name, not the folded one in the history, which is the measured reason the two sides are not symmetric.

Scope: why toolConfig is not touched

A toolSpec name is not interchangeable the same way. The model calls back under whatever name the spec advertises, and tools/registry.py resolves by exact name (tool_error(f"Unknown tool: {name}")), so folding a spec name needs a reverse mapping to survive the round trip. That is a different change with a different failure mode, and it is what #54881 builds (below). A test asserts convert_tools_to_converse is unchanged, so a later change has to state that it is changing it.

Honest trade-off

If a historical read.file folds to read_file and a real tool of that name exists, the replayed history shows a call that never happened under that name. That is semantically imprecise. It is also strictly better than a session that 400s forever, and the imprecision is confined to history a model only reads for context — the toolUseId pairing, the arguments and the result text are all untouched. Names are still recorded verbatim by normalize_converse_response (asserted in a test); the folding happens at the wire boundary only.

Relationship to #90016 and #54881

Both are the same code area and both are currently mergeable=false / dirty, so neither lands as-is; I checked before writing this.

Tests

tests/agent/test_bedrock_adapter.py::TestReplayedToolNameConformsToConverse, 18 node ids. They cover the reported name on both replay paths, hyphen/underscore names replayed verbatim, the 64-char truncation, ["", None, " ", "<|>", 12] all yielding a name the API accepts, toolResult still pairing by toolUseId, convert_tools_to_converse unchanged, inbound names recorded verbatim, and a mixed history where only the bad entry changes.

Full file on this branch: 97 passed, 2 failed. The 2 failures are TestAgentBedrockStreamRecovery::test_streaming_denial_falls_back_to_converse_via_bedrock_stream and ::test_stale_connection_evicts_client_on_agent_stream_path, both ModuleNotFoundError: No module named 'psutil' on my machine, and both failing identically on clean main.

Repo-wide, on the EC2 box with the full venv (82 test files touching agent/, tests/agent/, tests/e2e/core/providers/ and the Bedrock-adjacent CLI paths):

BASE (clean main 2ef41d2b58)   3 failed, 3558 passed, 11 skipped, 3 xfailed
FIX  (this branch)             3 failed, 3576 passed, 11 skipped, 3 xfailed
diff of the FAILED/ERROR sets  IDENTICAL

The +18 passing is exactly this PR's new tests. The 3 shared failures (test_provider_catalog.py::test_api_key_providers_expose_a_credential_env_var, test_local_env_blocklist.py::test_runtime_provenance_is_independent_of_aliases_and_virtual_env[True], test_tui_gateway_server.py::test_load_cfg_raw_sees_replacement_with_pinned_mtime_and_size) are pre-existing on main. Separately, tests/acp_adapter/test_named_provider_catalogs.py fails at collection on clean main (ModuleNotFoundError: No module named 'acp') and aborts the whole run, so it is excluded from both sides of that comparison; it is unrelated to this change.

🤖 Generated with Claude Code

Converse validates every toolUse.name against [a-zA-Z0-9_-]{1,64}, and it
revalidates the whole message history on each request. So a single malformed
name recorded in a past assistant turn makes every later request in that
session fail with a 400 ValidationException -- a plain "hi" included -- and
the session stays broken until it is abandoned (NousResearch#90008).

Normalize the name only where a past tool call is replayed onto the wire, and
deliberately not in convert_tools_to_converse. A replayed name has no reader
left: the call already carries its toolResult, and Converse pairs those by
toolUseId rather than by name. A toolSpec name is not interchangeable that
way, because the model would call back under the folded name and
tools/registry.py resolves tools by exact name.

Measured against zai.glm-4.7-flash in us-east-2 through this module:
  - pattern violation reports at messages.N.member.content.M.member.toolUse.name
    (RequestId c7ba8744-619a-49e5-90c1-3321ebd3465a)
  - the cap reports "length less than or equal to 64"
    (RequestId 57594bc1-3671-4ef6-8193-b0e25ef360a9)
  - the hyphen is accepted, so it is kept rather than folded
    (RequestId b837dffa-02ee-4f7a-889e-7134fef982cf)
  - a folded name present in no toolConfig.tools entry is accepted and the
    turn completes (RequestId 85adb3e8-971b-490c-8e78-e31881e415e4)
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/bedrock AWS Bedrock (boto3, IAM) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Sep 25, 2026

This branch has not been deployed

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists provider/bedrock AWS Bedrock (boto3, IAM) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bedrock ConverseStream fails when toolUse.name contains unsupported characters

2 participants