Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions contributing/samples/governance/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Governance Plugin Example

Budget enforcement, circuit breaking, and model degradation for ADK agents.

## Quickstart

```python
from google.adk_community.governance import GovernanceConfig, VeronicaGovernancePlugin

plugin = VeronicaGovernancePlugin(GovernanceConfig(max_cost_usd=1.0))

runner = Runner(agent=agent, session_service=session_service, plugins=[plugin])
```

That's it. The plugin intercepts model and tool callbacks automatically.

## Setup

```bash
pip install google-adk-community
export GOOGLE_API_KEY="your-key"
```

## Run the full example

```bash
python main.py
```

## What it does

The example creates three agents (orchestrator, researcher, summarizer) and
registers a `VeronicaGovernancePlugin` on the Runner. The plugin:

- Enforces a $0.50 org budget and $0.25 per-agent budget
- Blocks the `shell_exec` tool
- Degrades to `gemini-2.0-flash-lite` when budget hits 70%
- Disables `web_search` during degradation
- Trips the circuit breaker after 3 consecutive failures

After the run completes, the plugin logs a summary:

```
[GOVERNANCE] Run complete in 2.3s. Model calls: 4, Tool calls: 0.
[GOVERNANCE] Budget: $0.0023 / $0.5000 (0.5% used).
[GOVERNANCE] Agent 'researcher': $0.0012 / $0.2500.
[GOVERNANCE] Agent 'summarizer': $0.0008 / $0.2500.
```

If degradation triggers, the summary includes:

```
[GOVERNANCE] Degradation events (1):
[GOVERNANCE] Agent 'researcher' at 72.0% -- degraded gemini-2.5-flash -> gemini-2.0-flash-lite.
```
125 changes: 125 additions & 0 deletions contributing/samples/governance/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
# Copyright 2025 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.

"""Example: multi-agent workflow with governance plugin.

This example shows how to register VeronicaGovernancePlugin on an ADK
Runner to enforce per-agent budgets, block tools, and degrade to a
cheaper model when budget runs low.

Usage:
export GOOGLE_API_KEY="your-key"
python main.py
"""

import asyncio
import logging

from google.adk import Runner
from google.adk.agents import Agent
from google.adk.sessions import InMemorySessionService
from google.genai import types

from google.adk_community.governance import GovernanceConfig
from google.adk_community.governance import VeronicaGovernancePlugin

logging.basicConfig(level=logging.INFO, format="%(message)s")


def main():
# Configure governance limits
config = GovernanceConfig(
max_cost_usd=0.50, # org-level: 50 cents
agent_max_cost_usd=0.25, # per-agent: 25 cents
failure_threshold=3, # circuit breaker after 3 failures
recovery_timeout_s=30.0,
degradation_threshold=0.7, # degrade at 70% budget
fallback_model="gemini-2.0-flash-lite",
blocked_tools=["shell_exec"],
disable_tools_on_degrade=["web_search"],
)

plugin = VeronicaGovernancePlugin(config=config)

# Define agents
researcher = Agent(
model="gemini-2.5-flash",
name="researcher",
instruction=(
"You are a research assistant. Answer questions using your"
" knowledge. Be concise."
),
)

summarizer = Agent(
model="gemini-2.5-flash",
name="summarizer",
instruction=(
"You summarize text provided to you. Keep summaries to 2-3 sentences."
),
)

# Orchestrator delegates to sub-agents
orchestrator = Agent(
model="gemini-2.5-flash",
name="orchestrator",
instruction=(
"You coordinate research tasks. Use the researcher agent to"
" find information, then the summarizer to condense it."
),
sub_agents=[researcher, summarizer],
)

# Create runner with governance plugin
session_service = InMemorySessionService()
runner = Runner(
agent=orchestrator,
app_name="governance_demo",
session_service=session_service,
plugins=[plugin],
)

async def run():
session = await session_service.create_session(
app_name="governance_demo",
user_id="demo_user",
)

user_message = types.Content(
role="user",
parts=[types.Part(text="What is agent governance?")],
)

