Skip to content

Commit 59f3c98

Browse files
authored
Merge branch 'main' into feat/get-user-state
2 parents 8e18bdc + ae95a97 commit 59f3c98

6 files changed

Lines changed: 667 additions & 12 deletions

File tree

.agents/skills/adk-pr-triage/SKILL.md

Lines changed: 302 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Helper script for ADK PR Triage verification and remote update."""
17+
18+
from __future__ import annotations
19+
20+
import argparse
21+
import json
22+
import subprocess
23+
import sys
24+
25+
26+
def run_command(args: list[str]) -> tuple[int, str, str]:
27+
"""Runs a shell command and returns its exit code, stdout, and stderr."""
28+
try:
29+
res = subprocess.run(args, capture_output=True, text=True, check=False)
30+
return res.returncode, res.stdout.strip(), res.stderr.strip()
31+
except Exception as e:
32+
return -1, "", str(e)
33+
34+
35+
def verify_cla(pr_number: str) -> bool:
36+
"""Verifies if the Google CLA is signed for the given PR number."""
37+
print(f"[*] Fetching status checks for PR #{pr_number}...")
38+
cmd = [
39+
"gh",
40+
"pr",
41+
"view",
42+
pr_number,
43+
"--repo",
44+
"google/adk-python",
45+
"--json",
46+
"statusCheckRollup",
47+
]
48+
code, stdout, stderr = run_command(cmd)
49+
if code != 0:
50+
print(
51+
f"Error: Failed to fetch PR details from GitHub: {stderr}",
52+
file=sys.stderr,
53+
)
54+
sys.exit(1)
55+
56+
try:
57+
data = json.loads(stdout)
58+
except json.JSONDecodeError:
59+
print("Error: Failed to parse GitHub API JSON response.", file=sys.stderr)
60+
sys.exit(1)
61+
62+
status_checks = data.get("statusCheckRollup", [])
63+
cla_check = None
64+
for check in status_checks:
65+
if check.get("name") == "cla/google":
66+
cla_check = check
67+
break
68+
69+
if not cla_check:
70+
print("\n" + "=" * 80)
71+
print("🚨 CRITICAL COMPLIANCE REFUSAL: GOOGLE CLA NOT SIGNED/VERIFIED 🚨")
72+
print("=" * 80)
73+
print(
74+
"Error: The mandatory 'cla/google' status check is completely missing"
75+
" on GitHub."
76+
)
77+
print(
78+
"The contributor HAS NOT signed the Google Contributor License"
79+
" Agreement."
80+
)
81+
print(
82+
"Legal policy strictly prohibits triaging, downloading, or reviewing"
83+
" this PR."
84+
)
85+
print("=" * 80 + "\n")
86+
return False
87+
88+
conclusion = cla_check.get("conclusion")
89+
if conclusion != "SUCCESS":
90+
print("\n" + "=" * 80)
91+
print("🚨 CRITICAL COMPLIANCE REFUSAL: GOOGLE CLA NOT SIGNED/VERIFIED 🚨")
92+
print("=" * 80)
93+
print(
94+
"Error: The 'cla/google' status check has the status:"
95+
f" '{conclusion or 'UNKNOWN'}'."
96+
)
97+
print(
98+
"The contributor HAS NOT successfully signed or verified the Google"
99+
" CLA."
100+
)
101+
print(
102+
"Legal policy strictly prohibits triaging, downloading, or reviewing"
103+
" this PR."
104+
)
105+
print("=" * 80 + "\n")
106+
return False
107+
108+
print("✅ Google CLA is verified and signed (status SUCCESS).")
109+
return True
110+
111+
112+
def update_pr_branch(pr_number: str) -> None:
113+
"""Updates the remote PR branch with the latest changes from the base branch."""
114+
print(
115+
f"\n[*] Attempting to update PR #{pr_number} branch via remote REBASE..."
116+
)
117+
rebase_cmd = [
118+
"gh",
119+
"pr",
120+
"update-branch",
121+
pr_number,
122+
"--rebase",
123+
"--repo",
124+
"google/adk-python",
125+
]
126+
code, stdout, stderr = run_command(rebase_cmd)
127+
if code == 0:
128+
print(
129+
"✅ Successfully updated PR branch on GitHub by rebasing onto base"
130+
" branch!"
131+
)
132+
if stdout:
133+
print(stdout)
134+
return
135+
136+
print(f"Warning: Remote rebase-update failed: {stderr}")
137+
print("[*] Falling back to standard remote MERGE commit update...")
138+
139+
merge_cmd = [
140+
"gh",
141+
"pr",
142+
"update-branch",
143+
pr_number,
144+
"--repo",
145+
"google/adk-python",
146+
]
147+
code, stdout, stderr = run_command(merge_cmd)
148+
if code == 0:
149+
print(
150+
"✅ Successfully updated PR branch on GitHub via standard merge commit!"
151+
)
152+
if stdout:
153+
print(stdout)
154+
return
155+
156+
print(
157+
"\n[!] Warning: Remote branch update failed completely on GitHub server:"
158+
f" {stderr}"
159+
)
160+
print(" This is typical if edits are disabled on the contributor's fork.")
161+
print(
162+
" No worries! We will automatically rebase locally after checking out."
163+
)
164+
165+
166+
def main() -> None:
167+
parser = argparse.ArgumentParser(
168+
description="Triage PR verification and sync helper."
169+
)
170+
parser.add_argument(
171+
"pr_number", help="The GitHub Pull Request number (e.g. 5875)."
172+
)
173+
parser.add_argument(
174+
"--skip-update",
175+
action="store_true",
176+
help="Skip updating the remote PR branch on GitHub.",
177+
)
178+
args = parser.parse_args()
179+
180+
# Step 1: Verify CLA
181+
if not verify_cla(args.pr_number):
182+
sys.exit(2) # Exit code 2 indicates compliance refusal
183+
184+
# Step 2: Update branch
185+
if not args.skip_update:
186+
update_pr_branch(args.pr_number)
187+
188+
print("\n[*] Verification complete. Safe to proceed with checkout.")
189+
sys.exit(0)
190+
191+
192+
if __name__ == "__main__":
193+
main()

.github/workflows/copybara-pr-handler.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ jobs:
5757
console.log(`\n--- Processing commit ${sha.substring(0, 7)} ---`);
5858
console.log(`Committer: ${committer}`);
5959
60-
// Check if this is a Copybara commit
60+
// Check if this is a Copybara commit or has a pull request reference
6161
const isCopybara = committer === 'Copybara-Service' ||
6262
commit.author?.email === 'genai-sdk-bot@google.com' ||
6363
message.includes('GitOrigin-RevId:') ||
64-
message.includes('PiperOrigin-RevId:');
64+
message.includes('PiperOrigin-RevId:') ||
65+
/Merge:?\s+https:\/\/github\.com\/google\/adk-python\/pull\/(\d+)/.test(message);
6566
6667
if (!isCopybara) {
6768
console.log('Not a Copybara commit, skipping');
@@ -118,6 +119,14 @@ jobs:
118119
body: `Thank you @${author} for your contribution! 🎉\n\nYour changes have been successfully imported and merged via Copybara in commit ${commitSha}.\n\nClosing this PR as the changes are now in the main branch.`
119120
});
120121
122+
// Add 'merged' label to the PR
123+
await github.rest.issues.addLabels({
124+
owner: context.repo.owner,
125+
repo: context.repo.repo,
126+
issue_number: prNumber,
127+
labels: ['merged']
128+
});
129+
121130
// Close the PR
122131
await github.rest.pulls.update({
123132
owner: context.repo.owner,

AGENTS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ For all matters regarding ADK development, please use the appropriate skill:
1818
- Read `.agents/skills/adk-review/SKILL.md` for full instructions.
1919
- **`adk-issue`**: Use this skill when analyzing and triaging GitHub issues for the adk-python repository to verify legitimacy, recommend fixes, and check for existing PRs.
2020
- Read `.agents/skills/adk-issue/SKILL.md` for full instructions.
21+
- **`adk-pr-triage`**: Use this skill when triaging and analyzing GitHub pull requests (PRs) to evaluate their objectives, legitimacy, value, and alignment with ADK's architectural, styling, and testing principles.
22+
- Read `.agents/skills/adk-pr-triage/SKILL.md` for full instructions.
23+
2124

2225
## Project Overview
2326

src/google/adk/flows/llm_flows/base_llm_flow.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ async def _process_agent_tools(
420420
instances, and calls ``process_llm_request`` on each to register
421421
tool declarations in the request.
422422
423+
Tool-union resolution is dispatched concurrently via ``asyncio.gather``
424+
to overlap I/O-bound listings (e.g. MCP ``list_tools`` over the
425+
network). The subsequent ``process_llm_request`` calls are kept
426+
serial in the original ``agent.tools`` order: some tools read/write
427+
``llm_request`` state (e.g. ``GoogleSearchTool`` writes
428+
``llm_request.model``; ``ComputerUseToolset`` performs an idempotency
429+
check on ``llm_request.config.tools``) and rely on observing the
430+
post-state of earlier tools.
431+
423432
After this function returns, ``llm_request.tools_dict`` maps tool
424433
names to ``BaseTool`` instances ready for function call dispatch.
425434
@@ -429,12 +438,34 @@ async def _process_agent_tools(
429438
llm_request: The LLM request to populate with tool declarations.
430439
"""
431440
agent = invocation_context.agent
432-
if not hasattr(agent, 'tools') or not agent.tools:
441+
if agent is None or not hasattr(agent, 'tools') or not agent.tools:
433442
return
434443

