Skip to content

Runner.run_async runs an LlmAgent with neither new_message nor invocation_id instead of raising ValueError #7934

Description

@yifanxiong272

🔴 Required Information

Describe the Bug:
Runner.run_async documents "Raises: ValueError: ... If both invocation_id and new_message are None.", and _run_with_trace has that check ("Running an agent requires either a new_message or an invocation_id ..."). But when the root agent is an LlmAgent, run_async takes the _run_node_async branch first and returns before reaching the check. So the run starts with no user message and calls the model with empty contents.

Steps to Reproduce:

  1. pip install google-adk==2.13.0
  2. Run the script below. It uses a recording model, so no API key is needed.

Expected Behavior:
ValueError, as documented, before the model is called.

Observed Behavior:
No error; the model is called once with no contents.

Environment Details:

  • ADK Library Version (pip show google-adk): 2.13.0, and main at 95d8610
  • Desktop OS: Linux
  • Python Version (python -V): 3.12

Model Information:

  • Are you using LiteLLM: No
  • Which model is being used: a BaseLlm subclass that records its calls (any model shows it)

🟡 Optional Information

Minimal Reproduction Code:

import asyncio
from typing import AsyncGenerator

from google.adk.agents import LlmAgent
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_request import LlmRequest
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

CALLS = []


class Recorder(BaseLlm):
    model: str = "recorder"

    async def generate_content_async(self, llm_request: LlmRequest, stream: bool = False) -> AsyncGenerator[LlmResponse, None]:
        CALLS.append([c.role for c in llm_request.contents])
        yield LlmResponse(content=types.Content(role="model", parts=[types.Part(text="hello")]))


async def main():
    runner = Runner(app_name="demo", agent=LlmAgent(name="greeter", model=Recorder()), session_service=InMemorySessionService())
    await runner.session_service.create_session(app_name="demo", user_id="u", session_id="s")
    try:
        events = [event async for event in runner.run_async(user_id="u", session_id="s", new_message=None)]
        print("no error;", len(events), "events; model calls:", CALLS)
    except ValueError as exc:
        print("ValueError:", exc)


asyncio.run(main())

Logs:

no error; 1 events; model calls: [[]]

Additional Context:
In runners.py the if isinstance(self.agent, LlmAgent): branch (and the BaseNode one after it) return before _run_with_trace, where the check is. Moving the check before those branches covers them all.

How often has this issue occurred?:
Always (100%)

Activity

  1. mohitdixit02 commented on Nov 2, 2026

    @mohitdixit02

    Hi! I was able to reproduce this locally on ADK 2.13.0. In Runner.run_async, the LlmAgent branch routes into _run_node_async and returns early before reaching the new_message/invocation_id check in _run_with_trace, resulting in calling the model with empty contents instead of raising ValueError.

    I'd like to work on this and submit a PR with unit tests. Could you please assign this to me?

  2. surajksharma07 commented on Nov 2, 2026

    @surajksharma07
    Collaborator

    @yifanxiong272. Reproduced this on 2.13.0 and on current main for both an LlmAgent and a Workflow root while a BaseAgent root still raises. In 1.x every root raised so this looks like it came in with the 2.x node path.

    @mohitdixit02 one thing on #7936: a few existing tests run with new_message=None on purpose to re-run over the session history (test_llm_agent_include_contents.py and two in test_compaction_runner_e2e.py) and they fail with the check moved to the top as-is.

    As a workaround, raising only when there’s no new_message, no invocation_id and the session has no events kept those tests green and still raised for the empty case. Could you both check if that works on your side?

    Before #7936 goes for review please give it a full run of the runner, include_contents and compaction tests and add a case for a session that already has history.

  3. mohitdixit02 commented on Nov 3, 2026

    @mohitdixit02

    @surajksharma07
    Checked on my end and that works cleanly.

    I've updated the check in #7936 to inspect not session.events before raising when new_message and invocation_id are absent.
    Full runs of test_runners.py, test_compaction_runner_e2e.py, and test_llm_agent_include_contents.py are all fine, and I've added a unit test covering run_async execution over existing session history with new_message=None.

    Pushed the update to the PR branch!

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

Metadata

Metadata

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions