Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletions src/google/adk/flows/llm_flows/contents.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def _rearrange_events_for_async_function_responses_in_history(

def _rearrange_events_for_latest_function_response(
events: list[Event],
all_events: list[Event] | None = None,
) -> list[Event]:
"""Rearrange the events for the latest function_response.

Expand All @@ -134,6 +135,7 @@ def _rearrange_events_for_latest_function_response(

Args:
events: A list of events.
all_events: Full session history to search for function_call.

Returns:
A list of events with the latest function_response rearranged.
Expand All @@ -159,15 +161,18 @@ def _rearrange_events_for_latest_function_response(
if function_call.id in function_responses_ids:
return events

# Search in full session history if available
search_events = all_events if all_events else events
function_call_event = None
function_call_event_idx = -1
# look for corresponding function call event reversely
for idx in range(len(events) - 2, -1, -1):
event = events[idx]
for idx in range(len(search_events) - 2, -1, -1):
event = search_events[idx]
function_calls = event.get_function_calls()
if function_calls:
for function_call in function_calls:
if function_call.id in function_responses_ids:
function_call_event_idx = idx
function_call_event = event
function_call_ids = {
function_call.id for function_call in function_calls
}
Expand All @@ -184,6 +189,18 @@ def _rearrange_events_for_latest_function_response(
# the last response event
function_responses_ids = function_call_ids
break
if function_call_event:
break

# Find the index of the function_call_event in the events list
if function_call_event:
for idx, event in enumerate(events):
if (
event.invocation_id == function_call_event.invocation_id
and event.timestamp == function_call_event.timestamp
):
Comment on lines +198 to +201
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For identifying an event, it's more robust to use the event.id field, which is a unique UUID. Comparing invocation_id and timestamp can be brittle for the following reasons:

  • invocation_id is not unique to a single event; multiple events can share one.
  • timestamp is a float, and direct equality comparison (==) can be unreliable due to floating-point precision issues.

Using event.id provides a more reliable way to match the exact event.

      if event.id == function_call_event.id:

Copy link
Collaborator

@ryanaiagent ryanaiagent Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is consistent with how event uniqueness is managed in other parts of the ADK. Can you implement this suggestion.

function_call_event_idx = idx
break

if function_call_event_idx == -1:
logger.debug(
Expand Down Expand Up @@ -424,7 +441,7 @@ def _get_contents(

# Rearrange events for proper function call/response pairing
result_events = _rearrange_events_for_latest_function_response(
filtered_events
filtered_events, rewind_filtered_events
)
result_events = _rearrange_events_for_async_function_responses_in_history(
result_events
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Tests for function_response sent in separate request (async tools)."""

from google.adk.agents.llm_agent import Agent
from google.adk.events.event import Event
from google.adk.flows.llm_flows import contents
from google.adk.models.llm_request import LlmRequest
from google.genai import types
import pytest

from ... import testing_utils


@pytest.mark.asyncio
async def test_function_response_in_separate_request():
"""Test function_response sent separately finds function_call in history."""
agent = Agent(model="gemini-2.5-flash", name="test_agent")
llm_request = LlmRequest(model="gemini-2.5-flash")
invocation_context = await testing_utils.create_invocation_context(
agent=agent
)

function_call = types.FunctionCall(
id="async_call_123", name="async_tool", args={"job": "start"}
)
function_response = types.FunctionResponse(
id="async_call_123",
name="async_tool",
response={"status": "completed"},
)

# Simulate async job: function_call in early session history,
# function_response arrives much later in separate request
events = [
Event(
invocation_id="inv1",
author="user",
content=types.UserContent("Start async job"),
),
Event(
invocation_id="inv2",
author="test_agent",
content=types.ModelContent([types.Part(function_call=function_call)]),
),
Event(
invocation_id="inv3",
author="test_agent",
content=types.ModelContent("Job started, waiting for completion..."),
),
# Much later: function_response arrives (separate SSE request)
Event(
invocation_id="inv4",
author="user",
content=types.UserContent(
[types.Part(function_response=function_response)]
),
),
]
# Simulate event cloning that happens during processing
events = [e.model_copy(deep=True) for e in events]
invocation_context.session.events = events

# Should not raise ValueError
async for _ in contents.request_processor.run_async(
invocation_context, llm_request
):
pass

# Verify function_response is processed
assert any(
hasattr(part, "function_response")
and part.function_response
and part.function_response.id == "async_call_123"
for content in llm_request.contents
for part in content.parts
)