async for event in runner.run_async(
session_id=session.id,
user_id="demo_user",
new_message=user_message,
):
if event.content and event.content.parts:
for part in event.content.parts:
if part.text:
print(f"[{event.author}] {part.text[:200]}")

# After run, the plugin logs a governance summary automatically.
# You can also inspect programmatically:
snap = plugin.budget.snapshot()
print(f"\nTotal spent: ${snap.org_spent_usd:.4f}")
for agent, spent in snap.agent_spent.items():
print(f" {agent}: ${spent:.4f}")

asyncio.run(run())


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions src/google/adk_community/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from . import governance
from . import memory
from . import sessions
from . import version
Expand Down
23 changes: 23 additions & 0 deletions src/google/adk_community/governance/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2025 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.

"""Community governance plugins for ADK."""

from .veronica_governance_plugin import GovernanceConfig
from .veronica_governance_plugin import VeronicaGovernancePlugin

__all__ = [
"GovernanceConfig",
"VeronicaGovernancePlugin",
]
109 changes: 109 additions & 0 deletions src/google/adk_community/governance/_budget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# Copyright 2025 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.

"""Budget tracking for governance plugin."""

from __future__ import annotations

from dataclasses import dataclass
import threading


@dataclass
class BudgetSnapshot:
"""Read-only snapshot of current budget state."""

org_spent_usd: float
org_limit_usd: float
agent_spent: dict[str, float]
agent_limit_usd: float

@property
def org_utilization(self) -> float:
if self.org_limit_usd <= 0:
return 1.0
return self.org_spent_usd / self.org_limit_usd


class BudgetTracker:
"""Thread-safe budget tracker with per-agent and org-level limits."""

def __init__(
self,
*,
org_limit_usd: float,
agent_limit_usd: float,
cost_per_1k_input_tokens: float,
cost_per_1k_output_tokens: float,
) -> None:
self._org_limit_usd = org_limit_usd
self._agent_limit_usd = agent_limit_usd
self._cost_per_1k_input = cost_per_1k_input_tokens
self._cost_per_1k_output = cost_per_1k_output_tokens
self._org_spent_usd: float = 0.0
self._agent_spent: dict[str, float] = {}
self._lock = threading.Lock()

def estimate_cost(
self,
input_tokens: int,
output_tokens: int,
) -> float:
"""Estimate cost from token counts (clamped to non-negative)."""
raw = (
max(input_tokens, 0) / 1000.0 * self._cost_per_1k_input
+ max(output_tokens, 0) / 1000.0 * self._cost_per_1k_output
)
return max(raw, 0.0)

def check(self, agent_name: str) -> tuple[bool, str]:
"""Check if agent is within budget. Returns (allowed, reason)."""
with self._lock:
if self._org_spent_usd >= self._org_limit_usd:
return False, (
f"Org budget exhausted: ${self._org_spent_usd:.4f}"
f" / ${self._org_limit_usd:.4f}"
)
agent_spent = self._agent_spent.get(agent_name, 0.0)
if agent_spent >= self._agent_limit_usd:
return False, (
f"Agent '{agent_name}' budget exhausted:"
f" ${agent_spent:.4f} / ${self._agent_limit_usd:.4f}"
)
return True, ""

def record(self, agent_name: str, cost_usd: float) -> None:
"""Record cost for an agent."""
with self._lock:
self._org_spent_usd += cost_usd
self._agent_spent[agent_name] = (
self._agent_spent.get(agent_name, 0.0) + cost_usd
)

def utilization(self) -> float:
"""Current org-level budget utilization (0.0 to 1.0+)."""
with self._lock:
if self._org_limit_usd <= 0:
return 1.0
return self._org_spent_usd / self._org_limit_usd

def snapshot(self) -> BudgetSnapshot:
"""Return a read-only snapshot of current budget state."""
with self._lock:
return BudgetSnapshot(
org_spent_usd=self._org_spent_usd,
org_limit_usd=self._org_limit_usd,
agent_spent=dict(self._agent_spent),
agent_limit_usd=self._agent_limit_usd,
)
Loading