Skip to content

Commit 67bf8e8

Browse files
authored
Merge branch 'main' into fix/safe-json-serialize-exception-handling
2 parents 39607ef + 029b87d commit 67bf8e8

16 files changed

Lines changed: 509 additions & 74 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ optional-dependencies.extensions = [
124124
"k8s-agent-sandbox>=0.1.1.post3", # For GkeCodeExecutor sandbox mode
125125
"kubernetes>=29", # For GkeCodeExecutor
126126
"langgraph>=0.2.60,<0.4.8", # For LangGraphAgent
127-
"litellm>=1.75.5,<=1.82.6", # For LiteLlm class. Upper bound pinned: versions 1.82.7+ compromised in supply chain attack.
127+
"litellm>=1.83.7,<=1.83.14", # For LiteLlm class. Lower bound: 5 CVE patches (2026-04). Upper bound pinned to current latest; bump deliberately. See #5488.
128128
"llama-index-embeddings-google-genai>=0.3", # For files retrieval using LlamaIndex.
129129
"llama-index-readers-file>=0.4", # For retrieval using LlamaIndex.
130130
"lxml>=5.3", # For load_web_page tool.
@@ -143,7 +143,7 @@ optional-dependencies.test = [
143143
"kubernetes>=29", # For GkeCodeExecutor
144144
"langchain-community>=0.3.17",
145145
"langgraph>=0.2.60,<0.4.8", # For LangGraphAgent
146-
"litellm>=1.75.5,<=1.82.6", # For LiteLLM tests. Upper bound pinned: versions 1.82.7+ compromised in supply chain attack.
146+
"litellm>=1.83.7,<=1.83.14", # For LiteLLM tests. Lower bound: 5 CVE patches (2026-04). Upper bound pinned to current latest; bump deliberately. See #5488.
147147
"llama-index-readers-file>=0.4", # For retrieval tests
148148
"openai>=1.100.2", # For LiteLLM
149149
"opentelemetry-instrumentation-google-genai>=0.3b0,<1",

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,11 @@ def _construct_message_parts_from_session(
422422
if not isinstance(converted_parts, list):
423423
converted_parts = [converted_parts] if converted_parts else []
424424

425+
if event.author == "user":
426+
for part in converted_parts:
427+
part.root.metadata = part.root.metadata or {}
428+
part.root.metadata["is_user_input"] = True
429+
425430
if converted_parts:
426431
message_parts.extend(converted_parts)
427432
else:

src/google/adk/integrations/agent_identity/gcp_auth_provider.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def _construct_auth_credential(
8282
return AuthCredential(
8383
auth_type=AuthCredentialTypes.HTTP,
8484
http=HttpAuth(
85-
scheme="bearer",
85+
scheme="Bearer",
8686
credentials=HttpCredentials(token=response.token),
8787
),
8888
)

src/google/adk/optimization/local_eval_sampler.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,11 +289,7 @@ def _extract_eval_data(
289289
for eval_metric_result in per_invocation_result.eval_metric_results:
290290
eval_metric_results.append({
291291
"metric_name": eval_metric_result.metric_name,
292-
"score": (
293-
round(eval_metric_result.score, 2)
294-
if eval_metric_result.score is not None
295-
else None
296-
), # accurate enough
292+
"score": round(eval_metric_result.score, 2), # accurate enough
297293
"eval_status": eval_metric_result.eval_status.name,
298294
})
299295
per_invocation_result_dict = {

src/google/adk/plugins/reflect_retry_tool_plugin.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -242,8 +242,8 @@ async def _handle_tool_error(
242242
"""
243243
if self.max_retries == 0:
244244
if self.throw_exception_if_retry_exceeded:
245-
raise error
246-
return self._get_tool_retry_exceed_msg(tool, error, tool_args)
245+
raise self._ensure_exception(error)
246+
return self._get_tool_retry_exceed_msg(tool, tool_args, error)
247247

248248
scope_key = self._get_scope_key(tool_context)
249249
async with self._lock:
@@ -260,7 +260,7 @@ async def _handle_tool_error(
260260

261261
# Max Retry exceeded
262262
if self.throw_exception_if_retry_exceeded:
263-
raise error
263+
raise self._ensure_exception(error)
264264
else:
265265
return self._get_tool_retry_exceed_msg(tool, tool_args, error)
266266

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Interface for skill registry."""
16+
17+
from __future__ import annotations
18+
19+
import abc
20+
from typing import Any
21+
from typing import Dict
22+
from typing import List
23+
24+
from . import models
25+
26+
27+
class SkillRegistry(abc.ABC):
28+
"""Interface for a skill registry."""
29+
30+
@abc.abstractmethod
31+
async def get_skill(
32+
self, *, name: str, version: str | None = None
33+
) -> models.Skill:
34+
"""Fetches a skill from the registry.
35+
36+
Args:
37+
name: The name of the skill.
38+
version: Optional version of the skill.
39+
40+
Returns:
41+
A Skill object.
42+
"""
43+
pass
44+
45+
@abc.abstractmethod
46+
async def search_skills(
47+
self,
48+
*,
49+
query: str,
50+
filters: Dict[str, Any] | None = None,
51+
**kwargs,
52+
) -> List[models.Frontmatter]:
53+
"""Searches for skills in the registry.
54+
55+
Args:
56+
query: The search query.
57+
filters: Optional filters.
58+
**kwargs: Additional implementation-specific arguments.
59+
60+
Returns:
61+
A list of Frontmatter objects for discovery.
62+
"""
63+
pass
64+
65+
@abc.abstractmethod
66+
def get_filter_schema(self) -> Dict[str, Any] | None:
67+
"""Returns the JSON schema for the filters supported by this registry.
68+
69+
Returns:
70+
A JSON schema dict or None if filters are not supported
71+
"""
72+
pass
73+
74+
def get_search_description(self) -> str:
75+
"""Returns the description for the search_skills tool.
76+
77+
Registries can override this to provide specialized instructions to the
78+
model on how to use their specific search capabilities.
79+
"""
80+
return (
81+
"Searches for relevant skills in the registry based on a semantic or"
82+
" keyword query."
83+
)

src/google/adk/telemetry/_experimental_semconv.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
try:
5252
from opentelemetry.semconv._incubating.attributes.gen_ai_attributes import GEN_AI_TOOL_DEFINITIONS
5353
except ImportError:
54-
GEN_AI_TOOL_DEFINITIONS = 'gen_ai.tool_definitions'
54+
GEN_AI_TOOL_DEFINITIONS = 'gen_ai.tool.definitions'
5555

5656
OTEL_SEMCONV_STABILITY_OPT_IN = 'OTEL_SEMCONV_STABILITY_OPT_IN'
5757

src/google/adk/tools/skill_toolset.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from ..features import FeatureName
3838
from ..skills import models
3939
from ..skills import prompt
40+
from ..skills.skill_registry import SkillRegistry
4041
from .base_tool import BaseTool
4142
from .base_toolset import BaseToolset
4243
from .function_tool import FunctionTool
@@ -155,6 +156,20 @@ async def run_async(
155156
}
156157

157158
skill = self._toolset._get_skill(skill_name)
159+
if not skill and self._toolset._registry:
160+
try:
161+
skill = await self._toolset._registry.get_skill(name=skill_name)
162+
if skill:
163+
self._toolset._skills[skill_name] = skill
164+
except Exception as e:
165+
logger.exception(
166+
"Failed to fetch skill '%s' from registry.", skill_name
167+
)
168+
return {
169+
"error": f"Failed to fetch skill '{skill_name}' from registry: {e}",
170+
"error_code": "REGISTRY_ERROR",
171+
}
172+
158173
if not skill:
159174
return {
160175
"error": f"Skill '{skill_name}' not found.",
@@ -771,14 +786,75 @@ async def run_async(
771786
)
772787

773788

789+
@experimental(FeatureName.SKILL_TOOLSET)
790+
class SearchSkillsTool(BaseTool):
791+
"""Tool to search for relevant skills in the registry."""
792+
793+
def __init__(self, toolset: "SkillToolset"):
794+
super().__init__(
795+
name="search_skills",
796+
description=toolset._registry.get_search_description(),
797+
)
798+
self._toolset = toolset
799+
800+
def _get_declaration(self) -> types.FunctionDeclaration | None:
801+
properties = {
802+
"query": {
803+
"type": "string",
804+
"description": "Semantic or keyword search query.",
805+
},
806+
}
807+
filter_schema = self._toolset._registry.get_filter_schema()
808+
if filter_schema:
809+
properties["filters"] = filter_schema
810+
return types.FunctionDeclaration(
811+
name=self.name,
812+
description=self.description,
813+
parameters_json_schema={
814+
"type": "object",
815+
"properties": properties,
816+
"required": ["query"],
817+
},
818+
)
819+
820+
async def run_async(
821+
self, *, args: dict[str, Any], tool_context: ToolContext
822+
) -> Any:
823+
query = args.get("query")
824+
filters = args.get("filters")
825+
826+
if not query:
827+
return {
828+
"error": "Argument 'query' is required.",
829+
"error_code": "INVALID_ARGUMENTS",
830+
}
831+
832+
results = await self._toolset._registry.search_skills(
833+
query=query, filters=filters
834+
)
835+
836+
formatted_results = []
837+
for r in results:
838+
if r.name in self._toolset._skills:
839+
logger.warning(
840+
"Naming conflict detected: Skill '%s' exists both locally and in"
841+
" the registry. Filtering out the registry skill.",
842+
r.name,
843+
)
844+
continue
845+
formatted_results.append(r.model_dump())
846+
return formatted_results
847+
848+
774849
@experimental(FeatureName.SKILL_TOOLSET)
775850
class SkillToolset(BaseToolset):
776851
"""A toolset for managing and interacting with agent skills."""
777852

778853
def __init__(
779854
self,
780-
skills: list[models.Skill],
855+
skills: list[models.Skill] | None = None,
781856
*,
857+
registry: Optional[SkillRegistry] = None,
782858
code_executor: Optional[BaseCodeExecutor] = None,
783859
script_timeout: int = _DEFAULT_SCRIPT_TIMEOUT,
784860
additional_tools: list[ToolUnion] | None = None,
@@ -787,6 +863,7 @@ def __init__(
787863
788864
Args:
789865
skills: List of skills to register.
866+
registry: Optional skill registry for dynamic discovery.
790867
code_executor: Optional code executor for script execution.
791868
script_timeout: Timeout in seconds for shell script execution via
792869
subprocess.run. Defaults to 300 seconds. Does not apply to Python
@@ -796,12 +873,13 @@ def __init__(
796873

797874
# Check for duplicate skill names
798875
seen: set[str] = set()
799-
for skill in skills:
876+
for skill in skills or []:
800877
if skill.name in seen:
801878
raise ValueError(f"Duplicate skill name '{skill.name}'.")
802879
seen.add(skill.name)
803880

804-
self._skills = {skill.name: skill for skill in skills}
881+
self._skills = {skill.name: skill for skill in skills or []}
882+
self._registry = registry
805883
self._code_executor = code_executor
806884
self._script_timeout = script_timeout
807885
self._use_invocation_cache = False
@@ -824,6 +902,8 @@ def __init__(
824902
LoadSkillResourceTool(self),
825903
RunSkillScriptTool(self),
826904
]
905+
if self._registry:
906+
self._tools.append(SearchSkillsTool(self))
827907

828908
async def get_tools(
829909
self, readonly_context: ReadonlyContext | None = None
@@ -904,6 +984,12 @@ async def process_llm_request(
904984
skills_xml = prompt.format_skills_as_xml(skills)
905985
instructions = []
906986
instructions.append(_DEFAULT_SKILL_SYSTEM_INSTRUCTION)
987+
if self._registry:
988+
instructions.append(
989+
"\nYou can also use the `search_skills` tool to discover additional"
990+
" skills in the registry if the available skills listed below are"
991+
" not sufficient.\n"
992+
)
907993
instructions.append(skills_xml)
908994
llm_request.append_instructions(instructions)
909995

0 commit comments

Comments
 (0)