Skip to content

Commit 0fccc79

Browse files
ananyablonkoJacksunwei
authored andcommitted
fix(core): ParallelAgent's branching with include_contents='none' may cause index out of range error
Merge #2961 Fixes #2404 Consider code where a sub_agent somewhere under a ParallelAgent has include_contents='none'. ```python import asyncio import os from typing import TypedDict from dotenv import load_dotenv from google.adk.agents import LlmAgent, ParallelAgent, SequentialAgent from google.adk.runners import Runner from google.adk.sessions import DatabaseSessionService, InMemorySessionService from google.genai import types import logging USE_DB=False load_dotenv() logging.basicConfig( filename='log.log', level=logging.DEBUG ) for logger_name in ["httpcore"]: logging.getLogger(logger_name).setLevel(logging.ERROR) class AgentArgs(TypedDict): model: str instruction: str class SessionArgs(TypedDict): user_id: str session_id: str agent_args = AgentArgs( model="gemini-2.0-flash", instruction="Answer 'Ack' and nothing else." ) session_args = SessionArgs( user_id="0", session_id="0" ) app_name = "Test" def create_agent_in_branch(i: int): agent_1 = LlmAgent( name=f"subagent_{i}_1", **agent_args ) agent_2 = LlmAgent( name=f"subagent_{i}_2", include_contents='none', **agent_args ) return SequentialAgent( name=f"agent_{i}", sub_agents=[agent_1, agent_2] ) root = ParallelAgent( name="root", sub_agents=[create_agent_in_branch(i) for i in range(1, 5)] ) runner = Runner( agent=root, app_name=app_name, session_service=DatabaseSessionService(db_url=os.getenv("DB_URL", "")) if USE_DB else InMemorySessionService(), ) async def main() -> None: try: await runner.session_service.delete_session(app_name=app_name, **session_args) except: pass await runner.session_service.create_session(app_name=app_name, **session_args) message = types.Content(role="user", parts=[types.Part(text=" ")]) async for event in runner.run_async(**session_args, new_message=message): if event.is_final_response(): print(((event.content or types.Content()).parts or [types.Part()])[0].text or "") if __name__ == '__main__': asyncio.run(main()) ``` The log here will often have one or more ```subagent_{i}_2``` not receive their prompts from ```subagent_{i}_1```. This inconsistency is caused by the way ```include_contents='none'``` is implemented in flows/llm_flows/contents.py:328-356: ```python for i in range(len(events) - 1, -1, -1): event = events[i] if event.author == 'user' or _is_other_agent_reply(agent_name, event): return _get_contents(current_branch, events[i:], agent_name) return [] ``` That is, we first find the most recent (other-agent / user) event, **even if it's not on our branch**, and then filter the remainder. Thus in the above example, we may sometimes filter out all events, when we expect to have the event of a previous agent in a ```SequentialAgent```. The solution is to first filter events by branch, and only then search for the latest. ### TEST SUMMARY: ``` =================================================== short test summary info =================================================== FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-LoopAgent-LoopAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-google.adk.agents.LoopAgent-LoopAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-google.adk.agents.loop_agent.LoopAgent-LoopAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-ParallelAgent-ParallelAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-google.adk.agents.ParallelAgent-ParallelAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-google.adk.agents.parallel_agent.ParallelAgent-ParallelAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-SequentialAgent-SequentialAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-google.adk.agents.SequentialAgent-SequentialAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[GOOGLE_AI-google.adk.agents.sequential_agent.SequentialAgent-SequentialAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-LoopAgent-LoopAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-google.adk.agents.LoopAgent-LoopAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-google.adk.agents.loop_agent.LoopAgent-LoopAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-ParallelAgent-ParallelAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-google.adk.agents.ParallelAgent-ParallelAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-google.adk.agents.parallel_agent.ParallelAgent-ParallelAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-SequentialAgent-SequentialAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-google.adk.agents.SequentialAgent-SequentialAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_with_sub_agents[VERTEX-google.adk.agents.sequential_agent.SequentialAgent-SequentialAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_llm_agent_with_sub_agents[GOOGLE_AI-LlmAgent-LlmAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_llm_agent_with_sub_agents[GOOGLE_AI-google.adk.agents.LlmAgent-LlmAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_llm_agent_with_sub_agents[GOOGLE_AI-google.adk.agents.llm_agent.LlmAgent-LlmAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_llm_agent_with_sub_agents[VERTEX-LlmAgent-LlmAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_llm_agent_with_sub_agents[VERTEX-google.adk.agents.LlmAgent-LlmAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/agents/test_agent_config.py::test_agent_config_discriminator_llm_agent_with_sub_agents[VERTEX-google.adk.agents.llm_agent.LlmAgent-LlmAgent] - FileNotFoundError: Config file not found: C:\Users\AnanYablonko\AppData\Local\Temp\pytest-of-AnanYablonko\pytest-1\test_age... FAILED tests/unittests/cli/utils/test_cli_tools_click.py::test_cli_run_invokes_run_cli[GOOGLE_AI] - AssertionError: assert 1 == 0 FAILED tests/unittests/cli/utils/test_cli_tools_click.py::test_cli_run_invokes_run_cli[VERTEX] - AssertionError: assert 1 == 0 FAILED tests/unittests/cli/utils/test_cli_tools_click.py::test_cli_eval_with_eval_set_file_path[GOOGLE_AI] - assert 0 == 1 FAILED tests/unittests/cli/utils/test_cli_tools_click.py::test_cli_eval_with_eval_set_file_path[VERTEX] - assert 0 == 1 FAILED tests/unittests/evaluation/test_local_eval_sets_manager.py::TestLocalEvalSetsManager::test_local_eval_sets_manager_update_eval_case_eval_set_not_found[GOOGLE_AI] - OSError: [Errno 22] Invalid argument: '<tests.unittests.evaluation.test_local_eval_sets_manager.TestLocalEvalSetsManager ob... FAILED tests/unittests/evaluation/test_local_eval_sets_manager.py::TestLocalEvalSetsManager::test_local_eval_sets_manager_update_eval_case_eval_set_not_found[VERTEX] - OSError: [Errno 22] Invalid argument: '<tests.unittests.evaluation.test_local_eval_sets_manager.TestLocalEvalSetsManager ob... FAILED tests/unittests/evaluation/test_local_eval_sets_manager.py::TestLocalEvalSetsManager::test_local_eval_sets_manager_delete_eval_case_eval_set_not_found[GOOGLE_AI] - OSError: [Errno 22] Invalid argument: '<tests.unittests.evaluation.test_local_eval_sets_manager.TestLocalEvalSetsManager ob... FAILED tests/unittests/evaluation/test_local_eval_sets_manager.py::TestLocalEvalSetsManager::test_local_eval_sets_manager_delete_eval_case_eval_set_not_found[VERTEX] - OSError: [Errno 22] Invalid argument: '<tests.unittests.evaluation.test_local_eval_sets_manager.TestLocalEvalSetsManager ob... FAILED tests/unittests/sessions/test_session_service.py::test_get_session_with_config[GOOGLE_AI-SessionServiceType.DATABASE] - OSError: [Errno 22] Invalid argument FAILED tests/unittests/sessions/test_session_service.py::test_get_session_with_config[VERTEX-SessionServiceType.DATABASE] - OSError: [Errno 22] Invalid argument ================================= 34 failed, 4540 passed, 3044 warnings in 187.68s (0:03:07) ================================== ``` Co-authored-by: Wei Sun (Jack) <weisun@google.com> COPYBARA_INTEGRATE_REVIEW=#2961 from ananyablonko:main b4a21ad PiperOrigin-RevId: 828700099
1 parent 63b69fb commit 0fccc79

File tree

2 files changed

+88
-21
lines changed

2 files changed

+88
-21
lines changed

src/google/adk/flows/llm_flows/contents.py

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,30 @@ def _contains_empty_content(event: Event) -> bool:
234234
) and (not event.output_transcription and not event.input_transcription)
235235

236236

237+
def _should_include_event_in_context(
238+
current_branch: Optional[str], event: Event
239+
) -> bool:
240+
"""Determines if an event should be included in the LLM context.
241+
242+
This filters out events that are considered empty (e.g., no text, function
243+
calls, or transcriptions), do not belong to the current agent's branch, or
244+
are internal events like authentication or confirmation requests.
245+
246+
Args:
247+
current_branch: The current branch of the agent.
248+
event: The event to filter.
249+
250+
Returns:
251+
True if the event should be included in the context, False otherwise.
252+
"""
253+
return not (
254+
_contains_empty_content(event)
255+
or not _is_event_belongs_to_branch(current_branch, event)
256+
or _is_auth_event(event)
257+
or _is_request_confirmation_event(event)
258+
)
259+
260+
237261
def _process_compaction_events(events: list[Event]) -> list[Event]:
238262
"""Processes events by applying compaction.
239263
@@ -331,24 +355,15 @@ def _get_contents(
331355

332356
# Parse the events, leaving the contents and the function calls and
333357
# responses from the current agent.
334-
raw_filtered_events = []
335-
has_compaction_events = False
336-
for event in rewind_filtered_events:
337-
if _contains_empty_content(event):
338-
continue
339-
if not _is_event_belongs_to_branch(current_branch, event):
340-
# Skip events not belong to current branch.
341-
continue
342-
if _is_auth_event(event):
343-
# Skip auth events.
344-
continue
345-
if _is_request_confirmation_event(event):
346-
# Skip request confirmation events.
347-
continue
348-
349-
if event.actions and event.actions.compaction:
350-
has_compaction_events = True
351-
raw_filtered_events.append(event)
358+
raw_filtered_events = [
359+
e
360+
for e in rewind_filtered_events
361+
if _should_include_event_in_context(current_branch, e)
362+
]
363+
364+
has_compaction_events = any(
365+
e.actions and e.actions.compaction for e in raw_filtered_events
366+
)
352367

353368
if has_compaction_events:
354369
events_to_process = _process_compaction_events(raw_filtered_events)
@@ -441,9 +456,9 @@ def _get_current_turn_contents(
441456
# Find the latest event that starts the current turn and process from there
442457
for i in range(len(events) - 1, -1, -1):
443458
event = events[i]
444-
if not event.content:
445-
continue
446-
if event.author == 'user' or _is_other_agent_reply(agent_name, event):
459+
if _should_include_event_in_context(current_branch, event) and (
460+
event.author == 'user' or _is_other_agent_reply(agent_name, event)
461+
):
447462
return _get_contents(current_branch, events[i:], agent_name)
448463

449464
return []

tests/unittests/flows/llm_flows/test_contents.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,58 @@ async def test_include_contents_none_multi_agent_current_turn():
197197
assert llm_request.contents[1] == types.ModelContent("Current agent in turn")
198198

199199

200+
@pytest.mark.asyncio
201+
async def test_include_contents_none_multi_branch_current_turn():
202+
"""Test current turn detection in multi-branch scenarios with include_contents='none'."""
203+
agent = Agent(
204+
model="gemini-2.5-flash", name="current_agent", include_contents="none"
205+
)
206+
llm_request = LlmRequest(model="gemini-2.5-flash")
207+
invocation_context = await testing_utils.create_invocation_context(
208+
agent=agent
209+
)
210+
invocation_context.branch = "root.parent_agent"
211+
212+
# Create multi-branch conversation where current turn starts from user
213+
# This can arise from having a Parallel Agent with two or more Sequential
214+
# Agents as sub agents, each with two Llm Agents as sub agents
215+
events = [
216+
Event(
217+
invocation_id="inv1",
218+
branch="root",
219+
author="user",
220+
content=types.UserContent("First user message"),
221+
),
222+
Event(
223+
invocation_id="inv1",
224+
branch="root.parent_agent",
225+
author="sibling_agent",
226+
content=types.ModelContent("Sibling agent response"),
227+
),
228+
Event(
229+
invocation_id="inv1",
230+
branch="root.uncle_agent",
231+
author="cousin_agent",
232+
content=types.ModelContent("Cousin agent response"),
233+
),
234+
]
235+
invocation_context.session.events = events
236+
237+
# Process the request
238+
async for _ in contents.request_processor.run_async(
239+
invocation_context, llm_request
240+
):
241+
pass
242+
243+
# Verify current turn starts from the most recent other agent message of the current branch
244+
assert len(llm_request.contents) == 1
245+
assert llm_request.contents[0].role == "user"
246+
assert llm_request.contents[0].parts == [
247+
types.Part(text="For context:"),
248+
types.Part(text="[sibling_agent] said: Sibling agent response"),
249+
]
250+
251+
200252
@pytest.mark.asyncio
201253
async def test_authentication_events_are_filtered():
202254
"""Test that authentication function calls and responses are filtered out."""

0 commit comments

Comments
 (0)