Skip to content

A task-mode root agent loses its task input once it asks the user a question #7384

Description

@lamberttraccard

🔴 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.

  1. 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.
  2. A later task in the same session runs under the same isolation scope (<agent>@1), which _find_active_task_scope already 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:

  1. pip install google-adk==2.11.0
  2. 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.py is unchanged on main.
  • 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 scope reporting@1 drops it.
  • _build_task_input_user_content then looks for a function call whose id is the scope, finds none (a root has no delegating call), and falls back to invocation_context.user_content. When a message continues a paused task, _node_runner_utils deliberately leaves user_content as 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 called finish_task that scope stays in finished_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

  1. vinitsonawane45 commented on Oct 2, 2026

    @vinitsonawane45

    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, and invocation_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.

  2. added theissue type on Oct 5, 2026
  3. added
    core[Component] This issue is related to the core interface and implementation
    on Oct 5, 2026
  4. surajksharma07 commented on Oct 5, 2026

    @surajksharma07
    Collaborator

    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.

  5. ahlag commented on Oct 8, 2026

    @ahlag

    @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.py peer-isolation case.

  6. surajksharma07 commented on Oct 9, 2026

    @surajksharma07
    Collaborator

    @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.

  7. ahlag commented on Oct 9, 2026

    @ahlag

    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 a to_a2a root.

  8. vinitsonawane45 commented on Oct 9, 2026

    @vinitsonawane45

    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.

  9. surajksharma07 commented on Oct 12, 2026

    @surajksharma07
    Collaborator

    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.

  10. vinitsonawane45 commented on Oct 12, 2026

    @vinitsonawane45

    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!

  11. surajksharma07 commented on Oct 13, 2026

    @surajksharma07
    Collaborator

    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.

  12. vinitsonawane45 commented on Oct 13, 2026

    @vinitsonawane45

    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) == 2 to 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 --check also passed.

    Thanks for pointing out these gaps. The additional regression coverage is now in place for review.

  13. lamberttraccard commented on Oct 15, 2026

    @lamberttraccard
    Author

    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 to RemoteA2aAgent(mode="task"), and the remote root asks for the period before calling finish_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 and Q3, calls finish_task with "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.

  14. surajksharma07 commented on Oct 15, 2026

    @surajksharma07
    Collaborator

    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?

  15. vinitsonawane45 commented on Oct 15, 2026

    @vinitsonawane45

    Hi @surajksharma07, I’ve rebased PR #7419 onto the latest main, incorporated the AgentMode.TASK update, 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!

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

Metadata

Metadata

Labels

core[Component] This issue is related to the core interface and implementation

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions