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
2 changes: 1 addition & 1 deletion .github/workflows/check-file-contents.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ jobs:
# 1. Identify files containing any googleapis.com URL.
set +e
FILES_WITH_ENDPOINTS=$(grep -lE 'https?://[a-zA-Z0-9.-]+\.googleapis\.com' $CHANGED_FILES)
# 2. From those, identify files that are MISSING the required mTLS version.
if [ -n "$FILES_WITH_ENDPOINTS" ]; then
FILES_MISSING_MTLS=$(grep -L '.mtls.googleapis.com' $FILES_WITH_ENDPOINTS)
Expand Down
1 change: 1 addition & 0 deletions contributing/samples/integrations/data_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@
],
)


# NOTE: The generate_chart tool requires 'altair' and 'vl-convert-python' to be
# installed in your environment. You can install them using:
# pip install altair vl-convert-python
Expand Down
6 changes: 6 additions & 0 deletions src/google/adk/apps/compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,12 @@ def _event_function_call_ids(event: Event) -> set[str]:

def _event_function_response_ids(event: Event) -> set[str]:
"""Returns function response ids found in an event."""
# Ignore intermediate/pending LRF responses marked on the event.
if event.actions and getattr(
event.actions, 'is_intermediate_long_running_response', False
):
return set()

function_response_ids: set[str] = set()
for function_response in event.get_function_responses():
if function_response.id:
Expand Down
9 changes: 5 additions & 4 deletions src/google/adk/cli/utils/evals.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
from __future__ import annotations

import os
from typing import Any, TYPE_CHECKING
from typing import Any
from typing import TYPE_CHECKING

from pydantic import alias_generators
from pydantic import BaseModel
Expand Down Expand Up @@ -77,9 +78,9 @@ def create_gcs_eval_managers_from_uri(
from ...evaluation.gcs_eval_sets_manager import GcsEvalSetsManager
except ImportError as e:
raise RuntimeError(
'GCS evaluation managers require Google Cloud optional dependencies.\n'
'Please install them using: pip install google-adk[gcp]\n'
'Or: pip install google-cloud-storage>=2.18'
'GCS evaluation managers require Google Cloud optional'
' dependencies.\nPlease install them using: pip install'
' google-adk[gcp]\nOr: pip install google-cloud-storage>=2.18'
) from e

gcs_bucket = eval_storage_uri.split('://')[1]
Expand Down
7 changes: 7 additions & 0 deletions src/google/adk/events/event_actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,10 @@ class EventActions(BaseModel):

set_model_response: Optional[Any] = None
"""The model response structured output."""

# Marks a function_response event as an intermediate/pending response
# emitted by a long-running tool. When True, compaction should treat the
# event's function response as non-final and not use it to consider a
# function call resolved.
is_intermediate_long_running_response: Optional[bool] = None
"""If true, the function_response is intermediate/pending for an LRF."""
10 changes: 10 additions & 0 deletions src/google/adk/flows/llm_flows/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,16 @@ async def run_tool_and_update_queue(tool, function_args, tool_context):
' pending.'
)
}
# Mark the tool context actions so downstream compaction logic knows
# this function_response is an intermediate/pending update and should
# not be treated as a final response that resolves the corresponding
# function call.
try:
tool_context.actions.is_intermediate_long_running_response = True
except Exception:
# Defensive: do not let instrumentation or missing actions break
# tool execution.
logger.debug('Unable to mark intermediate long-running response')
else:
# Check if we should run tools in thread pool to avoid blocking event loop
thread_pool_config = invocation_context.run_config.tool_thread_pool_config
Expand Down
6 changes: 4 additions & 2 deletions src/google/adk/skills/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ def _list_skills_in_gcs_dir(
except ImportError as e:
raise ImportError(
"google-cloud-storage is required to list skills in GCS. Install it"
" with `pip install google-cloud-storage` or `pip install google-adk[gcp]`."
" with `pip install google-cloud-storage` or `pip install"
" google-adk[gcp]`."
) from e

client = storage.Client(project=project_id, credentials=credentials)
Expand Down Expand Up @@ -478,7 +479,8 @@ def _load_skill_from_gcs_dir(
except ImportError as e:
raise ImportError(
"google-cloud-storage is required to load skills from GCS. Install it"
" with `pip install google-cloud-storage` or `pip install google-adk[gcp]`."
" with `pip install google-cloud-storage` or `pip install"
" google-adk[gcp]`."
) from e

client = storage.Client(project=project_id, credentials=credentials)
Expand Down
205 changes: 205 additions & 0 deletions tests/unittests/apps/test_compaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from google.adk.agents.base_agent import BaseAgent
from google.adk.apps.app import App
from google.adk.apps.app import EventsCompactionConfig
from google.adk.apps.app import ResumabilityConfig
from google.adk.apps.base_events_summarizer import BaseEventsSummarizer
from google.adk.apps.compaction import _run_compaction_for_sliding_window
import google.adk.apps.compaction as compaction_module
Expand Down Expand Up @@ -1198,6 +1199,210 @@ async def test_sliding_window_pending_function_call_remains_in_contents(
)
self.assertEqual(result_contents[2].parts[0].text, 'e3')

@pytest.mark.xfail(
reason=(
'Sliding-window compaction folds unresolved long-running tool calls '
'before the resume response arrives.'
)
)
async def test_sliding_window_compacts_long_running_function_call_before_resume(
self,
):
"""Long-running tool resumes should survive sliding-window compaction."""
app = App(
name='test',
root_agent=Mock(spec=BaseAgent),
resumability_config=ResumabilityConfig(is_resumable=True),
events_compaction_config=EventsCompactionConfig(
summarizer=self.mock_compactor,
compaction_interval=2,
overlap_size=0,
),
)
function_call_id = 'long-running-call-1'
events = [
self._create_event(1.0, 'inv1', 'e1'),
Event(
timestamp=2.0,
invocation_id='inv2',
author='agent',
content=Content(
role='model',
parts=[
Part(
function_call=types.FunctionCall(
id=function_call_id,
name='long_running_tool_func',
args={},
)
)
],
),
long_running_tool_ids={function_call_id},
),
Event(
timestamp=3.0,
invocation_id='inv2',
author='agent',
content=Content(
role='user',
parts=[
Part(
function_response=types.FunctionResponse(
id=function_call_id,
name='long_running_tool_func',
response={'status': 'pending'},
)
)
],
),
),
self._create_event(4.0, 'inv3', 'e3'),
]
session = Session(app_name='test', user_id='u1', id='s1', events=events)

mock_compacted_event = self._create_compacted_event(
1.0, 4.0, 'Summary inv1-inv3'
)
self.mock_compactor.maybe_summarize_events.return_value = (
mock_compacted_event
)

await _run_compaction_for_sliding_window(
app, session, self.mock_session_service
)

appended_event = self.mock_session_service.append_event.call_args[1][
'event'
]
resume_event = Event(
timestamp=5.0,
invocation_id='inv4',
author='user',
content=Content(
role='user',
parts=[
Part(
function_response=types.FunctionResponse(
id=function_call_id,
name='long_running_tool_func',
response={'status': 'confirmed'},
)
)
],
),
)

_contents._get_contents(None, events + [appended_event, resume_event])

async def test_sliding_window_preserves_long_running_call_until_resume(
self,
):
"""Regression: intermediate LRF responses must not resolve the call for compaction."""
app = App(
name='test',
root_agent=Mock(spec=BaseAgent),
resumability_config=ResumabilityConfig(is_resumable=True),
events_compaction_config=EventsCompactionConfig(
summarizer=self.mock_compactor,
compaction_interval=2,
overlap_size=0,
),
)
function_call_id = 'long-running-call-1'
events = [
self._create_event(1.0, 'inv1', 'e1'),
Event(
timestamp=2.0,
invocation_id='inv2',
author='agent',
content=Content(
role='model',
parts=[
Part(
function_call=types.FunctionCall(
id=function_call_id,
name='long_running_tool_func',
args={},
)
)
],
),
long_running_tool_ids={function_call_id},
),
# Intermediate/pending response — mark as intermediate via actions
Event(
timestamp=3.0,
invocation_id='inv2',
author='agent',
content=Content(
role='user',
parts=[
Part(
function_response=types.FunctionResponse(
id=function_call_id,
name='long_running_tool_func',
response={'status': 'pending'},
)
)
],
),
actions=EventActions(is_intermediate_long_running_response=True),
),
self._create_event(4.0, 'inv3', 'e3'),
]
session = Session(app_name='test', user_id='u1', id='s1', events=events)

mock_compacted_event = self._create_compacted_event(
1.0, 4.0, 'Summary inv1-inv3'
)
self.mock_compactor.maybe_summarize_events.return_value = (
mock_compacted_event
)

await _run_compaction_for_sliding_window(
app, session, self.mock_session_service
)

appended_event = self.mock_session_service.append_event.call_args[1][
'event'
]

# Now simulate the actual resume arriving later and build contents — this
# should not raise and the long-running call should still be resolvable.
resume_event = Event(
timestamp=5.0,
invocation_id='inv4',
author='user',
content=Content(
role='user',
parts=[
Part(
function_response=types.FunctionResponse(
id=function_call_id,
name='long_running_tool_func',
response={'status': 'confirmed'},
)
)
],
),
)

result_contents = _contents._get_contents(
None, events + [appended_event, resume_event]
)

# Ensure the long-running function call remains visible (one of the
# content items should be a function_call with the expected name).
assert any(
(
c.parts
and c.parts[0].function_call
and c.parts[0].function_call.name == 'long_running_tool_func'
)
for c in result_contents
)

async def test_token_threshold_excludes_pending_function_call_events(self):
"""Token-threshold compaction stays contiguous before pending calls."""
app = App(
Expand Down
2 changes: 0 additions & 2 deletions tests/unittests/skills/test__utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,5 +393,3 @@ def mock_import(name, globals=None, locals=None, fromlist=(), level=0):
with mock.patch("builtins.__import__", mock_import):
with pytest.raises(ImportError, match="google-cloud-storage is required"):
_load_skill_from_gcs_dir("my-bucket", "skills/my-skill/")


Loading