chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.10) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.11) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.12) (push) Waiting to run
Continuous Integration / Mypy Check (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.10) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.11) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.12) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.13) (push) Waiting to run
Continuous Integration / Unit Tests (Python 3.14) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Waiting to run
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Waiting to run
Copybara PR Handler / close-imported-pr (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
@@ -0,0 +1,58 @@
# ADK Agent with Plugin
### What is ADK Plugin?
At its core, ADK extensibility is built on
[**callbacks**](https://google.github.io/adk-docs/callbacks/): functions you
write that ADK automatically executes at key stages of an agent's lifecycle.
**A Plugin is simply a class that packages these individual callback functions
together for a broader purpose.**
While a standard Agent Callback is configured on a *single agent, a single tool*
for a *specific task*, a Plugin is registered *once* on the `Runner` and its
callbacks apply *globally* to every agent, tool, and LLM call managed by that
runner. This makes Plugins the ideal solution for implementing horizontal
features that cut across your entire application.
### What can plugins do?
Plugins are incredibly versatile. By implementing different callback methods, you
can achieve a wide range of functionalities.
- **Logging & Tracing**: Create detailed logs of agent, tool, and LLM activity
for debugging and performance analysis.
- **Policy Enforcement**: Implement security guardrails. For example, a
before_tool_callback can check if a user is authorized to use a specific
tool and prevent its execution by returning a value.
- **Monitoring & Metrics**: Collect and export metrics on token usage,
execution times, and invocation counts to monitoring systems like Prometheus
or Stackdriver.
- **Caching**: In before_model_callback or before_tool_callback, you can
check if a request has been made before. If so, you can return a cached
response, skipping the expensive LLM or tool call entirely.
- **Request/Response Modification**: Dynamically add information to LLM prompts
(e.g., in before_model_callback) or standardize tool outputs (e.g., in
after_tool_callback).
### Run the agent
**Note: Plugin is NOT supported in `adk web`yet.**
Use following command to run the main.py
```bash
python3 -m contributing.samples.plugin_basic.main
```
It should output the following content. Note that the outputs from plugin are
printed.
```bash
[Plugin] Agent run count: 1
[Plugin] LLM request count: 1
** Got event from hello_world
Hello world: query is [hello world]
** Got event from hello_world
[Plugin] LLM request count: 2
** Got event from hello_world
```
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from .main import root_agent
@@ -0,0 +1,43 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from google.adk.agents.base_agent import BaseAgent
from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest
from google.adk.plugins.base_plugin import BasePlugin
class CountInvocationPlugin(BasePlugin):
"""A custom plugin that counts agent and tool invocations."""
def __init__(self) -> None:
"""Initialize the plugin with counters."""
super().__init__(name="count_invocation")
self.agent_count: int = 0
self.tool_count: int = 0
self.llm_request_count: int = 0
async def before_agent_callback(
self, *, agent: BaseAgent, callback_context: CallbackContext
) -> None:
"""Count agent runs."""
self.agent_count += 1
print(f"[Plugin] Agent run count: {self.agent_count}")
async def before_model_callback(
self, *, callback_context: CallbackContext, llm_request: LlmRequest
) -> None:
"""Count LLM requests."""
self.llm_request_count += 1
print(f"[Plugin] LLM request count: {self.llm_request_count}")
@@ -0,0 +1,64 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
from google.adk import Agent
from google.adk.runners import InMemoryRunner
from google.adk.tools.tool_context import ToolContext
from google.genai import types
# [Step 2] Import the plugin.
from .count_plugin import CountInvocationPlugin
async def hello_world(tool_context: ToolContext, query: str):
print(f'Hello world: query is [{query}]')
root_agent = Agent(
name='hello_world',
description='Prints hello world with user query.',
instruction="""Use hello_world tool to print hello world and user query.
""",
tools=[hello_world],
)
async def main():
"""Main entry point for the agent."""
prompt = 'hello world'
runner = InMemoryRunner(
agent=root_agent,
app_name='test_app_with_plugin',
# [Step 2] Add your plugin here. You can add multiple plugins.
plugins=[CountInvocationPlugin()],
)
session = await runner.session_service.create_session(
user_id='user',
app_name='test_app_with_plugin',
)
async for event in runner.run_async(
user_id='user',
session_id=session.id,
new_message=types.Content(
role='user', parts=[types.Part.from_text(text=prompt)]
),
):
print(f'** Got event from {event.author}')
if __name__ == '__main__':
asyncio.run(main())
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,123 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Sample agent demonstrating DebugLoggingPlugin usage.
This sample shows how to use the DebugLoggingPlugin to capture complete
debug information (LLM requests/responses, tool calls, events, session state)
to a YAML file for debugging purposes.
Usage:
adk run contributing/samples/plugin_debug_logging
After running, check the generated `adk_debug.yaml` file for detailed logs.
"""
from typing import Any
from google.adk.agents import LlmAgent
from google.adk.apps import App
from google.adk.plugins import DebugLoggingPlugin
def get_weather(city: str) -> dict[str, Any]:
"""Get the current weather for a city.
Args:
city: The name of the city to get weather for.
Returns:
A dictionary containing weather information.
"""
# Simulated weather data
weather_data = {
"new york": {"temperature": 22, "condition": "sunny", "humidity": 45},
"london": {"temperature": 15, "condition": "cloudy", "humidity": 70},
"tokyo": {"temperature": 28, "condition": "humid", "humidity": 85},
"paris": {"temperature": 18, "condition": "rainy", "humidity": 80},
}
city_lower = city.lower()
if city_lower in weather_data:
data = weather_data[city_lower]
return {
"city": city,
"temperature_celsius": data["temperature"],
"condition": data["condition"],
"humidity_percent": data["humidity"],
}
else:
return {
"city": city,
"error": f"Weather data not available for {city}",
}
def calculate(expression: str) -> dict[str, Any]:
"""Evaluate a simple mathematical expression.
Args:
expression: A mathematical expression to evaluate (e.g., "2 + 2").
Returns:
A dictionary containing the result or error.
"""
try:
# Only allow safe mathematical operations
allowed_chars = set("0123456789+-*/.() ")
if not all(c in allowed_chars for c in expression):
return {"error": "Invalid characters in expression"}
result = eval(expression) # Safe due to character restriction
return {"expression": expression, "result": result}
except Exception as e:
return {"expression": expression, "error": str(e)}
# Sample queries to try:
# - "What's the weather in Tokyo?"
# - "Calculate 15 * 7 + 3"
# - "What's the weather in London and calculate 100 / 4"
root_agent = LlmAgent(
name="debug_demo_agent",
description="A demo agent that shows DebugLoggingPlugin capabilities",
instruction="""You are a helpful assistant that can:
1. Get weather information for cities (New York, London, Tokyo, Paris)
2. Perform simple calculations
When asked about weather, use the get_weather tool.
When asked to calculate, use the calculate tool.
Be concise in your responses.""",
tools=[get_weather, calculate],
)
# Create the app with DebugLoggingPlugin
# The plugin will write detailed debug information to adk_debug.yaml
app = App(
name="plugin_debug_logging",
root_agent=root_agent,
plugins=[
# DebugLoggingPlugin captures complete interaction data to a YAML file
# Options:
# output_path: Path to output file (default: "adk_debug.yaml")
# include_session_state: Include session state snapshot (default: True)
# include_system_instruction: Include full system instruction (default: True)
DebugLoggingPlugin(
output_path="adk_debug.yaml",
include_session_state=True,
include_system_instruction=True,
),
],
)
@@ -0,0 +1,75 @@
# Reflect And Retry Tool Plugin
`ReflectAndRetryToolPlugin` provides self-healing, concurrent-safe error
recovery for tool failures.
**Key Features:**
- **Concurrency Safe:** Uses locking to safely handle parallel tool
executions
- **Configurable Scope:** Tracks failures per-invocation (default) or globally
using the `TrackingScope` enum.
- **Extensible Scoping:** The `_get_scope_key` method can be overridden to
implement custom tracking logic (e.g., per-user or per-session).
- **Granular Tracking:** Failure counts are tracked per-tool within the
defined scope. A success with one tool resets its counter without affecting
others.
- **Custom Error Extraction:** Supports detecting errors in normal tool
responses that don't throw exceptions, by overriding the
`extract_error_from_result` method.
## Samples
Here are some sample agents to demonstrate the usage of the plugin.
### Basic Usage
This is a hello world example to show the basic usage of the plugin. The
`guess_number_tool` is hacked with both Exceptions and error responses. With the
help of the `CustomRetryPlugin`, both above error types can lead to retries.
For example, here is the output from agent:
```
I'll guess the number 50. Let's see how it is!
My guess of 50 was too high! I'll try a smaller number this time. Let's go with 25.
My guess of 25 was still too high! I'm going smaller. How about 10?
Still too high! My guess of 10 was also too large. I'll try 5 this time.
My guess of 5 is "almost valid"! That's good news, it means I'm getting very close. I'll try 4.
My guess of 4 is still "almost valid," just like 5. It seems I'm still hovering around the right answer. Let's try 3!
I guessed the number 3, and it is valid! I found it!
```
You can run the agent with:
```bash
$ adk web contributing/samples/plugin_reflect_tool_retry
```
Select "basic" and provide the following prompt to see the agent retrying tool
calls:
```
Please guess a number! Tell me what number you guess and how is it.
```
### Hallucinating tool calls
The "hallucinating_func_name" agent is an example to show the plugin can retry
hallucinating tool calls.
For example, we used the `after_model_callback` to hack a tool call with the
wrong name then the agent can retry calling with the right tool name.
You can run the agent with:
```bash
$ adk web contributing/samples/plugin_reflect_tool_retry
```
Select "hallucinating_func_name" and provide the following prompt to see the
agent retrying tool calls:
```
Roll a 6 sided die
```
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,83 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any
from google.adk.agents import LlmAgent
from google.adk.apps.app import App
from google.adk.plugins import LoggingPlugin
from google.adk.plugins import ReflectAndRetryToolPlugin
APP_NAME = "basic"
USER_ID = "test_user"
def guess_number_tool(query: int) -> dict[str, Any]:
"""A tool that guesses a number.
Args:
query: The number to guess.
Returns:
A dictionary containing the status and result of the tool execution.
"""
target_number = 3
if query == target_number:
return {"status": "success", "result": "Number is valid."}
if abs(query - target_number) <= 2:
return {"status": "error", "error_message": "Number is almost valid."}
if query > target_number:
raise ValueError("Number is too large.")
if query < target_number:
raise ValueError("Number is too small.")
raise ValueError("Number is invalid.")
class CustomRetryPlugin(ReflectAndRetryToolPlugin):
async def extract_error_from_result(
self, *, tool, tool_args, tool_context, result
):
return result if result.get("status") == "error" else None
# Sample query: "guess a number between 1 and 50"
root_agent = LlmAgent(
name="hello_world",
description="Helpful agent",
instruction="""Your goal is to guess a secret positive integer by using the
`guess_number_tool`.
The tool will provide feedback on each guess.
Your objective is to keep guessing until guess_number_tool returns
'status: success'.
Start by guessing 50, and use the tool's feedback to adjust your guesses
and find the target number.""",
tools=[guess_number_tool],
)
app = App(
name=APP_NAME,
root_agent=root_agent,
plugins=[
CustomRetryPlugin(
max_retries=20, throw_exception_if_retry_exceeded=False
),
LoggingPlugin(),
],
)
@@ -0,0 +1,15 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from . import agent
@@ -0,0 +1,82 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import random
from google.adk.agents import LlmAgent
from google.adk.agents.callback_context import CallbackContext
from google.adk.apps.app import App
from google.adk.models.llm_response import LlmResponse
from google.adk.plugins import ReflectAndRetryToolPlugin
from google.adk.tools.tool_context import ToolContext
APP_NAME = "hallucinating_func_name"
USER_ID = "test_user"
hallucinated = False # Whether the tool name is hallucinated
def roll_die(sides: int, tool_context: ToolContext) -> int:
"""Roll a die and return the rolled result.
Args:
sides: The integer number of sides the die has.
Returns:
An integer of the result of rolling the die.
"""
result = random.randint(1, sides)
if not "rolls" in tool_context.state:
tool_context.state["rolls"] = []
tool_context.state["rolls"] = tool_context.state["rolls"] + [result]
return result
def after_model_callback(
callback_context: CallbackContext, llm_response: LlmResponse
):
"""After model callback to produce one hallucinating tool call."""
global hallucinated
if hallucinated:
return None
if (
llm_response.content
and llm_response.content.parts
and llm_response.content.parts[0].function_call
and llm_response.content.parts[0].function_call.name == "roll_die"
):
llm_response.content.parts[0].function_call.name = "roll_die_wrong_name"
hallucinated = True
return None
root_agent = LlmAgent(
name="hello_world",
description="Helpful agent",
instruction="""Use guess_number_tool to guess a number.""",
tools=[roll_die],
after_model_callback=after_model_callback,
)
app = App(
name=APP_NAME,
root_agent=root_agent,
plugins=[
ReflectAndRetryToolPlugin(max_retries=3),
],
)