From c873b223e6c8d87c223a9f497dde9c6101a06867 Mon Sep 17 00:00:00 2001 From: "anan.yablonko" Date: Tue, 16 Sep 2025 18:17:31 +0300 Subject: [PATCH 1/3] fix #2404: branching and include_contents='none' --- src/google/adk/flows/llm_flows/contents.py | 49 ++++++++++------- .../flows/llm_flows/test_contents.py | 52 +++++++++++++++++++ 2 files changed, 83 insertions(+), 18 deletions(-) diff --git a/src/google/adk/flows/llm_flows/contents.py b/src/google/adk/flows/llm_flows/contents.py index 93d9a332ae..d1eb42f17f 100644 --- a/src/google/adk/flows/llm_flows/contents.py +++ b/src/google/adk/flows/llm_flows/contents.py @@ -221,6 +221,26 @@ def _contains_empty_content(event: Event) -> bool: or event.content.parts[0].text == '' ) and (not event.output_transcription and not event.input_transcription) +def _should_include_event_in_context(current_branch: Optional[str], event: Event) -> bool: + """Filters an event. + Removes events without content or function calls, and + events that otherwise do not belong to the context. + + Args: + current_branch: The current branch of the agent. + event: Events to filter. + + Returns: + True if the event should be included in the context, + False otherwise. + """ + return not (False + or _contains_empty_content(event) + or not _is_event_belongs_to_branch(current_branch, event) + or _is_auth_event(event) + or _is_request_confirmation_event(event) + ) + def _get_contents( current_branch: Optional[str], events: list[Event], agent_name: str = '' @@ -240,23 +260,10 @@ def _get_contents( accumulated_input_transcription = '' accumulated_output_transcription = '' - # Parse the events, leaving the contents and the function calls and - # responses from the current agent. - raw_filtered_events = [] - for event in events: - if _contains_empty_content(event): - continue - if not _is_event_belongs_to_branch(current_branch, event): - # Skip events not belong to current branch. - continue - if _is_auth_event(event): - # Skip auth events. - continue - if _is_request_confirmation_event(event): - # Skip request confirmation events. - continue - - raw_filtered_events.append(event) + raw_filtered_events = [ + e for e in events + if _should_include_event_in_context(current_branch, e) + ] filtered_events = [] # aggregate transcription events @@ -343,7 +350,13 @@ def _get_current_turn_contents( # Find the latest event that starts the current turn and process from there for i in range(len(events) - 1, -1, -1): event = events[i] - if event.author == 'user' or _is_other_agent_reply(agent_name, event): + if ( + _should_include_event_in_context(current_branch, event) + and ( + event.author == 'user' + or _is_other_agent_reply(agent_name, event) + ) + ): return _get_contents(current_branch, events[i:], agent_name) return [] diff --git a/tests/unittests/flows/llm_flows/test_contents.py b/tests/unittests/flows/llm_flows/test_contents.py index 7283a0ce7f..e8cfd1b64e 100644 --- a/tests/unittests/flows/llm_flows/test_contents.py +++ b/tests/unittests/flows/llm_flows/test_contents.py @@ -197,6 +197,58 @@ async def test_include_contents_none_multi_agent_current_turn(): assert llm_request.contents[1] == types.ModelContent("Current agent in turn") +@pytest.mark.asyncio +async def test_include_contents_none_multi_branch_current_turn(): + """Test current turn detection in multi-branch scenarios with include_contents='none'.""" + agent = Agent( + model="gemini-2.5-flash", name="current_agent", include_contents="none" + ) + llm_request = LlmRequest(model="gemini-2.5-flash") + invocation_context = await testing_utils.create_invocation_context( + agent=agent + ) + invocation_context.branch = "root.parent_agent" + + # Create multi-branch conversation where current turn starts from user + # This can arise from having a Parallel Agent with two or more Sequential + # Agents as sub agents, each with two Llm Agents as sub agents + events = [ + Event( + invocation_id="inv1", + branch="root", + author="user", + content=types.UserContent("First user message"), + ), + Event( + invocation_id="inv1", + branch="root.parent_agent", + author="sibling_agent", + content=types.ModelContent("Sibling agent response"), + ), + Event( + invocation_id="inv1", + branch="root.uncle_agent", + author="cousin_agent", + content=types.ModelContent("Cousin agent response"), + ), + ] + invocation_context.session.events = events + + # Process the request + async for _ in contents.request_processor.run_async( + invocation_context, llm_request + ): + pass + + # Verify current turn starts from the most recent other agent message of the current branch + assert len(llm_request.contents) == 1 + assert llm_request.contents[0].role == "user" + assert llm_request.contents[0].parts == [ + types.Part(text="For context:"), + types.Part(text="[sibling_agent] said: Sibling agent response"), + ] + + @pytest.mark.asyncio async def test_authentication_events_are_filtered(): """Test that authentication function calls and responses are filtered out.""" From 671bb730cb35cce35b4b74c04bbbaf1da53a2b6e Mon Sep 17 00:00:00 2001 From: "anan.yablonko" Date: Tue, 16 Sep 2025 22:10:44 +0300 Subject: [PATCH 2/3] Improve readability (gemini assist) --- src/google/adk/flows/llm_flows/contents.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/google/adk/flows/llm_flows/contents.py b/src/google/adk/flows/llm_flows/contents.py index d1eb42f17f..42d41c873d 100644 --- a/src/google/adk/flows/llm_flows/contents.py +++ b/src/google/adk/flows/llm_flows/contents.py @@ -222,20 +222,21 @@ def _contains_empty_content(event: Event) -> bool: ) and (not event.output_transcription and not event.input_transcription) def _should_include_event_in_context(current_branch: Optional[str], event: Event) -> bool: - """Filters an event. - Removes events without content or function calls, and - events that otherwise do not belong to the context. + """Determines if an event should be included in the LLM context. + + This filters out events that are considered empty (e.g., no text, function + calls, or transcriptions), do not belong to the current agent's branch, or + are internal events like authentication or confirmation requests. Args: current_branch: The current branch of the agent. - event: Events to filter. + event: The event to filter. Returns: - True if the event should be included in the context, - False otherwise. + True if the event should be included in the context, False otherwise. """ - return not (False - or _contains_empty_content(event) + return not ( + _contains_empty_content(event) or not _is_event_belongs_to_branch(current_branch, event) or _is_auth_event(event) or _is_request_confirmation_event(event) From 8a5a42939e540d2f6b5d9e970148f55ce64accc8 Mon Sep 17 00:00:00 2001 From: "anan.yablonko" Date: Mon, 27 Oct 2025 18:18:29 +0200 Subject: [PATCH 3/3] formatting --- src/google/adk/flows/llm_flows/contents.py | 29 +++++++++++----------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/google/adk/flows/llm_flows/contents.py b/src/google/adk/flows/llm_flows/contents.py index 55491a2fd6..f2e74d21a6 100644 --- a/src/google/adk/flows/llm_flows/contents.py +++ b/src/google/adk/flows/llm_flows/contents.py @@ -233,7 +233,10 @@ def _contains_empty_content(event: Event) -> bool: or event.content.parts[0].text == '' ) and (not event.output_transcription and not event.input_transcription) -def _should_include_event_in_context(current_branch: Optional[str], event: Event) -> bool: + +def _should_include_event_in_context( + current_branch: Optional[str], event: Event +) -> bool: """Determines if an event should be included in the LLM context. This filters out events that are considered empty (e.g., no text, function @@ -248,12 +251,12 @@ def _should_include_event_in_context(current_branch: Optional[str], event: Event True if the event should be included in the context, False otherwise. """ return not ( - _contains_empty_content(event) - or not _is_event_belongs_to_branch(current_branch, event) - or _is_auth_event(event) - or _is_request_confirmation_event(event) + _contains_empty_content(event) + or not _is_event_belongs_to_branch(current_branch, event) + or _is_auth_event(event) + or _is_request_confirmation_event(event) ) - + def _process_compaction_events(events: list[Event]) -> list[Event]: """Processes events by applying compaction. @@ -356,10 +359,10 @@ def _get_contents( # Parse the events, leaving the contents and the function calls and # responses from the current agent. raw_filtered_events = [ - e for e in rewind_filtered_events - if _should_include_event_in_context(current_branch, e) + e + for e in rewind_filtered_events + if _should_include_event_in_context(current_branch, e) ] - if has_compaction_events: events_to_process = _process_compaction_events(raw_filtered_events) @@ -452,12 +455,8 @@ def _get_current_turn_contents( # Find the latest event that starts the current turn and process from there for i in range(len(events) - 1, -1, -1): event = events[i] - if ( - _should_include_event_in_context(current_branch, event) - and ( - event.author == 'user' - or _is_other_agent_reply(agent_name, event) - ) + if _should_include_event_in_context(current_branch, event) and ( + event.author == 'user' or _is_other_agent_reply(agent_name, event) ): return _get_contents(current_branch, events[i:], agent_name)