435444
multiple_tools = len(agent.tools) > 1
436445
model = agent.canonical_model
437-
for tool_union in agent.tools:
446+
447+
from ...agents.llm_agent import _convert_tool_union_to_tools
448+
449+
# Resolve tool_unions in parallel. ``asyncio.gather`` preserves
450+
# input order in the returned list, so the serial commit phase below
451+
# still observes ``agent.tools`` order. If any resolution raises,
452+
# gather cancels the siblings and propagates -- same observable
453+
# behavior as the previous serial loop, which would propagate the
454+
# first exception and abandon the rest.
455+
resolved_tools_per_union = await asyncio.gather(*(
456+
_convert_tool_union_to_tools(
457+
tool_union,
458+
ReadonlyContext(invocation_context),
459+
model,
460+
multiple_tools,
461+
)
462+
for tool_union in agent.tools
463+
))
464+
465+
# Serial commit phase, in original ``agent.tools`` order. Mutations
466+
# to ``llm_request`` and reads of its state (model, config.tools,
467+
# tools_dict) preserve today's ordering semantics exactly.
468+
for tool_union, tools in zip(agent.tools, resolved_tools_per_union):
438469
tool_context = ToolContext(invocation_context)
439470

440471
# If it's a toolset, process it first
@@ -443,15 +474,7 @@ async def _process_agent_tools(
443474
tool_context=tool_context, llm_request=llm_request
444475
)
445476

446-
from ...agents.llm_agent import _convert_tool_union_to_tools
447-
448477
# Then process all tools from this tool union
449-
tools = await _convert_tool_union_to_tools(
450-
tool_union,
451-
ReadonlyContext(invocation_context),
452-
model,
453-
multiple_tools,
454-
)
455478
for tool in tools:
456479
await tool.process_llm_request(
457480
tool_context=tool_context, llm_request=llm_request

0 commit comments

Comments
 (0)