Repository navigation
Description
🔴 Required Information
Describe the Bug:
With AnthropicLlm, the history ADK sends to Claude is not the previous request plus Claude's reply. Claude signs each thinking block over everything before it (preserved thinking), and on accounts where that check is enforced the API refuses the request with a 400 ("Invalid signature in redacted_thinking block. The block is bound to a different conversation."). Three causes, all in 2.11.0:
- A
single_turnsub-agent's turn leaks into the coordinator's history._SingleTurnAgentTool.run_async(tools/agent_tool.py) runs the sub-agent withoverride_branchbut nooverride_isolation_scope, unlike_dispatch_task_fcfor task mode.InvocationContext.isolation_scopesays single-turn agents are scoped under the originating function call id, and the collaboration guide says each single-turn agent "operates in its own isolated session branch". The sub-agent's events keep scopeNone, and a root coordinator has no branch, so_should_include_event_in_contextkeeps them. They are left out of the request right after the call, then inserted as "For context: below is a transcript of what another agent did" text in the next one, before the model's previous reply. AgentToolreturns Python objects that the Anthropic adapter cannot serialize.validate_schema(utils/_schema_utils.py) returnsmodel_dump(exclude_none=True)withoutmode="json", so adatetime.datefield in the sub-agent'soutput_schemareachesjson.dumpsinanthropic_llm._part_to_message_blockand the run stops withTypeError: Object of type date is not JSON serializable.- A thinking block with empty text comes back as
redacted_thinking._part_to_message_blocksends a thought part with no text and a signature as{"type": "redacted_thinking", "data": <signature>}. Claude Sonnet 5.5 and Opus 5.5 return thinking blocks with empty text by default (display: "omitted"), so every thinking block is rewritten. (It also drops thecallerfield oftool_useblocks. That one is consistent across requests.)
AgentTool avoids cause 1 but its docstring calls direct use discouraged, and no public setting gives a root coordinator a branch (a Workflow node gets none, ParallelAgent is deprecated and fails to run a single-turn sub-agent).
Steps to Reproduce:
pip install "google-adk==2.11.0"(withanthropic1.11.0,httpx2)- Run the script below. It uses a fake Messages API behind the real
AsyncAnthropicclient, so no key or network is needed. - It compares each request with the previous request plus the model's reply.
Expected Behavior:
Each request starts with the previous request's messages and the model's reply, unchanged, and a sub-agent's output schema with a date reaches the model as JSON.
Observed Behavior:
1. single_turn sub-agent:
request 2, message 3: sent {"role": "user", "content": [{"text": "For context: below is a transcript of what another agent did, quoted between <<<BEGIN_QUOTED_AGENT_CONTENT>>> and <<<END_, expected {"role": "assistant", "content": [{"type": "thinking", "thinking": "summary", "signature": "sig-1"}, {"type": "tool_use", "id": "toolu_1", "name": "clock", "inp
2. AgentTool, output schema with a date:
run stopped: TypeError: Object of type date is not JSON serializable
3. AgentTool, empty thinking text (display omitted):
request 1, message 1: sent {"role": "assistant", "content": [{"type": "redacted_thinking", "data": "sig-0"}, {"id": "toolu_0", "name": "sub", "input": {"request": "go"}, "type": "tool_use, expected {"role": "assistant", "content": [{"type": "thinking", "thinking": "", "signature": "sig-0"}, {"type": "tool_use", "id": "toolu_0", "name": "sub", "input": {"re
request 2, message 3: sent {"role": "assistant", "content": [{"type": "redacted_thinking", "data": "sig-1"}, {"id": "toolu_1", "name": "clock", "input": {}, "type": "tool_use"}]}, expected {"role": "assistant", "content": [{"type": "thinking", "thinking": "", "signature": "sig-1"}, {"type": "tool_use", "id": "toolu_1", "name": "clock", "input": {}
Environment Details:
- ADK Library Version (pip show google-adk): 2.11.0
- Desktop OS: Linux
- Python Version (python -V): 3.11.13
Model Information:
- Are you using LiteLLM: No
- Which model is being used: claude-sonnet-5-5 through
AnthropicLlm
🟡 Optional Information
Regression: Not known.
Minimal Reproduction Code:
"""Reproduce three history changes in ADK's Anthropic path (no network).
A fake Messages API behind the real AsyncAnthropic client records each
request body. For each pair of consecutive requests, the earlier
messages plus the model's reply should open the later request unchanged.
"""
import asyncio
import datetime
import json
import httpx2
from anthropic import AsyncAnthropic
from google.adk.agents import LlmAgent
from google.adk.models.anthropic_llm import AnthropicLlm
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.adk.tools.agent_tool import AgentTool
from google.genai import types
from pydantic import BaseModel
class Note(BaseModel):
day: datetime.date
def make_fake(script):
bodies, replies = [], []
async def handle(request):
body = json.loads(request.content)
if body["system"].startswith("Sub"):
content = [{"type": "text", "text": '{"day": "2026-10-07"}'}]
stop = "end_turn"
else:
content, stop = script(len(bodies))
bodies.append(body)
replies.append(content)
reply = {
"id": "msg", "type": "message", "role": "assistant",
"model": body["model"], "content": content,
"stop_reason": stop, "stop_sequence": None,
"usage": {"input_tokens": 1, "output_tokens": 1},
}
return httpx2.Response(200, json=reply)
client = AsyncAnthropic(
api_key="x",
http_client=httpx2.AsyncClient(
transport=httpx2.MockTransport(handle)
),
)
return client, bodies, replies
def script(thinking_text):
def turn(n):
thinking = {
"type": "thinking", "thinking": thinking_text,
"signature": f"sig-{n}",
}
calls = [("sub", {"request": "go"}), ("clock", {})]
if n < len(calls):
name, args = calls[n]
return [thinking, {
"type": "tool_use", "id": f"toolu_{n}", "name": name,
"input": args, "caller": {"type": "direct"},
}], "tool_use"
return [thinking, {"type": "text", "text": "done"}], "end_turn"
return turn
def clock() -> dict:
"""Return the time."""
return {"time": "noon"}
async def run(mode, thinking_text="summary", output_schema=None):
client, bodies, replies = make_fake(script(thinking_text))
model = AnthropicLlm(model="claude-sonnet-5-5", client=client)
sub = LlmAgent(
name="sub", model=model, instruction="Sub agent.",
description="A helper.", output_schema=output_schema,
)
if mode == "single_turn":
sub.mode = "single_turn"
root = LlmAgent(name="root", model=model, instruction="Root.",
tools=[clock], sub_agents=[sub])
else:
root = LlmAgent(name="root", model=model, instruction="Root.",
tools=[clock, AgentTool(sub)])
sessions = InMemorySessionService()
runner = Runner(app_name="r", agent=root, session_service=sessions)
await sessions.create_session(app_name="r", user_id="u", session_id="s")
message = types.Content(role="user", parts=[types.Part(text="Hi")])
try:
async for _ in runner.run_async(
user_id="u", session_id="s", new_message=message
):
pass
except Exception as exc:
return [f"run stopped: {type(exc).__name__}: {exc}"]
finally:
await runner.close()
problems = []
for i, later in enumerate(bodies[1:]):
# ADK also drops "caller" from tool_use blocks, ignored here.
reply = [
{k: v for k, v in b.items() if k != "caller"} for b in replies[i]
]
sent = bodies[i]["messages"] + [
{"role": "assistant", "content": reply}
]
for n, msg in enumerate(sent):
if n >= len(later["messages"]) or later["messages"][n] != msg:
problems.append(
f"request {i + 1}, message {n}: sent "
f"{json.dumps(later['messages'][n])[:160]}, "
f"expected {json.dumps(msg)[:160]}"
)
break
return problems
async def main():
print("1. single_turn sub-agent:")
print(*await run("single_turn"), sep="\n")
print("2. AgentTool, output schema with a date:")
print(*await run("agent_tool", output_schema=Note), sep="\n")
print("3. AgentTool, empty thinking text (display omitted):")
print(*await run("agent_tool", thinking_text=""), sep="\n")
asyncio.run(main())How often has this issue occurred?:
- Always (100%)
Activity
piyushmakad commentedon Oct 12, 2026
I reproduced the datetime.date serialization failure on Windows with Python 3.11. I have a focused fix that uses Pydantic JSON serialization in validate_schema, plus regression tests for a model and a list of models. The 65 tests in test_schema_utils.py pass. Would a PR for this one part of #7480 be welcome?
llalitkumarrr commentedon Oct 14, 2026
Hello @wukath,
Right now the history sent back to Claude gets changed between requests. Claude's thinking signatures then fail and the API returns a 400. We reproduced all three causes on 2.11.0.
PR #7500 should fix the empty thinking cause. The date serialization cause can be fixed safely inside the Anthropic adapter.
For the single-turn sub-agent leak the fix could be
override_isolation_scope=fc_id. But this might lead to other issues.Could you please take a look at it and share your input?
nboukraa commentedon Oct 17, 2026
In short: #7500 fixes the empty thinking cause, a date fix in the Anthropic adapter covers both paths, and
override_isolation_scope=fc_idkeeps the coordinator's history intact but changes what the sub-agent is asked. Thanks @llalitkumarrr for reproducing all three. The details below may help @wukath. All of them are on 2.11.0, with the reproduction script from this issue extended to also record the sub-agent's own requests.1. Empty thinking. With the one-line change from #7500 (head
1b91520), case 3 sends each thinking block back exactly as Claude sent it:{"type": "thinking", "thinking": "", "signature": ...}.2. Dates. A fix in the Anthropic adapter works for me. With
AgentTooland with a single-turn sub-agent, the run stops at the same line,json.dumps(response_data)in_part_to_message_block(anthropic_llm.py:568), so one fix there covers both. For context:validate_schemawas switched tomode="json"on 1 October (d1e61e5, from #6750, marked as a breaking change) and rolled back the same day (731ea30).3. Single-turn leak. I added
override_isolation_scope=fc_idto therun_nodecall in_SingleTurnAgentTool.run_async, as task mode already does. Case 1 then passes: the coordinator's history only grows, and it still receives the sub-agent's answer as atool_result.One thing changes for the sub-agent itself. Without the change, its request holds one message, its input (
go). With it, its first message is the user's message to the coordinator (Hi) followed by_SINGLE_TURN_NUDGE("Complete the task using only the information provided above"), and its input comes second. The cause is in_build_task_input_user_content. It looks for the function call whose id equals the scope, but the session the sub-agent sees holds only the user's message, not the coordinator's call (toolu_0). So it falls back touser_content. Its docstring expects that to hold the node input, stamped by the workflow wrapper, but on this path it still holds the user's message to the coordinator. The same happens in our own coordinator: its specialist gets the owner's question and the nudge before its JSON input.I can run a branch through the same checks if that helps.
fix(schema): keep fallback structured tool output JSON serializable
fix(schema): preserve annotated schemas while merging current main
nboukraa commentedon Oct 24, 2026
Status on 2.12.0 and 2.13.0, which narrows this issue to two causes.
Cause 1 (single-turn leak) is fixed in 2.12.0 by 2e06fdc ("keep tool sub-branch events out of the calling agent's LLM context").
_is_tool_sub_branch_eventkeeps the sub-agent's events out of the coordinator's request by branch. Case 1 of the script in this issue passes on 2.12.0, and our own coordinator with single-turn sub-agents also keeps its history append-only.So
_SingleTurnAgentToolno longer needsoverride_isolation_scope=fc_id, which would have two side effects:_find_active_task_scopetreats a scope as closed only after afinish_taskcall, and only task agents have that tool, so the runner would take the finished sub-agent for a paused task.Causes 2 and 3 remain in 2.13.0.
validate_schemastill dumps withoutmode="json", and_part_to_message_blockstill sends a thought part with a signature and no text asredacted_thinking. #7591 and #7500 address them.fix: preserve empty Anthropic thinking blocks