Skip to content

Commit 9a2c371

Browse files
authored
Merge branch 'main' into test/tool-confirmation-coverage
2 parents 165e2a7 + a562a31 commit 9a2c371

20 files changed

Lines changed: 776 additions & 723 deletions

File tree

.github/workflows/pr-triage.yml

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,24 @@
1515
name: ADK Pull Request Triaging Agent
1616

1717
on:
18-
pull_request_target:
19-
types: [opened, reopened, edited]
2018
schedule:
2119
# Run every 6 hours
2220
- cron: '0 */6 * * *'
2321
workflow_dispatch:
2422
inputs:
2523
pr_number:
26-
description: 'The Pull Request number to triage'
27-
required: true
24+
description: 'The Pull Request number to triage (leave empty for batch mode)'
25+
required: false
26+
type: 'string'
27+
pr_count:
28+
description: 'Number of PRs to process in batch mode (default: 10)'
29+
required: false
30+
default: '10'
2831
type: 'string'
2932

3033
jobs:
3134
agent-triage-pull-request:
32-
if: >-
33-
github.event_name == 'schedule' ||
34-
github.event_name == 'workflow_dispatch' || (
35-
github.event.pull_request.head.repo.full_name == github.repository &&
36-
!contains(github.event.pull_request.labels.*.name, 'google-contributor')
37-
)
35+
if: github.repository == 'google/adk-python'
3836
runs-on: ubuntu-latest
3937
permissions:
4038
pull-requests: write
@@ -62,6 +60,7 @@ jobs:
6260
OWNER: 'google'
6361
REPO: 'adk-python'
6462
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number || '' }}
63+
PR_COUNT_TO_PROCESS: ${{ github.event.inputs.pr_count || '10' }}
6564
INTERACTIVE: ${{ vars.PR_TRIAGE_INTERACTIVE }}
6665
PYTHONPATH: contributing/samples/adk_team
6766
run: python -m adk_pr_triaging_agent.main

AGENTS.md

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,3 @@
1-
# AI Coding Assistant Context
2-
3-
This document provides context for AI coding assistants (Antigravity, Gemini CLI, etc.) to understand the ADK Python project and assist with development.
4-
5-
## ADK Knowledge, Architecture, and Style
6-
7-
For all matters regarding ADK development, please use the appropriate skill:
8-
9-
- **`adk-architecture`**: Use this skill whenever you need to understand the architecture, event flow, or state management of the ADK system, or when designing or modifying core components and public APIs.
10-
- Read `.agents/skills/adk-architecture/SKILL.md` for full instructions.
11-
- **`adk-style`**: Use this skill whenever writing code, tests, or reviewing PRs for the ADK project to ensure compliance with styling and coding conventions. Also use it for committing, bug fixing, and testing rules.
12-
- Read `.agents/skills/adk-style/SKILL.md` for full instructions.
13-
- **`adk-git`**: Use this skill for any git operation (commit, push, pull, rebase, etc.). It provides guidelines for Conventional Commits and branch naming.
14-
- Read `.agents/skills/adk-git/SKILL.md` for full instructions.
15-
- **`adk-sample-creator`**: Use this skill when creating new samples demonstrating features or agent patterns, or when adding examples to subdirectories under `contributing/`.
16-
- Read `.agents/skills/adk-sample-creator/SKILL.md` for full instructions.
17-
- **`adk-review`**: Use this skill to review local changes for errors, style compliance, unintended outcomes, and to check if associated design docs, guides, samples, or tests need updates.
18-
- Read `.agents/skills/adk-review/SKILL.md` for full instructions.
19-
- **`adk-issue`**: Use this skill when analyzing, triaging, and resolving GitHub issues for the adk-python repository (orchestrating both triage and fix implementation). Do NOT use this skill if the "/adk-issue-analyze" command is explicitly requested.
20-
- Read `.agents/skills/adk-issue/SKILL.md` for full instructions.
21-
- **`adk-issue-analyze`**: Use this skill to fetch, inspect, and analyze a GitHub issue in a strictly read-only manner. Use this skill when the "/adk-issue-analyze" command is explicitly called.
22-
- Read `.agents/skills/adk-issue-analyze/SKILL.md` for full instructions.
23-
- **`adk-issue-fix`**: Use this skill to implement the code changes, unit tests, and documentation updates for an approved GitHub issue fix. Use this skill when the "/adk-issue-fix" command is explicitly called.
24-
- Read `.agents/skills/adk-issue-fix/SKILL.md` for full instructions.
25-
- **`adk-pr-analyze`**: Use this skill to fetch, inspect, and analyze a GitHub pull request in a strictly read-only manner. Use this skill when the "/adk-pr-analyze" command is explicitly called.
26-
- Read `.agents/skills/adk-pr-analyze/SKILL.md` for full instructions.
27-
- **`adk-pr-triage`**: Use this skill to orchestrate triaging and reviewing GitHub pull requests (PRs) (orchestrating both analysis and user review/checkout). Do NOT use this skill if the "/adk-pr-analyze" command is explicitly requested.
28-
- Read `.agents/skills/adk-pr-triage/SKILL.md` for full instructions.
29-
30-
311
## Project Overview
322

333
The Agent Development Kit (ADK) is an open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents.
@@ -45,6 +15,10 @@ The Agent Development Kit (ADK) is an open-source, code-first Python toolkit for
4515

4616
For details on how the Runner works and the invocation lifecycle, please refer to the `adk-architecture` skill and the referenced documentation therein.
4717

18+
## ADK Knowledge, Architecture, and Style
19+
20+
Skills related to ADK development are in `.agents/skills/`.
21+
4822
## Project Architecture
4923

5024
For detailed architecture patterns, component descriptions, and core interfaces, please refer to the **`adk-architecture`** skill at `.agents/skills/adk-architecture/SKILL.md`.

contributing/samples/adk_team/adk_pr_triaging_agent/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def list_untriaged_pull_requests(pr_count: int) -> dict[str, Any]:
236236
query = f"repo:{OWNER}/{REPO} is:open is:pr"
237237
params = {
238238
"q": query,
239-
"sort": "created",
239+
"sort": "updated",
240240
"order": "desc",
241241
"per_page": 100,
242242
"page": 1,

contributing/samples/adk_team/adk_pr_triaging_agent/main.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from adk_pr_triaging_agent import agent
2020
from adk_pr_triaging_agent.settings import OWNER
21+
from adk_pr_triaging_agent.settings import PR_COUNT_TO_PROCESS
2122
from adk_pr_triaging_agent.settings import PULL_REQUEST_NUMBER
2223
from adk_pr_triaging_agent.settings import REPO
2324
from adk_pr_triaging_agent.utils import call_agent_async
@@ -44,10 +45,14 @@ async def main():
4445
if pr_number:
4546
prompt = f"Please triage pull request #{pr_number}!"
4647
else:
47-
print("No pull request number received. Operating in batch mode.")
48+
pr_count = parse_number_string(PR_COUNT_TO_PROCESS, default_value=10)
49+
print(
50+
"No pull request number received. Operating in batch mode (limit:"
51+
f" {pr_count})."
52+
)
4853
prompt = (
49-
"Please use 'list_untriaged_pull_requests' to find 10 pull requests"
50-
" that need triaging, then triage each one according to your"
54+
f"Please use 'list_untriaged_pull_requests' to find {pr_count} pull"
55+
" requests that need triaging, then triage each one according to your"
5156
" instructions."
5257
)
5358

contributing/samples/adk_team/adk_pr_triaging_agent/settings.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@
2828
OWNER = os.getenv("OWNER", "google")
2929
REPO = os.getenv("REPO", "adk-python")
3030
PULL_REQUEST_NUMBER = os.getenv("PULL_REQUEST_NUMBER")
31+
PR_COUNT_TO_PROCESS = os.getenv("PR_COUNT_TO_PROCESS", "10")
3132

3233
IS_INTERACTIVE = os.environ.get("INTERACTIVE", "1").lower() in ["true", "1"]

src/google/adk/cli/cli_tools_click.py

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,7 +2000,6 @@ def cli_api_server(
20002000
"cloud_run",
20012001
context_settings={
20022002
"allow_extra_args": True,
2003-
"allow_interspersed_args": False,
20042003
},
20052004
)
20062005
@click.option(
@@ -2177,34 +2176,7 @@ def cli_deploy_cloud_run(
21772176

21782177
_warn_if_with_ui(with_ui)
21792178

2180-
# Parse arguments to separate gcloud args (after --) from regular args
2181-
gcloud_args = []
2182-
if "--" in ctx.args:
2183-
separator_index = ctx.args.index("--")
2184-
gcloud_args = ctx.args[separator_index + 1 :]
2185-
regular_args = ctx.args[:separator_index]
2186-
2187-
# If there are regular args before --, that's an error
2188-
if regular_args:
2189-
click.secho(
2190-
"Error: Unexpected arguments after agent path and before '--':"
2191-
f" {' '.join(regular_args)}. \nOnly arguments after '--' are passed"
2192-
" to gcloud.",
2193-
fg="red",
2194-
err=True,
2195-
)
2196-
ctx.exit(2)
2197-
else:
2198-
# No -- separator, treat all args as an error to enforce the new behavior
2199-
if ctx.args:
2200-
click.secho(
2201-
f"Error: Unexpected arguments: {' '.join(ctx.args)}. \nUse '--' to"
2202-
" separate gcloud arguments, e.g.: adk deploy cloud_run [options]"
2203-
" agent_path -- --min-instances=2",
2204-
fg="red",
2205-
err=True,
2206-
)
2207-
ctx.exit(2)
2179+
gcloud_args = ctx.args
22082180

22092181
try:
22102182
from . import cli_deploy

src/google/adk/cli/utils/local_storage.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,21 @@ async def append_event(self, session: Session, event: Event) -> Event:
236236
service = await self._get_service(session.app_name)
237237
return await service.append_event(session, event)
238238

239+
async def close(self) -> None:
240+
"""Closes all underlying session services."""
241+
for service in self._services.values():
242+
if hasattr(service, "close"):
243+
await service.close()
244+
self._services.clear()
245+
246+
async def __aenter__(self) -> PerAgentDatabaseSessionService:
247+
"""Enters the async context manager."""
248+
return self
249+
250+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
251+
"""Exits the async context manager and closes the service."""
252+
await self.close()
253+
239254

240255
class PerAgentFileArtifactService(BaseArtifactService):
241256
"""Routes artifact storage to per-agent `.adk/artifacts` folders."""

src/google/adk/integrations/parameter_manager/parameter_client.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,17 @@
2626
from google.oauth2 import service_account
2727

2828
from ... import version
29+
from ...utils import mtls_utils
2930

3031
USER_AGENT = f"google-adk/{version.__version__}"
3132

33+
_DEFAULT_REGIONAL_ENDPOINT_TEMPLATE = (
34+
"parametermanager.{location}.rep.googleapis.com"
35+
)
36+
_DEFAULT_MTLS_REGIONAL_ENDPOINT_TEMPLATE = (
37+
"parametermanager.{location}.rep.mtls.googleapis.com"
38+
)
39+
3240

3341
class ParameterManagerClient:
3442
"""A client for interacting with Google Cloud Parameter Manager.
@@ -113,7 +121,11 @@ def __init__(
113121
client_options = None
114122
if location:
115123
client_options = {
116-
"api_endpoint": f"parametermanager.{location}.rep.googleapis.com"
124+
"api_endpoint": mtls_utils.get_api_endpoint(
125+
location,
126+
_DEFAULT_REGIONAL_ENDPOINT_TEMPLATE,
127+
_DEFAULT_MTLS_REGIONAL_ENDPOINT_TEMPLATE,
128+
)
117129
}
118130

119131
self._client = parametermanager_v1.ParameterManagerClient(

src/google/adk/models/interactions_utils.py

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,53 @@ def _get_latest_user_contents(
11661166
return latest_user_contents
11671167

11681168

1169+
async def _create_interactions(
1170+
api_client: Client,
1171+
*,
1172+
create_kwargs: dict[str, Any],
1173+
stream: bool,
1174+
) -> AsyncGenerator[LlmResponse, None]:
1175+
"""Issue ``interactions.create`` and convert the response(s) to LlmResponses.
1176+
1177+
This is the shared transport + conversion loop. The caller assembles
1178+
``create_kwargs`` (``model`` or ``agent``, ``input``, ``tools``, etc.); this
1179+
helper owns issuing the call and mapping the stream to ``LlmResponse``s.
1180+
1181+
Args:
1182+
api_client: The Google GenAI client.
1183+
create_kwargs: Keyword arguments passed verbatim to
1184+
``api_client.aio.interactions.create`` (excluding ``stream``).
1185+
stream: Whether to stream the response.
1186+
1187+
Yields:
1188+
LlmResponse objects converted from interaction responses.
1189+
"""
1190+
current_interaction_id: str | None = None
1191+
1192+
if stream:
1193+
responses = await api_client.aio.interactions.create(
1194+
**create_kwargs, stream=True
1195+
)
1196+
aggregated_parts: list[types.Part] = []
1197+
async for event in responses:
1198+
logger.debug(build_interactions_event_log(event))
1199+
interaction_id = _extract_stream_interaction_id(event)
1200+
if interaction_id:
1201+
current_interaction_id = interaction_id
1202+
llm_response = convert_interaction_event_to_llm_response(
1203+
event, aggregated_parts, current_interaction_id
1204+
)
1205+
if llm_response:
1206+
yield llm_response
1207+
else:
1208+
interaction = await api_client.aio.interactions.create(
1209+
**create_kwargs, stream=False
1210+
)
1211+
logger.info('Interaction response received.')
1212+
logger.debug(build_interactions_response_log(interaction))
1213+
yield convert_interaction_to_llm_response(interaction)
1214+
1215+
11691216
async def generate_content_via_interactions(
11701217
api_client: Client,
11711218
llm_request: LlmRequest,
@@ -1227,49 +1274,18 @@ async def generate_content_via_interactions(
12271274
)
12281275
)
12291276

1230-
# Track the current interaction ID from responses
1231-
current_interaction_id: str | None = None
1232-
1233-
if stream:
1234-
# Streaming mode
1235-
responses = await api_client.aio.interactions.create(
1236-
model=llm_request.model,
1237-
input=input_steps,
1238-
stream=True,
1239-
system_instruction=system_instruction,
1240-
tools=interaction_tools if interaction_tools else None,
1241-
generation_config=generation_config if generation_config else None,
1242-
previous_interaction_id=previous_interaction_id,
1243-
)
1244-
1245-
aggregated_parts: list[types.Part] = []
1246-
async for event in responses:
1247-
# Log the streaming event
1248-
logger.debug(build_interactions_event_log(event))
1249-
1250-
interaction_id = _extract_stream_interaction_id(event)
1251-
if interaction_id:
1252-
current_interaction_id = interaction_id
1253-
llm_response = convert_interaction_event_to_llm_response(
1254-
event, aggregated_parts, current_interaction_id
1255-
)
1256-
if llm_response:
1257-
yield llm_response
1258-
1259-
else:
1260-
# Non-streaming mode
1261-
interaction = await api_client.aio.interactions.create(
1262-
model=llm_request.model,
1263-
input=input_steps,
1264-
stream=False,
1265-
system_instruction=system_instruction,
1266-
tools=interaction_tools if interaction_tools else None,
1267-
generation_config=generation_config if generation_config else None,
1268-
previous_interaction_id=previous_interaction_id,
1269-
)
1270-
1271-
# Log the response
1272-
logger.info('Interaction response received from the model.')
1273-
logger.debug(build_interactions_response_log(interaction))
1274-
1275-
yield convert_interaction_to_llm_response(interaction)
1277+
# Assemble the create() kwargs for the model path and delegate the
1278+
# transport + conversion loop to the shared helper.
1279+
create_kwargs: dict[str, Any] = {
1280+
'model': llm_request.model,
1281+
'input': input_steps,
1282+
'system_instruction': system_instruction,
1283+
'tools': interaction_tools if interaction_tools else None,
1284+
'generation_config': generation_config if generation_config else None,
1285+
'previous_interaction_id': previous_interaction_id,
1286+
}
1287+
1288+
async for llm_response in _create_interactions(
1289+
api_client, create_kwargs=create_kwargs, stream=stream
1290+
):
1291+
yield llm_response

src/google/adk/sessions/database_session_service.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,18 @@
7979
_MARIADB_DIALECT = "mariadb"
8080
_MYSQL_DIALECT = "mysql"
8181
_POSTGRESQL_DIALECT = "postgresql"
82+
# Dialects whose DATETIME/TIMESTAMP columns do not retain timezone info, so
83+
# timezone-aware datetimes must have their tzinfo stripped before storage. This
84+
# keeps the value written by create_session consistent with the value read back
85+
# from storage; otherwise the stale-writer marker comparison in append_event
86+
# raises a false positive on the first append after create_session. Cloud
87+
# Spanner is intentionally excluded because its TIMESTAMP is timezone-aware.
88+
_NAIVE_DATETIME_DIALECTS = (
89+
_SQLITE_DIALECT,
90+
_POSTGRESQL_DIALECT,
91+
_MYSQL_DIALECT,
92+
_MARIADB_DIALECT,
93+
)
8294
# Tuple key order for in-process per-session lock maps:
8395
# (app_name, user_id, session_id).
8496
_SessionLockKey: TypeAlias = tuple[str, str, str]
@@ -348,6 +360,14 @@ def _supports_row_level_locking(self) -> bool:
348360
_POSTGRESQL_DIALECT,
349361
)
350362

363+
def _uses_naive_datetime(self) -> bool:
364+
"""Returns whether the active dialect stores datetimes without timezone info.
365+
366+
These dialects persist timezone-naive DATETIME/TIMESTAMP values, so
367+
timezone-aware datetimes must have their tzinfo stripped before storage.
368+
"""
369+
return self.db_engine.dialect.name in _NAIVE_DATETIME_DIALECTS
370+
351371
@asynccontextmanager
352372
async def _with_session_lock(
353373
self, *, app_name: str, user_id: str, session_id: str
@@ -531,8 +551,7 @@ async def create_session(
531551
now = datetime.fromtimestamp(platform_time.get_time(), tz=timezone.utc)
532552
is_sqlite = self.db_engine.dialect.name == _SQLITE_DIALECT
533553
is_postgresql = self.db_engine.dialect.name == _POSTGRESQL_DIALECT
534-
is_mysql = self.db_engine.dialect.name == _MYSQL_DIALECT
535-
if is_sqlite or is_postgresql or is_mysql:
554+
if self._uses_naive_datetime():
536555
now = now.replace(tzinfo=None)
537556

538557
storage_session = schema.StorageSession(

0 commit comments

Comments
 (0)