馃敶 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_turn sub-agent's turn leaks into the coordinator's history. _SingleTurnAgentTool.run_async (tools/agent_tool.py) runs the sub-agent with override_branch but no override_isolation_scope, unlike _dispatch_task_fc for task mode. InvocationContext.isolation_scope says 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 scope None, and a root coordinator has no branch, so _should_include_event_in_context keeps 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.
AgentTool returns Python objects that the Anthropic adapter cannot serialize. validate_schema (utils/_schema_utils.py) returns model_dump(exclude_none=True) without mode="json", so a datetime.date field in the sub-agent's output_schema reaches json.dumps in anthropic_llm._part_to_message_block and the run stops with TypeError: Object of type date is not JSON serializable.
- A thinking block with empty text comes back as
redacted_thinking. _part_to_message_block sends 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 the caller field of tool_use blocks. 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" (with anthropic 1.11.0, httpx2)
- Run the script below. It uses a fake Messages API behind the real
AsyncAnthropic client, 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?:
馃敶 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 ("Invalidsignatureinredacted_thinkingblock. The block is bound to a different conversation."). Three causes, all in 2.11.0: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.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.)AgentToolavoids cause 1 but its docstring calls direct use discouraged, and no public setting gives a root coordinator a branch (aWorkflownode gets none,ParallelAgentis deprecated and fails to run a single-turn sub-agent).Steps to Reproduce:
pip install "google-adk==2.11.0"(withanthropic1.11.0,httpx2)AsyncAnthropicclient, so no key or network is needed.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:
Environment Details:
Model Information:
AnthropicLlm馃煛 Optional Information
Regression: Not known.
Minimal Reproduction Code:
How often has this issue occurred?: