Skip to content

Commit e6bfac8

Browse files
authored
Merge branch 'main' into fix/mcp-mtls-auth-header-case
2 parents c637b5f + ea2ea73 commit e6bfac8

24 files changed

Lines changed: 748 additions & 703 deletions

File tree

.github/workflows/pr-triage.yml

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

1717
on:
18-
pull_request_target:
19-
types: [opened, reopened, edited]
18+
schedule:
19+
# Run every 6 hours
20+
- cron: '0 */6 * * *'
2021
workflow_dispatch:
2122
inputs:
2223
pr_number:
23-
description: 'The Pull Request number to triage'
24-
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'
2531
type: 'string'
2632

2733
jobs:
2834
agent-triage-pull-request:
29-
if: >-
30-
github.event_name == 'workflow_dispatch' || (
31-
github.event.pull_request.head.repo.full_name == github.repository &&
32-
!contains(github.event.pull_request.labels.*.name, 'google-contributor')
33-
)
35+
if: github.repository == 'google/adk-python'
3436
runs-on: ubuntu-latest
3537
permissions:
3638
pull-requests: write
@@ -57,7 +59,8 @@ jobs:
5759
GOOGLE_GENAI_USE_VERTEXAI: 0
5860
OWNER: 'google'
5961
REPO: 'adk-python'
60-
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
62+
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number || '' }}
63+
PR_COUNT_TO_PROCESS: ${{ github.event.inputs.pr_count || '10' }}
6164
INTERACTIVE: ${{ vars.PR_TRIAGE_INTERACTIVE }}
6265
PYTHONPATH: contributing/samples/adk_team
6366
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: 62 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from adk_pr_triaging_agent.settings import REPO
2222
from adk_pr_triaging_agent.utils import error_response
2323
from adk_pr_triaging_agent.utils import get_diff
24+
from adk_pr_triaging_agent.utils import get_request
2425
from adk_pr_triaging_agent.utils import post_request
2526
from adk_pr_triaging_agent.utils import read_file
2627
from adk_pr_triaging_agent.utils import run_graphql_query
@@ -219,6 +220,56 @@ def add_comment_to_pr(pr_number: int, comment: str) -> dict[str, Any]:
219220
}
220221

221222

223+
def list_untriaged_pull_requests(pr_count: int) -> dict[str, Any]:
224+
"""List open pull requests that need triaging.
225+
226+
Returns pull requests that need triaging (i.e. do not have google-contributor
227+
label and do not have any allowed triage category labels).
228+
229+
Args:
230+
pr_count: number of pull requests to return
231+
232+
Returns:
233+
The status of this request, with a list of pull requests when successful.
234+
"""
235+
url = f"{GITHUB_BASE_URL}/search/issues"
236+
query = f"repo:{OWNER}/{REPO} is:open is:pr"
237+
params = {
238+
"q": query,
239+
"sort": "created",
240+
"order": "desc",
241+
"per_page": 100,
242+
"page": 1,
243+
}
244+
245+
try:
246+
response = get_request(url, params)
247+
except requests.exceptions.RequestException as e:
248+
return error_response(f"Error: {e}")
249+
250+
issues = response.get("items", [])
251+
triage_labels = set(ALLOWED_LABELS)
252+
untriaged_prs = []
253+
254+
for pr in issues:
255+
pr_labels = {label["name"] for label in pr.get("labels", [])}
256+
if "google-contributor" in pr_labels:
257+
continue
258+
# If it already has any of the ALLOWED_LABELS, skip it.
259+
if pr_labels & triage_labels:
260+
continue
261+
262+
untriaged_prs.append({
263+
"number": pr["number"],
264+
"title": pr["title"],
265+
})
266+
267+
if len(untriaged_prs) >= pr_count:
268+
break
269+
270+
return {"status": "success", "pull_requests": untriaged_prs}
271+
272+
222273
root_agent = Agent(
223274
model="gemini-3.5-flash",
224275
name="adk_pr_triaging_assistant",
@@ -276,15 +327,16 @@ def add_comment_to_pr(pr_number: int, comment: str) -> dict[str, Any]:
276327
> This information will help reviewers to review your PR more efficiently. Thanks!
277328
278329
# 4. Steps
279-
When you are given a PR, here are the steps you should take:
280-
- Call the `get_pull_request_details` tool to get the details of the PR.
281-
- Skip the PR (i.e. do not label or comment) if any of the following is true:
282-
- the PR is closed
283-
- the PR is labeled with "google-contributor"
284-
- the PR is already labelled with the above labels (e.g. "documentation", "services", "tools", etc.).
285-
- Check if the PR is following the contribution guidelines.
286-
- If it's not following the guidelines, recommend or add a comment to the PR that points to the contribution guidelines (https://github.com/google/adk-python/blob/main/CONTRIBUTING.md).
287-
- If it's following the guidelines, recommend or add a label to the PR.
330+
- If you are asked to find pull requests that need triaging, use `list_untriaged_pull_requests` first.
331+
- For each pull request to be triaged:
332+
- Call the `get_pull_request_details` tool to get the details of the PR.
333+
- Skip the PR (i.e. do not label or comment) if any of the following is true:
334+
- the PR is closed
335+
- the PR is labeled with "google-contributor"
336+
- the PR is already labelled with the above labels (e.g. "documentation", "services", "tools", etc.).
337+
- Check if the PR is following the contribution guidelines.
338+
- If it's not following the guidelines, recommend or add a comment to the PR that points to the contribution guidelines (https://github.com/google/adk-python/blob/main/CONTRIBUTING.md).
339+
- If it's following the guidelines, recommend or add a label to the PR.
288340
289341
# 5. Output
290342
Present the following in an easy to read format highlighting PR number and your label.
@@ -293,6 +345,7 @@ def add_comment_to_pr(pr_number: int, comment: str) -> dict[str, Any]:
293345
- The comment you recommended or added to the PR with the justification
294346
""",
295347
tools=[
348+
list_untriaged_pull_requests,
296349
get_pull_request_details,
297350
add_label_to_pr,
298351
add_comment_to_pr,

contributing/samples/adk_team/adk_pr_triaging_agent/main.py

Lines changed: 12 additions & 4 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
@@ -41,13 +42,20 @@ async def main():
4142
)
4243

4344
pr_number = parse_number_string(PULL_REQUEST_NUMBER)
44-
if not pr_number:
45+
if pr_number:
46+
prompt = f"Please triage pull request #{pr_number}!"
47+
else:
48+
pr_count = parse_number_string(PR_COUNT_TO_PROCESS, default_value=10)
4549
print(
46-
f"Error: Invalid pull request number received: {PULL_REQUEST_NUMBER}."
50+
"No pull request number received. Operating in batch mode (limit:"
51+
f" {pr_count})."
52+
)
53+
prompt = (
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"
56+
" instructions."
4757
)
48-
return
4958

50-
prompt = f"Please triage pull request #{pr_number}!"
5159
response = await call_agent_async(runner, USER_ID, session.id, prompt)
5260
print(f"<<<< Agent Final Output: {response}\n")
5361

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/agents/config_agent_utils.py

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,8 @@ def _load_config_from_path(config_path: str) -> AgentConfig:
8686
"""Load an agent's configuration from a YAML file.
8787
8888
Args:
89-
config_path: Path to the YAML config file. Both relative and absolute
90-
paths are accepted.
89+
config_path: Path to the YAML config file. Both relative and absolute paths
90+
are accepted.
9191
9292
Returns:
9393
The loaded and validated AgentConfig object.
@@ -124,21 +124,31 @@ def resolve_agent_reference(
124124
Args:
125125
ref_config: The agent reference configuration (AgentRefConfig).
126126
referencing_agent_config_abs_path: The absolute path to the agent config
127-
that contains the reference.
127+
that contains the reference.
128128
129129
Returns:
130130
The created agent instance.
131131
"""
132132
if ref_config.config_path:
133133
if os.path.isabs(ref_config.config_path):
134-
return from_config(ref_config.config_path)
135-
else:
136-
return from_config(
137-
os.path.join(
138-
os.path.dirname(referencing_agent_config_abs_path),
139-
ref_config.config_path,
140-
)
134+
raise ValueError(
135+
"Absolute paths are not allowed in AgentRefConfig config_path:"
136+
f" {ref_config.config_path!r}"
141137
)
138+
agent_dir = os.path.dirname(referencing_agent_config_abs_path)
139+
resolved_path = os.path.realpath(
140+
os.path.join(agent_dir, ref_config.config_path)
141+
)
142+
canonical_agent_dir = os.path.realpath(agent_dir)
143+
if (
144+
os.path.commonpath([canonical_agent_dir, resolved_path])
145+
!= canonical_agent_dir
146+
):
147+
raise ValueError(
148+
f"Path traversal detected: config_path {ref_config.config_path!r}"
149+
" resolves outside the agent directory"
150+
)
151+
return from_config(resolved_path)
142152
elif ref_config.code:
143153
return _resolve_agent_code_reference(ref_config.code)
144154
else:

src/google/adk/agents/llm_agent.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,9 +520,13 @@ async def _run_async_impl(
520520
return
521521

522522
should_pause = False
523+
output_accumulator = ''
523524
async with Aclosing(self._llm_flow.run_async(ctx)) as agen:
524525
async for event in agen:
525526
self.__maybe_save_output_to_state(event)
527+
output_accumulator = self.__maybe_accumulate_streaming_output(
528+
event, output_accumulator
529+
)
526530
yield event
527531
if ctx.should_pause_invocation(event):
528532
# Do not pause immediately, wait until the long-running tool call is
@@ -544,9 +548,13 @@ async def _run_async_impl(
544548
async def _run_live_impl(
545549
self, ctx: InvocationContext
546550
) -> AsyncGenerator[Event, None]:
551+
output_accumulator = ''
547552
async with Aclosing(self._llm_flow.run_live(ctx)) as agen:
548553
async for event in agen:
549554
self.__maybe_save_output_to_state(event)
555+
output_accumulator = self.__maybe_accumulate_streaming_output(
556+
event, output_accumulator
557+
)
550558
yield event
551559
if ctx.end_invocation:
552560
return
@@ -962,6 +970,47 @@ def __maybe_save_output_to_state(self, event: Event):
962970
result = validate_schema(self.output_schema, result)
963971
event.actions.state_delta[self.output_key] = result
964972

973+
def __maybe_accumulate_streaming_output(
974+
self, event: Event, accumulator: str
975+
) -> str:
976+
"""Accumulates output_key text across a streaming model turn.
977+
978+
Streaming with tool calls produces non-partial events that carry text
979+
alongside a function_call. is_final_response() rejects those, so
980+
__maybe_save_output_to_state skips them and the text on those events
981+
is dropped from output_key. Accumulate every non-partial text-bearing
982+
event from this agent across the model turn so the segments survive
983+
in session state. See issue #5590.
984+
985+
No-op when accumulation doesn't apply (different author, no
986+
output_key, output_schema set, partial event, no content, no text).
987+
For applicable events, appends the event's text to ``accumulator``
988+
and writes the running value to state_delta[output_key], overwriting
989+
any value __maybe_save_output_to_state set on the same event.
990+
Returns the new accumulator value.
991+
"""
992+
if (
993+
not self.output_key
994+
or self.output_schema
995+
or event.author != self.name
996+
or event.partial
997+
or not event.content
998+
or not event.content.parts
999+
):
1000+
return accumulator
1001+
1002+
text = ''.join(
1003+
part.text
1004+
for part in event.content.parts
1005+
if part.text and not part.thought
1006+
)
1007+
if not text:
1008+
return accumulator
1009+
1010+
accumulator += text
1011+
event.actions.state_delta[self.output_key] = accumulator
1012+
return accumulator
1013+
9651014
@model_validator(mode='after')
9661015
def __model_validator_after(self) -> LlmAgent:
9671016
return self

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/flows/llm_flows/base_llm_flow.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -578,18 +578,9 @@ async def run_live(
578578
# initial_history_in_client_content to True. This tells the Live server
579579
# that the provided history already includes the model's past responses,
580580
# preventing the server from generating duplicate responses for those replayed turns.
581-
#
582-
# ``history_config`` is only supported by the Gemini Developer API
583-
# backend; the Vertex AI / Gemini Enterprise Agent Platform backend has
584-
# no such field on its live setup message and rejects it. On Vertex,
585-
# history is instead seeded by ``send_history`` below
586-
# (``send_client_content`` with prior turns), so we skip
587-
# ``history_config`` for that backend.
588581
if (
589582
llm_request.contents
590583
and not invocation_context.live_session_resumption_handle
591-
and isinstance(llm, Gemini)
592-
and llm._api_backend == GoogleLLMVariant.GEMINI_API # pylint: disable=protected-access
593584
):
594585
if not llm_request.live_connect_config:
595586
llm_request.live_connect_config = types.LiveConnectConfig()

0 commit comments

Comments
 (0)