Repository navigation
A task-mode root agent loses its task input once it asks the user a question #7384
Description
🔴 Required Information
Describe the Bug:
Runner.run_async documents a root LlmAgent(mode="task") as fully supported, and it is the server shape the RemoteA2aAgent task-mode guide puts behind to_a2a. Two things go wrong as soon as that root asks the user something before calling finish_task.
- On the turn after the question, the model no longer sees the original request. It sees the user's answer twice: once as the task input at the head of the contents, once in the history.
- A later task in the same session runs under the same isolation scope (
<agent>@1), which_find_active_task_scopealready counts as finished. The user's answer to a question in that later task is therefore not stamped with the scope, and it reaches the model only as the task input at the head of the contents. The history the model sees ends on its own question.
Steps to Reproduce:
pip install google-adk==2.11.0- Run the script below. It needs no A2A and no API key: a scripted model and an in-memory session.
import asyncio
from typing import Any
from google.adk.agents import LlmAgent
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import Runner
from google.adk.sessions import InMemorySessionService
from google.genai import types
from pydantic import Field
class ScriptedLlm(BaseLlm):
script: list[Any] = Field(default_factory=list)
requests: list[Any] = Field(default_factory=list)
async def generate_content_async(self, llm_request, stream=False):
self.requests.append(llm_request)
line = self.script.pop(0)
if isinstance(line, str):
part = types.Part(text=line)
else:
part = types.Part(function_call=types.FunctionCall(name=line[0], args=line[1]))
yield LlmResponse(content=types.Content(role="model", parts=[part]))
def seen(request):
return [
(c.role, p.text or (p.function_call and f"call {p.function_call.name}")
or (p.function_response and f"response {p.function_response.name}") or "")
for c in request.contents
for p in c.parts
]
async def main():
llm = ScriptedLlm(model="scripted", script=[
"Which period?",
("finish_task", {"result": "Q3 revenue by country"}),
"Which devices?",
("finish_task", {"result": "Q3 revenue by device"}),
])
agent = LlmAgent(name="reporting", model=llm, instruction="Run reports.", mode="task")
sessions = InMemorySessionService()
runner = Runner(app_name="app", agent=agent, session_service=sessions)
await sessions.create_session(app_name="app", user_id="u", session_id="s")
for text in ["Revenue by country", "Q3", "Same thing by device", "Desktop and mobile"]:
async for _ in runner.run_async(
user_id="u", session_id="s",
new_message=types.Content(role="user", parts=[types.Part(text=text)]),
):
pass
for index, request in enumerate(llm.requests):
print(f"model call {index}: {seen(request)}")
asyncio.run(main())Expected Behavior:
- Call 1:
[('user', 'Revenue by country'), ('model', 'Which period?'), ('user', 'Q3')]. - Call 3: "Same thing by device" as the task input, and a history that ends on
('user', 'Desktop and mobile'), after('model', 'Which devices?').
Observed Behavior:
model call 0: [('user', 'Revenue by country')]
model call 1: [('user', 'Q3'), ('model', 'Which period?'), ('user', 'Q3')]
model call 2: [('user', 'Same thing by device'), ('model', 'Which period?'), ('user', 'Q3'), ('model', 'call finish_task'), ('user', 'response finish_task')]
model call 3: [('user', 'Desktop and mobile'), ('model', 'Which period?'), ('user', 'Q3'), ('model', 'call finish_task'), ('user', 'response finish_task'), ('model', 'Which devices?')]
Environment Details:
- ADK Library Version (pip show google-adk): 2.11.0.
src/google/adk/flows/llm_flows/context/_contents.pyis unchanged onmain. - Desktop OS: macOS
- Python Version (python -V): 3.12.12
Model Information:
- Are you using LiteLLM: No, the reproduction uses a scripted
BaseLlm. Production goes through LiteLLM. - Which model is being used: N/A for the reproduction (Claude Sonnet in production)
🟡 Optional Information
Regression:
Not known. A root LlmAgent(mode="task") is what the reproduction exercises; it was not tried on earlier versions.
Where it comes from:
- The first user message of a task root is appended before the task opens, so it carries no
isolation_scope, and the contents filter for scopereporting@1drops it. _build_task_input_user_contentthen looks for a function call whose id is the scope, finds none (a root has no delegating call), and falls back toinvocation_context.user_content. When a message continues a paused task,_node_runner_utilsdeliberately leavesuser_contentas the new message ("A message that joins a paused task only borrowed that invocation's id"), so the fallback is the answer, not the request.- Every invocation of the root runs as node
reporting@1, so every task in the session shares one scope, and once one task has calledfinish_taskthat scope stays infinished_scopes.
Impact:
Any multi-turn task served as a root, which includes every to_a2a server a RemoteA2aAgent(mode="task") delegates to: the remote agent asks a clarifying question, and on the next turn it no longer knows what it was asked to do.
A chat coordinator that delegates to the same agent as a task sub-agent does not have the problem, because the delegating function call is in the session and its id is the scope.
Activity
vinitsonawane45 commentedon Oct 2, 2026
Hi @lamberttraccard, thanks for the detailed reproduction and the minimal scripted-model case.
I’d like to investigate this issue, specifically the root
LlmAgent(mode="task")task-input lifecycle across a paused question/answer turn and the interaction between_build_task_input_user_content, task scopes, andinvocation_context.user_content.I’ll first reproduce the behavior against the reported version and trace how the initial root task input is stored, filtered, and reconstructed after the task pauses. Then I’ll look for a minimal fix that preserves the original task input across follow-up turns without changing the existing behavior for delegated task agents.
If the behavior is confirmed, I’ll add regression coverage for both the single paused task and the subsequent task in the same session before proposing the fix.
surajksharma07 commentedon Oct 5, 2026
Your script reproduces this on 2.11.0 and main @lamberttraccard. It's actually a regression: 2.9.2 keeps "Revenue by country" on the follow-up turn. Bisected it to ddbb59e (v2.10.0) which now gives a task-mode root the fixed scope reporting@1.
A workaround worth trying: scope the root task as f"{node.name}@{ctx.invocation_id}" when ctx.node_path is empty (a small wrap of DynamicNodeScheduler._execute_step) and when there's no delegating FC build the task input from the user message that opened that scope. With that getting your expected call 1 and call 3 and the existing task/runner tests still pass. Happy to share the script.
Could you check whether it holds up behind to_a2a with LiteLLM on your side? Only tried it with the scripted model.
@vinitsonawane45 if you pick this up please test it against both turns of the repro and the peer-isolation case from ddbb59e (test_task_api_e2e.py) before opening a PR.
ahlag commentedon Oct 8, 2026
@vinitsonawane45 are you still working on this? If not, I'd be glad to pick it up following @surajksharma07's approach and test it against both repro turns and the
test_task_api_e2e.pypeer-isolation case.surajksharma07 commentedon Oct 9, 2026
@ahlag thanks for offering. @vinitsonawane45 hasn't posted an update in a week so unless they reply here in the next day or two please go ahead, just leave a note when you start so no one duplicates the work.
For context: this still reproduces on current main (128faab) even after the recent workflow isolation changes (097ae1e) and the per-invocation root scope workaround still gives the expected call 1 and call 3 there.
Before opening a PR please test it against both turns of the repro, the peer-isolation case in test_task_api_e2e.py and ideally a to_a2a root since that's the path the reporter hits.
@lamberttraccard whenever you get a chance it'd still help to know whether the workaround holds up behind to_a2a with LiteLLM on your side.
ahlag commentedon Oct 9, 2026
Thanks @surajksharma07! I'll give @vinitsonawane45 until Oct 11 to reply and post a note here before I start.
Once I do, I'll cover both turns of the repro, the peer-isolation case in
test_task_api_e2e.py, and ato_a2aroot.vinitsonawane45 commentedon Oct 9, 2026
Hi @surajksharma07, @ahlag, and @lamberttraccard — thanks for your patience.
I’ve implemented the root invocation-scoping fix and opened PR #7419. I’ve now also added an A2A client/server regression test covering clarification and resumption, and the targeted test passes locally.
Sorry for not posting an update sooner. I’m still following the issue and will respond to review feedback. The PR is open and awaiting review.
@ahlag, thanks for offering to pick this up. Since the PR is now open, please let me know if there’s any additional validation you’d like me to prioritize.
surajksharma07 commentedon Oct 12, 2026
Thanks @vinitsonawane45 for putting up #7419 and @ahlag for coordinating. Since the PR is up no need for a parallel one but a review would be welcome. Applied it on current main (ca3b490, applies cleanly): the repro now gives the expected call 1 and call 3, test_task_api_e2e.py is 14 passed / 1 xfailed and workflow/flows/agents/runners/a2a are green apart from two bash tool-node tests that fail on main too on Windows.
Two small things for review. The '@' in isolation_scope check also matches nested node paths like wf@1/node@1; it falls through harmlessly there but keying it to the root scope explicitly would be safer. Also each root task is now isolated per invocation so a second task like "Same thing by device" no longer sees the earlier one (2.9.2 did) so worth noting in the PR description.
Couldn't run the A2A clarification test here since it skips on a2a-sdk 0.3.x. Could you confirm it passes on 1.x and give the full unit suite a run before review?
@lamberttraccard if you can try #7419 behind to_a2a with LiteLLM that'd be the best real-world check. Will keep this issue updated.
vinitsonawane45 commentedon Oct 12, 2026
Hi @surajksharma07, I've updated PR #7419 to address the nested-scope matching concern, added regression tests, and documented the root task isolation behavior. The focused workflow, content, and A2A tests pass. Could you please review the latest changes and let me know if anything else needs to be addressed? Thanks!
surajksharma07 commentedon Oct 13, 2026
The latest #7419 looks good @vinitsonawane45. Rechecked it on current main (8350c14): merges cleanly, the repro gives the expected call 1 and call 3 and workflow/flows/runners/agents are green (3140 passed). The new root-task tests also fail without the fix so they're real regression tests.
The '/' check covers the nested-scope point. One gap: nothing tests the reporter's second symptom, a later task in the same session asking its own question (call 3). Adding the 4-turn scripted repro as a test would cover that. It would also help to assert len(parts) == 2 in test_root_task_survives_clarification so a duplicated answer gets caught too.
Merging on my side had no conflicts so it's only waiting on the required review and the workflow approval. Could you add those tests and rerun the focused + A2A v1 tests before review?
@lamberttraccard if you can try #7419 behind to_a2a with LiteLLM that would still be the best real-world check.
vinitsonawane45 commentedon Oct 13, 2026
Hi @surajksharma07, I’ve added the four-turn regression test covering sequential root tasks asking their own clarification questions in the same session. I’ve also added
assert len(parts) == 2to catch duplicated answers.The focused workflow tests, full workflow test file, A2A v1 clarification/resumption test, and targeted content tests all passed. Ruff, Pyink, and
git diff --checkalso passed.Thanks for pointing out these gaps. The additional regression coverage is now in place for review.
lamberttraccard commentedon Oct 15, 2026
Tried #7419 (791df85) on top of 2.11.0 behind
to_a2a, with Claude Haiku 4.5 through a LiteLLM proxy and a2a-sdk 1.2.1. A chat coordinator delegates toRemoteA2aAgent(mode="task"), and the remote root asks for the period before callingfinish_task.On 2.11.0, the remote model's second call gets
[user: Q3, model: <its question>, user: Q3]. The delegation is gone, and only its own question, which happened to restate the request, keeps a trace of it. It asks again. With #7419 it gets the delegation, its question andQ3, callsfinish_taskwith "revenue broken down by country for Q3", and the coordinator resumes with that result.The scripted repro gives the expected call 1 and call 3. Our two internal suites on ADK 2.11 (3,194 and 2,537 tests, A2A delegation included) pass on it, except the strict xfails we keep for this issue, which now pass.
surajksharma07 commentedon Oct 15, 2026
That’s exactly the check we were missing @lamberttraccard. Good to see the delegation survive the clarifying turn behind to_a2a with LiteLLM and a2a-sdk 1.2.1 and that your strict xfails flip.
Rechecked 791df85 against today’s main (156f007). Main still fails the repro so nothing upstream has fixed this yet. With #7419 on top call 1 and call 3 come out as expected and workflow/flows/runners/agents are green (3242 passed). The only failures are the two bash tool-node tests that fail on main on Windows too. The 4 new root-task tests also fail without the fix.
@vinitsonawane45 one heads-up: 4245327 changed the line just above your edit in _dynamic_node_scheduler.py ('task' -> AgentMode.TASK). It still merges without conflict for me but a quick rebase would keep the diff clean for review.
From our side this is ready. Just waiting on the required review and the 2 workflow approvals on #7419. @DeanChensj could you take a look when you get a chance?
vinitsonawane45 commentedon Oct 15, 2026
Hi @surajksharma07, I’ve rebased PR #7419 onto the latest main, incorporated the
AgentMode.TASKupdate, and fixed the A2A test import and formatting issue. The focused workflow, scheduler, content, and A2A tests all pass, along with Ruff and Pyink. The updated branch is pushed and ready for review. Thanks!