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
+69
View File
@@ -0,0 +1,69 @@
---
name: adk-agent-builder
description: Central hub for building, testing, and iterating on ADK agents. Trigger this skill when the user wants to create a new agent, configure modes (task, single-turn), or build graph-based workflows.
---
# ADK Agent Builder
This file serves as a directory of specialized reference guides for developing
agents with ADK. To avoid context pollution, read only the relevant reference
file based on your current task.
## Core Concepts Directory
Refer to these files for foundational knowledge:
- **Getting Started & Basic Agents**: [getting-started.md](references/getting-started.md)
- Environment setup, API key configuration, and minimal agent definitions.
- **Tool Catalog**: [tool-catalog.md](references/tool-catalog.md)
- How to bind function tools, MCP tools, OpenAPI specs, and Google API tools.
- **Agent Modes (Task / Single-Turn)**: [task-mode.md](references/task-mode.md)
- Multi-turn structured delegation and autonomous single-turn execution patterns.
- **Import Paths**: [import-paths.md](references/import-paths.md)
- Canonical and verbose import paths for core components, tools, and
events.
## Workflow & Graph Orchestration
Refer to these files when building complex graphs:
- **Function Nodes**: [function-nodes.md](references/function-nodes.md)
- How to use functions as nodes, type resolution, and generators.
- **Routing & Conditions**: [routing-and-conditions.md](references/routing-and-conditions.md)
- Edge patterns, dict-based routing, self-loops, and conditional execution.
- **LLM Agent Nodes**: [llm-agent-nodes.md](references/llm-agent-nodes.md)
- How to use LLM agents as workflow nodes, task wrappers, and handling output schemas.
- **Advanced Patterns**:
[advanced-patterns.md](references/advanced-patterns.md)
- Nested workflows, custom node types, and graph validation rules.
## Advanced Orchestration Patterns
- **Parallel Processing & Fan-Out**: [parallel-and-fanout.md](references/parallel-and-fanout.md)
- `ParallelWorker` for list splitting and concurrent processing, fan-out/join patterns.
- **Human-in-the-Loop**: [human-in-the-loop.md](references/human-in-the-loop.md)
- Pausing execution for user input, resumable workflows, and AuthConfig on nodes.
- **Dynamic Nodes**: [dynamic-nodes.md](references/dynamic-nodes.md)
- Scheduling nodes at runtime dynamically via `ctx.run_node()`.
## Infrastructure & Utilities
- **State & Events**: [state-and-events.md](references/state-and-events.md)
- Using context API, sharing global state, and yield event structures.
- **Session & Memory**:
[session-and-state.md](references/session-and-state.md)
- Session state mutation, scope conventions, and database session
services.
- **Callbacks & Plugins**:
[callbacks-and-plugins.md](references/callbacks-and-plugins.md)
- Implementing callbacks, plugin manager integration, and override
behavior.
- **Multi-Agent Systems**: [multi-agent.md](references/multi-agent.md)
- Hierarchical execution (e.g., `SequentialAgent`, `LoopAgent`, `ParallelAgent`).
- **Testing Strategies**: [testing.md](references/testing.md)
- Automated queries with `adk run`, unit tests, and integration testing with sample agents.
## Standards & Guidelines
- **Best Practices**: [best-practices.md](references/best-practices.md)
- Critical rules (Pydantic schemas, content events, state-based data flow).
@@ -0,0 +1,308 @@
# Advanced Workflow Patterns Reference
Nested workflows, dynamic nodes, retry configuration, custom node types, and graph construction.
## 📋 Agent Verification Checklist (Advanced Patterns)
Use this checklist when implementing complex workflows:
- [ ] **Validation**: Does your graph follow all 7 validation rules? (e.g., no unconditional cycles)
- [ ] **Custom Nodes**: If creating a custom node, did you override `get_name()` and `run()`?
- [ ] **Dynamic Execution**: If using `run_node`, did you follow the rules in the dedicated dynamic-nodes reference?
- [ ] **Waiting State**: Did you use `wait_for_output=True` if the node should stay in WAITING state until output is yielded?
## 💡 Quick Reference
- **Retry**: `RetryConfig(max_attempts=5, initial_delay=1.0)`
- **Custom Node Fields**: `rerun_on_resume`, `wait_for_output`, `retry_config`, `timeout`
## Nested Workflows
A `Workflow` is both an agent and a node. Use one workflow inside another:
```python
from google.adk.workflow import Workflow
# Inner workflow
inner = Workflow(
name="inner_pipeline",
edges=[
('START', step_a),
(step_a, step_b),
],
)
# Outer workflow using inner as a node
outer = Workflow(
name="outer_pipeline",
edges=[
('START', pre_process),
(pre_process, inner), # Nested workflow
(inner, post_process),
],
)
```
The inner workflow receives the predecessor's output as its START input and its terminal output flows to the next node in the outer workflow.
## Dynamic Node Scheduling
Schedule nodes at runtime using `ctx.run_node()`.
See the dedicated [Dynamic Node Scheduling Reference](dynamic-nodes.md) for
detailed rules, examples, and best practices.
## Retry Configuration
Configure automatic retry for nodes that may fail:
```python
from google.adk.workflow import RetryConfig
from google.adk.workflow import FunctionNode
retry = RetryConfig(
max_attempts=5, # Max attempts (default: 5). 0 or 1 = no retry
initial_delay=1.0, # Seconds before first retry (default: 1.0)
max_delay=60.0, # Max seconds between retries (default: 60.0)
backoff_factor=2.0, # Delay multiplier per attempt (default: 2.0)
jitter=1.0, # Randomness factor (default: 1.0, 0.0 = none)
exceptions=None, # Exception types to retry (None = all)
)
node = FunctionNode(
flaky_api_call,
name="api_call",
retry_config=retry,
)
```
### Retry delay formula
```
delay = initial_delay * (backoff_factor ^ attempt)
delay = min(delay, max_delay)
delay = delay * (1 + random(0, jitter))
```
### Accessing the attempt count
```python
def my_node(ctx: Context, node_input: str) -> str:
# attempt_count is 1 on the first try, ≥2 on retries
if ctx.attempt_count > 1:
print(f"Retry attempt {ctx.attempt_count}")
return "result"
```
## Custom Node Types
Subclass `BaseNode` for custom behavior:
```python
from google.adk.workflow import BaseNode
from google.adk.events.event import Event
from google.adk.agents.context import Context
from pydantic import ConfigDict, Field
from typing import Any, AsyncGenerator
from typing_extensions import override
class BatchProcessorNode(BaseNode):
"""Processes items in batches."""
model_config = ConfigDict(arbitrary_types_allowed=True)
name: str = Field(default="batch_processor")
batch_size: int = Field(default=10)
def __init__(self, *, name: str = "batch_processor", batch_size: int = 10):
super().__init__()
object.__setattr__(self, 'name', name)
object.__setattr__(self, 'batch_size', batch_size)
@override
def get_name(self) -> str:
return self.name
@override
async def run(
self,
*,
ctx: Context,
node_input: Any,
) -> AsyncGenerator[Any, None]:
items = node_input if isinstance(node_input, list) else [node_input]
results = []
for i in range(0, len(items), self.batch_size):
batch = items[i:i + self.batch_size]
batch_result = await process_batch(batch)
results.extend(batch_result)
yield Event(output=results)
```
### BaseNode Fields
| Field | Default | Description |
|-------|---------|-------------|
| `rerun_on_resume` | `False` | Whether to rerun after HITL interrupt |
| `wait_for_output` | `False` | Node stays in WAITING state until it yields output (see below) |
| `retry_config` | `None` | Retry configuration on failure |
| `timeout` | `None` | Max seconds for node to complete |
### wait_for_output
When `wait_for_output=True`, a node that finishes without yielding an `Event` with output moves to **WAITING** state instead of COMPLETED. Downstream nodes are **not** triggered. The node can then be re-triggered by upstream predecessors.
This is how `JoinNode` works internally — it runs once per predecessor, storing partial inputs, and only yields output (triggering downstream) when all predecessors have completed. `LlmAgentWrapper` in `task` mode also sets `wait_for_output=True` automatically.
```python
from google.adk.workflow import BaseNode
class CollectorNode(BaseNode):
wait_for_output: bool = True # Stay in WAITING until output is yielded
async def run(self, *, ctx, node_input):
# Store partial input, don't yield output yet
collected = ctx.state.get("collected", [])
collected.append(node_input)
yield Event(state={"collected": collected})
# Only yield output when we have enough
if len(collected) >= 3:
yield Event(output=collected)
# Now node transitions to COMPLETED and triggers downstream
```
Nodes with `wait_for_output=True` default:
- `JoinNode`: `True` (waits for all predecessors)
- `LlmAgentWrapper` (task mode): `True` (set in `model_post_init`)
- All other nodes: `False`
### Required Methods
| Method | Description |
|--------|-------------|
| `get_name() -> str` | Return the node name |
| `run(*, ctx, node_input) -> AsyncGenerator` | Execute the node, yield events |
## ToolNode
Wrap an ADK tool as a workflow node:
```python
from google.adk.workflow._tool_node import _ToolNode as ToolNode
from google.adk.tools.function_tool import FunctionTool
def search(query: str) -> str:
"""Search for information."""
return f"Results for: {query}"
tool = FunctionTool(search)
tool_node = ToolNode(tool, name="search_node")
agent = Workflow(
name="with_tool",
edges=[
('START', prepare_query),
(prepare_query, tool_node), # Input must be dict (tool args) or None
(tool_node, process_results),
],
)
```
**Important**: ToolNode input must be a dictionary of tool arguments or None.
## AgentNode
Wrap any `BaseAgent` (not just LlmAgent) as a workflow node:
```python
from google.adk.workflow._agent_node import AgentNode
from google.adk.agents.loop_agent import LoopAgent
loop = LoopAgent(
name="refine_loop",
sub_agents=[writer, reviewer],
max_iterations=3,
)
loop_node = AgentNode(agent=loop, name="refinement")
agent = Workflow(
name="with_loop",
edges=[
('START', loop_node),
(loop_node, final_step),
],
)
```
## Graph Validation Rules
The workflow graph is validated on construction. These rules are enforced:
1. START node must exist
2. START node must not have incoming edges
3. All non-START nodes must be reachable (appear as `to_node` in some edge)
4. No duplicate node names
5. No duplicate edges
6. At most one `__DEFAULT__` route per node
7. No unconditional cycles (cycles must have at least one routed edge)
## Edge Construction Patterns
```python
from google.adk.workflow import Edge
from google.adk.workflow._workflow_graph import WorkflowGraph
# Tuple syntax (most common)
edges = [
('START', node_a), # Simple edge
(node_a, node_b, "route"), # Routed edge
(node_a, (node_b, node_c)), # Fan-out
((node_b, node_c), join_node), # Fan-in
]
# Sequence shorthand (tuple with 3+ elements creates chain)
edges = [('START', node_a, node_b, node_c)]
# Equivalent to: [('START', node_a), (node_a, node_b), (node_b, node_c)]
# Routing map (dict syntax)
edges = [
(classifier, {"success": handler_a, "error": handler_b}),
]
# Edge objects (explicit)
edges = [
Edge(START, node_a),
Edge(node_a, node_b, route="success"),
]
# Edge.chain helper
edges = Edge.chain('START', node_a, node_b, node_c)
# Returns: [(START, node_a), (node_a, node_b), (node_b, node_c)]
# WorkflowGraph.from_edge_items
graph = WorkflowGraph.from_edge_items([
('START', node_a),
(node_a, node_b),
])
agent = Workflow(name="my_workflow", graph=graph)
```
## Source File Locations
| Component | File |
|-----------|------|
| Workflow | `src/google/adk/workflow/_workflow.py` |
| WorkflowGraph, Edge | `src/google/adk/workflow/_workflow_graph.py` |
| Context | `src/google/adk/agents/context.py` |
| FunctionNode | `src/google/adk/workflow/_function_node.py` |
| _LlmAgentWrapper | `src/google/adk/workflow/_llm_agent_wrapper.py` |
| AgentNode | `src/google/adk/workflow/_agent_node.py` |
| _ToolNode | `src/google/adk/workflow/_tool_node.py` |
| JoinNode | `src/google/adk/workflow/_join_node.py` |
| ParallelWorker | `src/google/adk/workflow/_parallel_worker.py` |
| BaseNode, START | `src/google/adk/workflow/_base_node.py` |
| @node decorator | `src/google/adk/workflow/_node.py` |
| RetryConfig | `src/google/adk/workflow/_retry_config.py` |
| Event | `src/google/adk/events/event.py` |
| RequestInput | `src/google/adk/events/request_input.py` |
@@ -0,0 +1,179 @@
# ADK Workflow Best Practices
This document outlines the critical best practices and rules for developing reliable and maintainable workflows with the ADK.
## 📋 Agent Code Verification Checklist
Use this checklist to verify your code before submitting or finalizing changes:
- [ ] **Schemas**: Are Pydantic `BaseModel` classes used for all inputs/outputs? (No raw dicts)
- [ ] **UI Output**: Do user-visible messages use `Event(message=...)`? (Not `output=`)
- [ ] **State Data Flow**: Is data stored in state and read via `{var}` or param names?
- [ ] **State Updates**: Are state updates done via `Event(state=...)`? (Avoid direct `ctx.state` mutation)
- [ ] **Outputs**: Does each node execution yield at most **one** `event.output`?
- [ ] **Semantics**: Are `yield` and `return` never mixed in the same function?
- [ ] **Instructions**: Are `{node_input}` templates NOT used in agent instructions?
- [ ] **HITL**: Are `interrupt_id`s unique per iteration in loops?
## Best Practices (MUST FOLLOW)
### Use Pydantic Models, Not Raw Dicts
**Always define Pydantic `BaseModel` classes** for function node inputs, outputs, LLM `output_schema`, and structured data. Never use `dict[str, Any]` when the shape is known:
```python
# ❌ WRONG: raw dicts
def lookup_flights(node_input: dict[str, Any]) -> dict[str, Any]:
return {"flight_cost": 500, "details": "Economy"}
# ✅ CORRECT: typed schemas
class FlightInfo(BaseModel):
flight_cost: int
details: str
def lookup_flights(node_input: Itinerary) -> FlightInfo:
return FlightInfo(flight_cost=500, details="Economy")
```
This applies to ALL data flowing through the graph: node inputs, node outputs, JoinNode results, LLM output schemas, and HITL response schemas.
### Emit Content Events for Web UI Display
`event.output` is internal — only `event.content` renders in the ADK web UI. For user-visible output, use `Event(message=...)`:
```python
def final_output(node_input: str):
yield Event(message=node_input) # message= renders in web UI
yield Event(output=node_input) # output= passes data to downstream nodes
# State-only event (no output, no message — just side-effect state update)
def store_data(node_input: str):
yield Event(state={"user_input": node_input})
> [!TIP]
> Function nodes can stream user-visible messages by yielding `Event(message="chunk", partial=True)`.
```
LLM agents emit content events automatically. Add them explicitly for function nodes that produce user-facing results.
### Prefer State-Based Data Flow with LLM Agents
Store data in state via `Event(state={...})` or `output_key`, then read it via instruction templates `{var}` or function parameter name injection. This is more robust than passing data through `node_input`, especially for routing workflows where multiple branches need the same data.
```python
# ✅ State-based: store early, read anywhere via {var} or param name
def process_input(node_input: str):
yield Event(state={"topic": node_input})
writer = Agent(name="writer", instruction='Write about "{topic}".', output_key="draft")
def send(draft: str): # draft resolved from ctx.state["draft"]
yield Event(message=draft)
# ❌ Fragile: threading data through node_input breaks at routing/loops
```
### Set State via Event, Not ctx.state
**Prefer `Event(state=...)` over `ctx.state[key] = ...`** for writing state. Event-based state is persisted in event history and replayable during non-resumable HITL. Direct `ctx.state` mutations are side effects that may be lost on replay.
```python
# ✅ Preferred
def save(node_input: str):
return Event(output=node_input, state={"user_request": node_input})
# ❌ Avoid
def save(ctx: Context, node_input: str) -> str:
ctx.state["user_request"] = node_input
return node_input
```
### One Output Event Per Node
Each node execution can yield many events, but **at most one should have `event.output`**. This applies to function nodes, LLM agents (including `task` and `single_turn` mode), and nested workflows. Multiple output events get silently merged into a list, which changes the downstream `node_input` type and usually causes errors. Similarly, at most one event can have `route` — multiple routed events raise `ValueError`.
```python
# ✅ Correct: one output event, other events for messages/state
def my_node(node_input: str):
yield Event(message="Processing...") # display only
yield Event(state={"status": "done"}) # state update only
yield Event(output="final result") # the single output
# ❌ Wrong: multiple output events
def my_node(node_input: str):
yield Event(output="first") # these get merged into ["first", "second"]
yield Event(output="second") # downstream expects str, gets list → TypeError
```
### Don't Mix yield and return Event
A function is either a **generator** (uses `yield`) or a **regular function** (uses `return`). Never mix them — in Python, a function with `yield` becomes a generator and any `return value` is silently ignored:
```python
# ✅ Generator: use yield for all events
def my_node(node_input: str):
yield Event(state={"key": "value"})
yield Event(output="result")
# ✅ Regular function: use return for a single value/event
def my_node(node_input: str):
return Event(output="result", state={"key": "value"})
# ✅ Regular function: return plain value (auto-wrapped in Event)
def my_node(node_input: str) -> str:
return "result"
# ❌ Wrong: mixing yield and return — the return is silently ignored
def my_node(node_input: str):
yield Event(state={"key": "value"})
return Event(output="result") # IGNORED — Python generator semantics
```
Use generators (`yield`) when you need multiple events (state + output + message). Use regular functions (`return`) for simple single-value output.
### Never Put node_input in LLM Agent Instructions
`{var}` templates in `instruction` resolve **only** from `ctx.state`. `node_input` is NOT available as a template variable — it is automatically sent as the user message to the LLM. Do not try to reference it in the instruction:
```python
# ❌ Wrong: {node_input} is not in state, raises KeyError
agent = Agent(
name="summarizer",
instruction="Summarize this: {node_input}",
)
# ✅ Correct: node_input already becomes the user message, just instruct
agent = Agent(
name="summarizer",
instruction="Summarize the following text in one sentence.",
)
# ✅ Correct: use state for data that needs to be in the instruction
agent = Agent(
name="writer",
instruction='Write about "{topic}". Previous feedback: {feedback?}',
output_key="draft",
)
```
### Workflow Cannot Be a Sub-Agent of LlmAgent
`Workflow`, `SequentialAgent`, `LoopAgent`, and `ParallelAgent` cannot be added as `sub_agents` of an `LlmAgent`. Agent transfer to workflow agents is not supported.
### Workflow Data Rules
- **`Event.output` must be JSON-serializable.** FunctionNode auto-converts BaseModel returns via `model_dump()`. Never store `types.Content` or other non-serializable objects in `Event.output`.
- **`output_key` stores dicts, not BaseModel instances.** LLM agents with `output_schema` run `validate_schema()``model_dump()`, so `ctx.state[output_key]` is a plain dict.
- **`ctx.state.get(key)` returns a dict.** Use dict access (`data["field"]`) or reconstruct (`MyModel(**data)`) for typed access.
## Human-in-the-Loop (HITL) Rules
### Unique interrupt_id in Loops
When a node requests input (yields `RequestInput`) inside a loop (e.g., a review-revise loop), you **MUST use a unique `interrupt_id` per iteration** (e.g., `review_{count}`).
If you reuse the same `interrupt_id`, the event-based state reconstruction will confuse responses from earlier iterations with the current one, leading to infinite restart loops!
```python
# ✅ Correct: unique ID per iteration
review_count = ctx.state.get('review_count', 0)
interrupt_id = f'review_{review_count}'
yield RequestInput(interrupt_id=interrupt_id, message="Approve?")
```
@@ -0,0 +1,98 @@
# Callbacks and Plugins
## 📋 Agent Verification Checklist (Callbacks)
Use this checklist when implementing callbacks or plugins:
- [ ] **Override Behavior**: Remember that returning a non-`None` value in a callback *overrides* the default behavior (e.g., skips model call or tool execution). Is that intentional?
- [ ] **Context Type**: Remember that `CallbackContext` is an alias for `Context`.
## 💡 Quick Reference (Callback Returns)
- **Continue Normal Flow**: Return `None`.
- **Override Model**: Return `LlmResponse` in `before_model`.
- **Override Tool**: Return `dict` in `before_tool`.
## Agent Callbacks
```python
root_agent = Agent(
before_agent_callback=my_before_cb, # Before agent runs
after_agent_callback=my_after_cb, # After agent runs
before_model_callback=my_before_model, # Before LLM call
after_model_callback=my_after_model, # After LLM call
before_tool_callback=my_before_tool, # Before tool call
after_tool_callback=my_after_tool, # After tool call
on_model_error_callback=my_error_cb, # On LLM error
on_tool_error_callback=my_tool_error_cb, # On tool error
...
)
```
**Note:** `CallbackContext` is a backward-compatible alias for `Context`. Both work identically.
## Callback Signatures
```python
# before_agent / after_agent
def callback(callback_context: CallbackContext):
return None # Continue normal flow
# OR return ModelContent to override
# before_model
def callback(callback_context, llm_request: LlmRequest):
return None # Continue to LLM
# OR return LlmResponse to skip LLM
# after_model
def callback(callback_context, llm_response):
return None # Use actual response
# OR return LlmResponse to override
# before_tool
def callback(tool, args, tool_context):
return None # Call tool normally
# OR return dict to skip tool
# after_tool
def callback(tool, args, tool_context, tool_response):
return None # Use actual response
# OR return dict to override
```
**Multiple callbacks:** Pass a list. They execute in order until one
returns non-None.
## Plugins (App-Level Callbacks)
```python
from google.adk.plugins.base_plugin import BasePlugin
class MyPlugin(BasePlugin):
def __init__(self):
super().__init__(name='my_plugin')
async def before_agent_callback(self, *, agent, callback_context):
pass
async def before_model_callback(self, *, callback_context, llm_request):
pass
```
## Built-in Plugins
| Plugin | Import | Purpose |
|--------|--------|---------|
| `ContextFilterPlugin` | `from google.adk.plugins.context_filter_plugin import ContextFilterPlugin` | Limit history in context |
| `SaveFilesAsArtifactsPlugin` | `from google.adk.plugins.save_files_as_artifacts_plugin import SaveFilesAsArtifactsPlugin` | Auto-save file outputs |
| `GlobalInstructionPlugin` | `from google.adk.plugins.global_instruction_plugin import GlobalInstructionPlugin` | Inject global instructions |
Usage with App:
```python
from google.adk.apps import App
from google.adk.plugins.context_filter_plugin import ContextFilterPlugin
app = App(
name='my_app',
root_agent=root_agent,
plugins=[ContextFilterPlugin(num_invocations_to_keep=3)],
)
```
@@ -0,0 +1,95 @@
# Dynamic Node Scheduling Reference
Schedule nodes at runtime using `ctx.run_node()`. This allows a node within a workflow to trigger the run of another node (or a callable that can be built into a node) and asynchronously wait for its result.
## 📋 Agent Verification Checklist (Dynamic Nodes)
Use this checklist when scheduling nodes dynamically:
- [ ] **Rerun on Resume**: Does the parent node calling `run_node` have `rerun_on_resume=True`?
- [ ] **Run ID**: If using an explicit `run_id`, does it contain non-numeric characters?
- [ ] **Param Name**: If passing input directly to a raw function via `node_input=...`, is that function's parameter named `node_input`?
- [ ] **Nesting**: If the child node *also* calls `run_node`, is it wrapped in `FunctionNode(..., rerun_on_resume=True)`?
## 💡 Quick Reference
- **Call**: `await ctx.run_node(node_like, node_input=...)`
- **Output Delegation**: Set `use_as_output=True` to make child output be the parent's output.
## Basic Usage
```python
from google.adk import Agent, Context, Event, Workflow
from google.adk.workflow import node
from pydantic import BaseModel
class Feedback(BaseModel):
grade: str
generate_headline = Agent(
name="generate_headline",
instruction='Write a headline about the topic "{topic}".',
)
evaluate_headline = Agent(
name="evaluate_headline",
instruction="Grade whether the headline is tech-related.",
output_schema=Feedback,
mode="single_turn",
)
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
yield Event(state={"topic": node_input})
while True:
headline = await ctx.run_node(generate_headline)
feedback = Feedback.model_validate(
await ctx.run_node(evaluate_headline, node_input=headline)
)
if feedback.grade == "tech-related":
yield headline
break
root_agent = Workflow(
name="root_agent",
edges=[("START", orchestrate)],
)
```
## Requirements & Rules
- **`rerun_on_resume=True`**: The parent node calling `ctx.run_node()` must have `rerun_on_resume=True`. This is required because dynamically scheduled nodes might be interrupted (e.g., for HITL), and the workflow needs to wake up and re-run the parent node to get the child node's response.
- **Unique Instance Names**: Each dynamic instance needs a unique name (auto-generated for Agent nodes).
- **Node-Like Acceptable**: `ctx.run_node()` accepts any node-like object (function, Agent, BaseNode).
- **Explicit `run_id` Constraint**: If you provide an explicit `run_id`, it **must contain non-numeric characters** (e.g., `"run_a"` instead of `"1"`) to prevent collision with auto-generated numeric IDs.
- **`use_as_output=True`**: Suppresses the parent node's own output and uses the child's output as the parent's output. This is achieved via `outputFor` annotation in events. This can only be called ONCE per parent node execution.
- **`use_sub_branch`**: (Optional) If set to `True`, attaches a branch segment (`node_name@run_id`) to the current execution branch to ensure event isolation for parallel or sub-agent runs.
## Best Practices
- Always `await` `ctx.run_node()` directly. Wrapping it in `asyncio.create_task()` means the task runs unsupervised — errors are silently swallowed and the task is not cancelled if the parent node is interrupted.
## Imperative Workflow Construction
As an alternative to defining static graph edges, you can use dynamic nodes to construct workflows in an imperative style using standard Python control flow. This approach can sometimes be more intuitive for complex conditional logic or parallel execution.
### Replacing Graph Patterns
#### 1. Sequences & Branching
Instead of defining edges with routes, use standard Python `if/else`:
```python
async def orchestrator(ctx: Context, node_input: str):
res_a = await ctx.run_node(step_a, node_input=node_input)
if "success" in res_a:
return await ctx.run_node(step_b, node_input=res_a)
else:
return await ctx.run_node(step_c, node_input=res_a)
```
### Important Pits & Best Practices
- **Function Parameter Mapping**: When passing a raw function to `run_node`, ADK defaults to `'state'` binding mode. If you want to pass input directly via `node_input=...` in `run_node`, **the function parameter MUST be named `node_input`**!
```python
def my_worker(node_input: str): # MUST be named 'node_input'
return f"Done: {node_input}"
```
- **Nested Dynamic Nodes**: If a dynamically scheduled node *itself* calls `run_node`, it acts as a parent node and **MUST have `rerun_on_resume=True`**! Since raw functions passed to `run_node` default to `False`, you must manually wrap the inner parent function in `FunctionNode(..., rerun_on_resume=True)`!
- **Generator Returns**: In nodes that use `yield` (generators), you cannot use `return value` to produce the final output (Python syntax error in async generators). You must yield `Event(output=...)` instead.
@@ -0,0 +1,320 @@
# Function Nodes Reference
Function nodes are the most common node type. Any Python function becomes a workflow node.
## 📋 Agent Verification Checklist (Function Nodes)
Use this checklist to verify your Function Node configuration:
- [ ] **Input Type**: If following an LLM agent without schema, is `node_input` typed as `Any` or `types.Content`? (Not `str`)
- [ ] **UI Output**: Do you yield `Event(message=...)` for results that should appear in the Web UI?
- [ ] **Outputs**: Does the function yield or return at most **one** `event.output`?
- [ ] **Union Types**: If using Union types for `node_input`, did you add `isinstance` checks in the body for actual validation?
## 💡 Quick Reference (Param Resolution)
- **`ctx`**: Workflow `Context` object.
- **`node_input`**: Output from the predecessor node.
- **Any other name**: Auto-resolved from `ctx.state[param_name]`.
## Imports
```python
from google.adk.workflow import FunctionNode
from google.adk.events.event import Event
from google.adk.agents.context import Context
from google.adk.workflow import node # @node decorator
```
## Basic Functions
A function returning a value automatically wraps it in an `Event`:
```python
def process(node_input: str) -> str:
return f"Processed: {node_input}"
# Async functions work too
async def fetch_data(node_input: str) -> dict:
result = await some_api_call(node_input)
return {"data": result}
```
## Function Signatures
FunctionNode inspects the function signature to resolve parameters:
| Parameter Name | Source |
|---------------|--------|
| `ctx` | Workflow `Context` object |
| `node_input` | Output from predecessor node |
| Any other name | Looked up from `ctx.state[param_name]` |
```python
# Receives both context and input
def my_node(ctx: Context, node_input: str) -> str:
session_id = ctx.session.id
return f"Session {session_id}: {node_input}"
# Receives only input
def simple(node_input: str) -> str:
return node_input.upper()
# Reads from state (other params resolved from ctx.state)
def uses_state(node_input: str, user_name: str) -> str:
# user_name read from ctx.state['user_name']
return f"{user_name}: {node_input}"
# No parameters at all
def constant() -> str:
return "hello"
```
## Generator Functions
Yield multiple events from a single node:
```python
# Async generator
async def multi_output(ctx: Context) -> AsyncGenerator[Any, None]:
yield Event(output="first output")
yield Event(output="second output")
# Sync generator
def sync_multi(node_input: str):
yield Event(output="step 1")
yield Event(output="step 2")
```
**At most one event should have `output`.** Multiple output events get silently merged into a list, changing the downstream type. Similarly, at most one event can have `route` (multiple raise `ValueError`). Use separate events for messages, state updates, and the single output.
## Yielding Raw Values
Yield raw values instead of Event objects. They are wrapped automatically:
```python
async def raw_yield(node_input: str):
yield "output value" # Wrapped in Event(output="output value")
```
## Returning None
If a function returns `None`, no event is emitted and no downstream node is triggered:
```python
def maybe_output(node_input: str) -> str | None:
if not node_input:
return None # No downstream trigger
return f"Got: {node_input}"
```
## Auto Type Conversion
FunctionNode automatically converts `dict` inputs to Pydantic models based on type hints:
```python
from pydantic import BaseModel
class Order(BaseModel):
item: str
quantity: int
def process_order(node_input: Order) -> str:
# If node_input is {'item': 'widget', 'quantity': 3},
# it's auto-converted to Order(item='widget', quantity=3)
return f"Order: {node_input.quantity}x {node_input.item}"
```
This works recursively for `list[Model]` and `dict[str, Model]` too.
### Pydantic Schemas with LLM Agents (Recommended Pattern)
Use `output_schema` on LLM agents to get structured, JSON-serializable output. This avoids `types.Content` serialization issues and enables auto-conversion in downstream function nodes:
```python
from pydantic import BaseModel
from google.adk.agents.llm_agent import LlmAgent
class ReviewResult(BaseModel):
score: int
feedback: str
approved: bool
reviewer = LlmAgent(
name="reviewer",
model="gemini-2.5-flash",
instruction="Review the code and provide structured feedback.",
output_schema=ReviewResult,
)
# Downstream function node receives dict, auto-converted to Pydantic model
def process_review(node_input: ReviewResult) -> str:
if node_input.approved:
return f"Approved with score {node_input.score}"
return f"Rejected: {node_input.feedback}"
```
**Why use `output_schema`:**
- LLM agent output becomes a `dict` (JSON-serializable) instead of `types.Content`
- Fixes `TypeError` when SQLite session service serializes JoinNode state
- Enables auto type conversion in downstream function nodes
- Provides structured data for programmatic access
## Explicit FunctionNode
For more control, create a `FunctionNode` explicitly:
```python
from google.adk.workflow import FunctionNode
from google.adk.workflow import RetryConfig
node = FunctionNode(
my_func,
name="custom_name", # Override inferred name
rerun_on_resume=True, # Rerun after HITL interrupt
retry_config=RetryConfig( # Retry on failure
max_attempts=3,
initial_delay=1.0,
),
)
```
## @node Decorator
The `@node` decorator provides syntactic sugar:
```python
from google.adk.workflow import node
@node
def my_func(node_input: str) -> str:
return node_input
@node(name="custom_name", rerun_on_resume=True)
async def my_async_func(node_input: str) -> str:
return node_input
# As a function call
my_node = node(some_func, name="renamed")
# Wrap as ParallelWorker
parallel = node(some_func, parallel_worker=True)
```
## Prefer Typed Schemas Over Raw Dicts
Use Pydantic models for node inputs, outputs, and state instead of raw `dict`. This gives you validation, IDE autocomplete, and self-documenting code:
```python
# ❌ Avoid: raw dicts are error-prone and opaque
def process(node_input: dict) -> dict:
return {"status": "done", "count": node_input["items"]}
# ✅ Prefer: typed schemas
class TaskInput(BaseModel):
items: list[str]
priority: str = "normal"
class TaskResult(BaseModel):
status: str
count: int
def process(node_input: TaskInput) -> TaskResult:
return TaskResult(status="done", count=len(node_input.items))
```
This applies to:
- **Function node inputs/outputs**: Use Pydantic models as `node_input` type hints and return types
- **LLM agent `output_schema`**: Always set `output_schema=MyModel` to get structured dict output instead of `types.Content`
- **`RequestInput.response_schema`**: Pass a Pydantic `BaseModel` class directly (e.g., `response_schema=MyModel`)
- **State values**: Store Pydantic model dicts (via `.model_dump()`) rather than hand-built dicts
FunctionNode auto-converts `dict` inputs to Pydantic models based on type hints (see [Auto Type Conversion](#auto-type-conversion) above), so typed schemas work seamlessly across the graph.
## Emitting Content Events for Web UI Display
In the ADK web UI, only `event.content` is rendered to the user — `event.output` is internal and not displayed. When a function node produces user-facing output, yield a content event in addition to the output event:
```python
from google.genai import types
from google.adk.events.event import Event
async def summarize(ctx: Context, node_input: str):
result = f"Summary: {node_input}"
# Content event: rendered in the web UI
yield Event(content=types.ModelContent(result))
# Output event: passed to downstream nodes
yield Event(output=result)
```
LLM agents emit content events automatically. For function nodes that are terminal (no downstream edges) or produce user-visible intermediate results, add the content event so users see output in the web UI.
## Events with Routes
Return an `Event` with a `route` for conditional branching:
```python
def classify(node_input: str):
if "urgent" in node_input:
return Event(output=node_input, route="urgent")
return Event(output=node_input, route="normal")
```
## Events with State Updates
Update shared workflow state via the `state` constructor parameter:
```python
def update_counter(node_input: str):
return Event(
output=node_input,
state={"counter": 1, "last_input": node_input},
)
```
Or use `ctx.state` directly:
```python
def update_via_context(ctx: Context, node_input: str) -> str:
ctx.state["counter"] = ctx.state.get("counter", 0) + 1
return node_input
```
## Type Validation (Important)
FunctionNode strictly type-checks `node_input` against the type hint. A `TypeError` is raised if the actual type doesn't match.
**Union types:** `node_input: list | dict` silently skips validation (FunctionNode detects Union via `get_origin()` and sets `is_instance = True`). This means Union hints won't crash, but they also won't catch wrong types — any value passes. Use `isinstance` checks inside the function body for actual validation.
**Common pitfall: LLM agent -> function node.** LlmAgentWrapper outputs `types.Content` (not `str`). If your function node follows an LLM agent and declares `node_input: str`, it will fail with:
```
TypeError: Parameter "node_input" expects type <class 'str'>
but received type <class 'google.genai.types.Content'>
```
**Fix:** Use `Any` for `node_input` and extract text manually:
```python
from typing import Any
from google.genai import types
def process(node_input: Any) -> str:
# Handle types.Content from LLM agents
if isinstance(node_input, types.Content):
return ''.join(p.text for p in (node_input.parts or []) if p.text)
return str(node_input) if node_input is not None else ''
```
**Output type summary by predecessor:**
| Predecessor Node Type | `node_input` Type |
|----------------------|-------------------|
| Function returning `str` | `str` |
| Function returning `dict` | `dict` |
| Function returning `Event(output=X)` | type of `X` |
| `LlmAgentWrapper` (no `output_schema`) | `types.Content` |
| `LlmAgentWrapper` (with `output_schema`) | `dict` |
| `JoinNode` | `dict[str, Any]` (keyed by predecessor names) |
| `ParallelWorker` | `list` |
| `START` (no `input_schema`) | `types.Content` (user's message) |
| `START` (with `input_schema`) | parsed schema type |
@@ -0,0 +1,434 @@
# Getting Started: Creating ADK Agents
Step-by-step guide covering environment setup, basic LLM agents, and workflow agents.
## 📋 New Agent Checklist
Use this checklist when creating a new agent to ensure it follows convention:
- [ ] **Directory**: Is there a directory for the agent?
- [ ] **__init__.py**: Does it contain `from . import agent`?
- [ ] **agent.py**: Does it define `root_agent` or `app`?
- [ ] **.env**: Is there a `.env` file with the appropriate API keys? (Do not commit to git)
## 💡 Quick Reference (CLI Commands)
- **Create**: `adk create <agent_name>` (Scaffolds a new agent project)
- **Web UI**: `adk web <path_to_agent_dir>` (Starts dev server at localhost:8000)
- **Run CLI**: `adk run <path_to_agent_dir>` (Interactive or query mode)
## 1. Set Up the Environment
Create a virtual environment and install the ADK:
```bash
# Create and activate virtual environment
python -m venv .venv
source .venv/bin/activate # macOS/Linux
# Install the ADK package
pip install google-adk
```
Or with `uv`:
```bash
uv venv --python "python3.11" ".venv"
source .venv/bin/activate
uv pip install google-adk
```
## 2. Configure API Keys
### Google AI Studio (recommended for getting started)
Obtain an API key from [Google AI Studio](https://aistudio.google.com/app/apikey).
Create a `.env` file in the agent directory:
```
GOOGLE_GENAI_USE_ENTERPRISE=FALSE
GOOGLE_API_KEY=YOUR_API_KEY
```
### Vertex AI
For production use with Google Cloud:
```
GOOGLE_GENAI_USE_ENTERPRISE=TRUE
GOOGLE_CLOUD_PROJECT=your-project-id
GOOGLE_CLOUD_LOCATION=us-central1
```
Run `gcloud auth application-default login` to authenticate.
### Vertex AI Express Mode
Combines Vertex AI with API key authentication:
```
GOOGLE_GENAI_USE_ENTERPRISE=TRUE
GOOGLE_API_KEY=YOUR_EXPRESS_MODE_KEY
```
## 3. Agent Directory Structure
The ADK CLI discovers agents by directory convention. Each agent directory must have:
```
my_agent/
├── __init__.py # Must import the agent module
├── agent.py # Must define root_agent
└── .env # API keys (not committed to git)
```
### __init__.py
```python
from . import agent
```
Or generate the project with the CLI:
```bash
adk create my_agent
```
## 4. Basic LLM Agent with Tools
Before building workflow agents, understand the basic LLM agent pattern. An `LlmAgent` (also aliased as `Agent`) connects an LLM to tools and instructions:
### agent.py
```python
from google.adk.agents.llm_agent import Agent
def get_weather(city: str) -> dict:
"""Returns the current weather for a specified city."""
# In production, call a real weather API
return {
"status": "success",
"city": city,
"weather": "sunny",
"temperature": "72F",
}
def get_current_time(city: str) -> dict:
"""Returns the current time in a specified city."""
import datetime
return {
"status": "success",
"city": city,
"time": datetime.datetime.now().strftime("%I:%M %p"),
}
root_agent = Agent(
model="gemini-2.5-flash",
name="root_agent",
description="An assistant that provides weather and time information.",
instruction="""You are a helpful assistant.
Use the get_weather tool to look up weather and
get_current_time to check the time in any city.
Always be friendly and concise.""",
tools=[get_weather, get_current_time],
)
```
### Key concepts
- **`model`**: The LLM to use (e.g., `"gemini-2.5-flash"`, `"gemini-2.5-pro"`)
- **`instruction`**: System prompt guiding the agent's behavior
- **`tools`**: Python functions the LLM can call. The function name, docstring, and type hints are sent to the LLM as the tool schema
- **`description`**: Used when this agent is a sub-agent (for transfer routing)
- **`output_key`**: Store the agent's final text output in session state under this key
### Tool function conventions
- Use clear function names and docstrings — the LLM sees these
- Type-hint all parameters — they define the tool's input schema
- Return a `dict` or `str` — the return value becomes the tool response
## 5. Run the Agent
### Web UI (primary debugging tool)
```bash
adk web my_agent/
```
Open `http://localhost:8000`. Select the agent from the dropdown, type a message, and see events in the Events tab.
**Note**: `adk web` is for development only, not production.
### CLI mode
```bash
adk run my_agent/
```
### API server
```bash
adk api_server my_agent/
```
### Programmatic execution
```python
import asyncio
from google.adk.runners import InMemoryRunner
from google.genai import types
async def main():
from my_agent import agent
runner = InMemoryRunner(
app_name="my_app",
agent=agent.root_agent,
)
session = await runner.session_service.create_session(
app_name="my_app", user_id="user1"
)
content = types.Content(
role="user", parts=[types.Part.from_text(text="What's the weather in Paris?")]
)
async for event in runner.run_async(
user_id="user1",
session_id=session.id,
new_message=content,
):
if event.content and event.content.parts:
if event.content.parts[0].text:
print(f"{event.author}: {event.content.parts[0].text}")
asyncio.run(main())
```
## 6. From LLM Agent to Workflow Agent
A `Workflow` extends the basic agent pattern with graph-based execution. Instead of a single LLM deciding what to do, define explicit nodes and edges:
### agent.py — Minimal Workflow
```python
from google.adk.workflow import Workflow
def greet(node_input: str) -> str:
return f"Hello! You said: {node_input}"
root_agent = Workflow(
name="my_workflow",
edges=[
('START', greet),
],
)
```
## 5. Sample: Sequential Pipeline with LLM Agents
A code write-review-refactor pipeline using `SequentialAgent`:
### agent.py
```python
from google.adk.agents.llm_agent import LlmAgent
from google.adk.agents.sequential_agent import SequentialAgent
code_writer_agent = LlmAgent(
name="CodeWriterAgent",
model="gemini-2.5-flash",
instruction="""You are a Python Code Generator.
Based *only* on the user's request, write Python code that fulfills the requirement.
Output *only* the complete Python code block.
""",
description="Writes initial Python code based on a specification.",
output_key="generated_code",
)
code_reviewer_agent = LlmAgent(
name="CodeReviewerAgent",
model="gemini-2.5-flash",
instruction="""You are an expert Python Code Reviewer.
Review the following code:
```python
{generated_code}
```
Provide feedback as a concise, bulleted list.
If the code is excellent, state: "No major issues found."
""",
description="Reviews code and provides feedback.",
output_key="review_comments",
)
code_refactorer_agent = LlmAgent(
name="CodeRefactorerAgent",
model="gemini-2.5-flash",
instruction="""You are a Python Code Refactoring AI.
Improve the code based on the review comments.
**Original Code:**
```python
{generated_code}
```
**Review Comments:**
{review_comments}
If no issues found, return the original code unchanged.
Output *only* the final Python code block.
""",
description="Refactors code based on review comments.",
output_key="refactored_code",
)
root_agent = SequentialAgent(
name="CodePipelineAgent",
sub_agents=[code_writer_agent, code_reviewer_agent, code_refactorer_agent],
description="Executes a sequence of code writing, reviewing, and refactoring.",
)
```
### Key patterns in this sample
- **`output_key`**: Each agent stores its output in session state, making it available to later agents
- **`{generated_code}`**: Instruction placeholders are resolved from session state at runtime
- **`SequentialAgent`**: Convenience wrapper that auto-generates `START -> agent1 -> agent2 -> agent3` edges
## 6. Sample: Graph Workflow with Functions and Routing
A data processing pipeline with conditional routing:
### agent.py
```python
from google.adk.workflow import Workflow
from google.adk.events.event import Event
from google.adk.agents.context import Context
def parse_input(node_input: str) -> dict:
"""Parse the user's input into a structured format."""
words = node_input.strip().split()
return {"text": node_input, "word_count": len(words)}
def classify(node_input: dict):
"""Route based on input length."""
if node_input["word_count"] > 10:
return Event(output=node_input, route="long")
return Event(output=node_input, route="short")
def handle_short(node_input: dict) -> str:
return f"Short input ({node_input['word_count']} words): {node_input['text']}"
def handle_long(node_input: dict) -> str:
return f"Long input ({node_input['word_count']} words). Summary: {node_input['text'][:50]}..."
root_agent = Workflow(
name="classifier_workflow",
input_schema=str,
edges=[
('START', parse_input),
(parse_input, classify),
(classify, handle_short, "short"),
(classify, handle_long, "long"),
],
)
```
## 7. Sample: Parallel Processing
Process a list of items concurrently:
### agent.py
```python
from google.adk.workflow import Workflow
from google.adk.workflow import node
def split_input(node_input: str) -> list:
"""Split comma-separated input into a list."""
return [item.strip() for item in node_input.split(",")]
@node(parallel_worker=True)
def process_item(node_input: str) -> dict:
"""Process a single item (runs in parallel for each list item)."""
return {"item": node_input, "length": len(node_input), "upper": node_input.upper()}
def format_results(node_input: list) -> str:
"""Format the parallel results into a readable summary."""
lines = [f"- {r['item']}: {r['length']} chars -> {r['upper']}" for r in node_input]
return "Results:\n" + "\n".join(lines)
root_agent = Workflow(
name="parallel_processor",
input_schema=str,
edges=[
('START', split_input),
(split_input, process_item),
(process_item, format_results),
],
)
```
## 8. Sample: Workflow with LLM Agent and Tools
Combine function nodes with an LLM agent that has tools:
### agent.py
```python
from google.adk.agents.llm_agent import LlmAgent
from google.adk.workflow import Workflow
from google.adk.agents.context import Context
def get_weather(city: str) -> dict:
"""Get the current weather for a city."""
# In production, call a real API
return {"city": city, "temp": "72F", "condition": "sunny"}
def extract_city(node_input: str) -> str:
"""Extract city name from user input."""
# Simple extraction; in production, use NLP or LLM
return node_input.strip()
weather_agent = LlmAgent(
name="weather_reporter",
model="gemini-2.5-flash",
instruction="""You are a friendly weather reporter.
Use the get_weather tool to look up the weather, then give
a natural-language weather report for the city.""",
tools=[get_weather],
)
def format_output(ctx: Context, node_input: str) -> str:
"""Add a friendly sign-off."""
return f"{node_input}\n\nHave a great day!"
root_agent = Workflow(
name="weather_workflow",
input_schema=str,
edges=[
('START', extract_city),
(extract_city, weather_agent),
(weather_agent, format_output),
],
)
```
## Troubleshooting
### "No module named 'google.adk'"
Ensure the virtual environment is activated and `google-adk` is installed.
### Agent not showing in `adk web`
Check that `__init__.py` contains `from . import agent` and `agent.py` defines `root_agent`.
### API key errors
Verify `.env` is in the agent directory (not the parent) and contains a valid `GOOGLE_API_KEY`.
### Model not found
Check the model name. Common models: `gemini-2.5-flash`, `gemini-2.5-pro`. The ADK also supports non-Google models (Anthropic, LiteLLM) with extra dependencies.
@@ -0,0 +1,279 @@
# Human-in-the-Loop (HITL) Reference
Pause workflow execution to request user input and resume with their response.
## 📋 Agent Verification Checklist (HITL)
Use this checklist when implementing human-in-the-loop logic:
- [ ] **Unique ID**: Is the `interrupt_id` unique per iteration in loops? (Critical to prevent infinite loops)
- [ ] **Resumability**: For multi-step HITL, did you export an `App` with `is_resumable=True`?
- [ ] **Resume Inputs**: If `rerun_on_resume=True` (default for LLM nodes), does the node handle `ctx.resume_inputs`?
## 💡 Quick Reference
- **Request Input**: `yield RequestInput(message="Question", response_schema=Schema)`
- **Resumable Config**: `ResumabilityConfig(is_resumable=True)`
HITL works in two modes:
### Resumable mode (recommended for multi-step HITL)
Export an `App` with resumability. The workflow checkpoints state and resumes at the interrupted node:
```python
from google.adk.apps.app import App, ResumabilityConfig
app = App(
name="my_app",
root_agent=workflow_agent,
resumability_config=ResumabilityConfig(is_resumable=True),
)
```
The agent loader checks for `app` before `root_agent`, so export both from `agent.py`.
### Non-resumable mode (simpler, no App needed)
The workflow replays from START on each user response, reconstructing state from session events. No `App` or `ResumabilityConfig` needed — just define `root_agent`. This works for simple single-interrupt HITL but replays all nodes up to the interrupt point on each resume.
## Imports
```python
from google.adk.events.request_input import RequestInput
from google.adk.agents.context import Context
from google.adk.workflow import Workflow
from google.adk.apps.app import App, ResumabilityConfig
```
## Basic Request Input
Yield or return a `RequestInput` to pause execution and ask the user for input:
```python
# Yield from a generator
async def approval_gate(ctx: Context, node_input: str):
yield RequestInput(
message="Please approve this action:",
response_schema={"type": "string"},
)
# Or return directly from a regular function (no generator needed)
def evaluate_request(request: TimeOffRequest):
if request.days <= 1:
return TimeOffDecision(approved=True) # Auto-approve
return RequestInput(
interrupt_id="manager_approval",
message="Please review this time off request.",
payload=request,
response_schema=TimeOffDecision,
)
```
The workflow pauses and emits a function call event to the user. When the user responds, the workflow resumes.
## RequestInput Fields
```python
from pydantic import BaseModel
class ApprovalResponse(BaseModel):
approved: bool
comment: str
RequestInput(
interrupt_id="custom_id", # Auto-generated UUID if omitted
message="Question for user", # Display message
payload={"key": "value"}, # Custom data to include
response_schema=ApprovalResponse, # Pydantic class, Python type, or JSON schema dict
)
```
| Field | Type | Description |
|-------|------|-------------|
| `interrupt_id` | `str` | Unique ID for this interrupt (auto-generated UUID) |
| `message` | `str` | Message shown to the user |
| `payload` | `Any` | Custom payload sent with the request |
| `response_schema` | `type \| dict` | Expected response format (Pydantic BaseModel class, Python type, or JSON schema dict) |
## Resume Behavior: rerun_on_resume
When a node is interrupted and the user responds, the `rerun_on_resume` flag controls what happens:
### rerun_on_resume=False (default for FunctionNode)
The user's response becomes the node's output. The node is NOT re-executed:
```python
from google.adk.workflow import FunctionNode
async def ask_approval(ctx: Context, node_input: str):
yield RequestInput(message="Approve?")
# Node won't rerun; user's response is passed as output to next node
approval_node = FunctionNode(ask_approval, rerun_on_resume=False)
```
### rerun_on_resume=True (default for LlmAgentWrapper)
The node is re-executed with the user's response available in `ctx.resume_inputs`:
```python
async def interactive_node(ctx: Context, node_input: str):
if ctx.resume_inputs:
# Second run: user responded
user_answer = list(ctx.resume_inputs.values())[0]
yield Event(output=f"User said: {user_answer}")
else:
# First run: ask the user
yield RequestInput(message="What should I do?")
```
## HITL with LLM Agents
LLM agents support HITL via `LongRunningFunctionTool`:
```python
from google.adk.tools.long_running_tool import LongRunningFunctionTool
def approval_tool(request: str) -> str:
"""Request human approval for an action."""
return f"Approved: {request}"
llm_agent = LlmAgent(
name="agent_with_approval",
model="gemini-2.5-flash",
instruction="When you need approval, use the approval_tool.",
tools=[LongRunningFunctionTool(func=approval_tool)],
)
# LlmAgentWrapper has rerun_on_resume=True by default
agent = Workflow(
name="hitl_workflow",
edges=[
('START', llm_agent),
(llm_agent, next_step),
],
)
```
## Multi-Step HITL
A node can request input multiple times by checking `ctx.resume_inputs`:
```python
async def multi_step_form(ctx: Context, node_input: str):
if not ctx.resume_inputs:
# Step 1: Ask for name
yield RequestInput(
interrupt_id="ask_name",
message="What is your name?",
)
return
if "ask_name" in ctx.resume_inputs and "ask_email" not in ctx.resume_inputs:
# Step 2: Ask for email
yield RequestInput(
interrupt_id="ask_email",
message="What is your email?",
)
return
# All inputs collected
name = ctx.resume_inputs["ask_name"]
email = ctx.resume_inputs["ask_email"]
yield Event(output={"name": name, "email": email})
```
## HITL in Loops (Unique interrupt_id)
When a HITL node can fire multiple times in a loop (e.g. reject → revise → re-approve), you **must use a unique `interrupt_id` per iteration**. Reusing the same ID causes event-based state reconstruction to confuse earlier responses with the current interrupt, resulting in an infinite restart loop.
```python
async def review(ctx: Context, node_input: Any):
# Counter-based unique ID per review cycle
review_count = ctx.state.get('review_count', 0)
interrupt_id = f'review_{review_count}'
response = ctx.resume_inputs.get(interrupt_id)
if response:
route = 'approved' if response.get('approved') else 'rejected'
yield Event(
output=response,
route=route,
state={'review_count': review_count + 1},
)
return
yield RequestInput(
interrupt_id=interrupt_id,
message="Approve this plan?",
response_schema=ApprovalSchema,
)
```
Key points:
- Store a counter in `ctx.state` and increment on each response
- Use the counter in the `interrupt_id` (e.g. `review_0`, `review_1`, ...)
- Look up `ctx.resume_inputs` with the same counter-based ID
- This applies to both resumable and non-resumable modes
## Resumability Configuration
### Resumable mode (recommended for multi-step HITL)
```python
from google.adk.apps.app import App, ResumabilityConfig
# Export BOTH root_agent and app from agent.py
root_agent = Workflow(name="my_workflow", edges=[...])
app = App(
name="my_app",
root_agent=root_agent,
resumability_config=ResumabilityConfig(is_resumable=True),
)
```
When `is_resumable=True`:
- Workflow state is checkpointed in session's `agent_states` map
- On resume, the workflow loads checkpointed state and resumes at the interrupted node
- Required for multi-step HITL, `LongRunningFunctionTool`, and complex workflows
### Non-resumable mode (simpler)
When `is_resumable=False` (default) or no `App` is exported:
- No state checkpointing — the workflow replays from START on each user response
- State is reconstructed from session events during replay
- Completed nodes are skipped; execution resumes at the interrupted node
- Works for simple single-interrupt HITL without needing `App` or `ResumabilityConfig`
- For multi-step HITL or complex workflows, use resumable mode instead
## Responding to HITL Requests
From the client side, respond to function calls:
```python
from google.genai import types
# Extract function_call_id from the interrupt event
function_call_id = interrupt_event.content.parts[0].function_call.id
# Create response
response = types.Content(
role="user",
parts=[types.Part(
function_response=types.FunctionResponse(
id=function_call_id,
name="adk_request_input",
response={"result": "User's answer here"},
)
)],
)
# Send response to resume the workflow
async for event in runner.run_async(
user_id=user_id,
session_id=session_id,
new_message=response,
):
# Process resumed workflow events
pass
```
@@ -0,0 +1,148 @@
# ADK Import Paths Quick Reference
## 📋 Agent Verification Checklist (Imports)
Use this checklist to ensure you are using the most idiomatic import paths:
- [ ] **Canonical Imports**: Did you use the short canonical imports where available (e.g., `from google.adk import Agent`) instead of the verbose ones?
- [ ] **Avoid Deprecated**: Are you avoiding deprecated paths (e.g., use `McpToolset` instead of `MCPToolset`)?
## Canonical Imports (preferred, used by all samples)
```python
from google.adk import Agent, Context, Event, Workflow
from google.adk.events import RequestInput
from google.adk.workflow import node, RetryConfig, Edge, JoinNode
```
## Core Agents
| Component | Import |
|-----------|--------|
| `Agent` (canonical) | `from google.adk import Agent` |
| `Agent` (verbose) | `from google.adk.agents.llm_agent import Agent` |
| `LlmAgent` | `from google.adk.agents.llm_agent import LlmAgent` |
| `SequentialAgent` | `from google.adk.agents.sequential_agent import SequentialAgent` |
| `ParallelAgent` | `from google.adk.agents.parallel_agent import ParallelAgent` |
| `LoopAgent` | `from google.adk.agents.loop_agent import LoopAgent` |
## Workflow Agents (Experimental)
| Component | Import |
|-----------|--------|
| `Workflow` | `from google.adk.workflow import Workflow` |
| `Edge` | `from google.adk.workflow import Edge` |
| `Agent` (supports task/single_turn mode) | `from google.adk import Agent` |
## Workflow Nodes
| Component | Import |
| ----------------------------------- | -------------------------------------- |
| `FunctionNode` | `from google.adk.workflow import |
: : FunctionNode` :
| `_LlmAgentWrapper` (private, | `from |
: auto-used) : google.adk.workflow._llm_agent_wrapper :
: : import _LlmAgentWrapper` :
| `AgentNode` | `from google.adk.workflow._agent_node |
: : import AgentNode` :
| `_ToolNode` (private) | `from google.adk.workflow._tool_node |
: : import _ToolNode` :
| `JoinNode` | `from google.adk.workflow import |
: : JoinNode` :
| Parallel-worker behavior (no public | Set `parallel_worker=True` on `@node` |
: class) : or `LlmAgent`; the framework wraps :
: : with an internal `_ParallelWorker` :
| `BaseNode`, `START` | `from google.adk.workflow import |
: : BaseNode, START` :
| `@node` decorator | `from google.adk.workflow import node` |
## Workflow Events and Context
| Component | Import |
|-----------|--------|
| `Event` | `from google.adk.events.event import Event` |
| `RequestInput` | `from google.adk.events.request_input import RequestInput` |
| `Context` | `from google.adk.agents.context import Context` |
| `WorkflowGraph` | `from google.adk.workflow._workflow_graph import WorkflowGraph` |
| `RetryConfig` | `from google.adk.workflow import RetryConfig` |
## Task Mode
| Component | Import |
|-----------|--------|
| `RequestTaskTool` | `from google.adk.agents.llm.task._request_task_tool import RequestTaskTool` |
| `FinishTaskTool` | `from google.adk.agents.llm.task._finish_task_tool import FinishTaskTool` |
| `TaskRequest`, `TaskResult` | `from google.adk.agents.llm.task._task_models import TaskRequest, TaskResult` |
## Tools
| Component | Import |
|-----------|--------|
| `FunctionTool` | `from google.adk.tools.function_tool import FunctionTool` |
| `BaseTool` | `from google.adk.tools.base_tool import BaseTool` |
| `BaseToolset` | `from google.adk.tools.base_toolset import BaseToolset` |
| `ToolContext` | `from google.adk.tools.tool_context import ToolContext` |
| `LongRunningFunctionTool` | `from google.adk.tools.long_running_tool import LongRunningFunctionTool` |
| `McpToolset` | `from google.adk.tools.mcp_tool.mcp_toolset import McpToolset` |
| `StdioConnectionParams` | `from google.adk.tools.mcp_tool import StdioConnectionParams` |
| `SseConnectionParams` | `from google.adk.tools.mcp_tool import SseConnectionParams` |
| `OpenAPIToolset` | `from google.adk.tools.openapi_tool import OpenAPIToolset` |
## Built-in Tools
| Tool | Import |
|------|--------|
| `google_search` | `from google.adk.tools import google_search` |
| `load_artifacts` | `from google.adk.tools import load_artifacts` |
| `load_memory` | `from google.adk.tools import load_memory` |
| `exit_loop` | `from google.adk.tools import exit_loop` |
| `transfer_to_agent` | `from google.adk.tools import transfer_to_agent` |
| `get_user_choice` | `from google.adk.tools import get_user_choice` |
## Runner and Session
| Component | Import |
|-----------|--------|
| `Runner` | `from google.adk.runners import Runner` |
| `InMemoryRunner` | `from google.adk.runners import InMemoryRunner` |
| `InMemorySessionService` | `from google.adk.sessions import InMemorySessionService` |
| `DatabaseSessionService` | `from google.adk.sessions import DatabaseSessionService` |
## App and Plugins
| Component | Import |
|-----------|--------|
| `App` | `from google.adk.apps import App` |
| `ResumabilityConfig` | `from google.adk.apps.app import ResumabilityConfig` |
| `BasePlugin` | `from google.adk.plugins.base_plugin import BasePlugin` |
| `ContextFilterPlugin` | `from google.adk.plugins.context_filter_plugin import ContextFilterPlugin` |
## Models
| Component | Import |
|-----------|--------|
| `LiteLlm` | `from google.adk.models.lite_llm import LiteLlm` |
| `LlmRequest` | `from google.adk.models.llm_request import LlmRequest` |
| `LlmResponse` | `from google.adk.models.llm_response import LlmResponse` |
## Callbacks
| Component | Import |
|-----------|--------|
| `CallbackContext` | `from google.adk.agents.callback_context import CallbackContext` |
| `ReadonlyContext` | `from google.adk.agents.readonly_context import ReadonlyContext` |
## Code Executors
| Component | Import |
|-----------|--------|
| `BuiltInCodeExecutor` | `from google.adk.code_executors.built_in_code_executor import BuiltInCodeExecutor` |
## Google GenAI Types
| Component | Import |
|-----------|--------|
| `types` | `from google.genai import types` |
| `Content` | `from google.genai.types import Content` |
| `ModelContent` | `from google.genai.types import ModelContent` |
| `Part` | `from google.genai.types import Part` |
| `GenerateContentConfig` | `from google.genai.types import GenerateContentConfig` |
@@ -0,0 +1,462 @@
# LLM Agent Nodes Reference
Embed LLM-powered agents as nodes in workflow graphs.
## 📋 Agent Verification Checklist (LLM Nodes)
Use this checklist to verify your LLM agent configuration:
- [ ] **Output Type**: If no `output_schema` is set, downstream now receives `str` (auto-extracted from `types.Content`). You can safely type-hint `node_input: str`.
- [ ] **State Serialization**: If this agent feeds into a `JoinNode`, did you set `output_schema` to avoid non-serializable `types.Content` errors?
- [ ] **Instructions**: Are `{var}` templates used in instructions resolving ONLY from `ctx.state`? (Not `node_input`)
- [ ] **Config**: Are instructions, tools, and response schema set on the `LlmAgent` directly, and NOT in `generate_content_config`?
## 💡 Quick Reference
- **Chat Mode**: Default. Multi-turn, keeps session history.
- **Single-Turn Mode**: Isolated. Set `mode="single_turn"` or rely on auto-wrapping defaults.
- **Task Mode**: Multi-turn within a task. Set `mode="task"`.
- **Stateless**: Set `include_contents="none"` to ignore session history.
## Imports
```python
from google.adk.agents.llm_agent import LlmAgent
from google.adk.workflow._llm_agent_wrapper import _LlmAgentWrapper # private
from google.adk.workflow import Workflow
```
## Choosing the Right LLM Agent
**Use `google.adk.agents.llm_agent.LlmAgent`** in workflow edges. It is auto-wrapped as `LlmAgentWrapper`, which emits `Event(output=...)` for downstream data passing. This is required for any LLM agent that needs to pass output to downstream function nodes via `node_input`.
```python
from google.adk.agents.llm_agent import LlmAgent
writer = LlmAgent(
name="writer",
model="gemini-2.5-flash",
instruction="Write a short story.",
output_schema=Story,
)
# writer is auto-wrapped as _LlmAgentWrapper — downstream gets Event(output=...)
agent = Workflow(
name="pipeline",
edges=[('START', writer), (writer, process_story)],
)
```
## Basic LLM Node
```python
from google.adk.agents.llm_agent import LlmAgent
writer = LlmAgent(
name="writer",
model="gemini-2.5-flash",
instruction="Write a short story based on the user's prompt.",
)
reviewer = LlmAgent(
name="reviewer",
model="gemini-2.5-flash",
instruction="Review the following story and provide feedback.",
)
agent = Workflow(
name="story_pipeline",
edges=[
('START', writer), # Auto-wrapped as LlmAgentWrapper
(writer, reviewer),
],
)
```
## LLM Agent Output Types
When an `LlmAgent` runs as a workflow node, `process_llm_agent_output` (in
`_llm_agent_wrapper.py`) sets `event.output` to:
- The **concatenated text** of the model's response (a `str`) — when
`output_schema` is not set.
- The **validated dict** (`model_dump()` of the Pydantic model) — when
`output_schema=MyModel` is set.
A downstream function node typed `node_input: str` therefore works in the
default case, and `node_input: dict` works when `output_schema` is set.
**Observability caveat:** the value above is set on the event internally and
forwarded to the next node, but `event.output` is **`None`** when you observe it
from `runner.run_async(...)` for the LLM agent's own event — the framework
clears it before the event reaches user code. Don't write tests that assert on
`event.output` for an LLM agent's event; assert on the downstream node's output,
on `session.state[output_key]`, or on `event.content.parts[*].text` instead.
```python
from pydantic import BaseModel
class CodeOutput(BaseModel):
code: str
language: str
writer = LlmAgent(
name="writer",
model="gemini-2.5-flash",
instruction="Write code. Return JSON with 'code' and 'language' fields.",
output_schema=CodeOutput,
)
# Downstream node receives a dict: {"code": "...", "language": "python"}
def process_code(node_input: dict) -> str:
return node_input["code"]
```
**Summary of LLM agent node output types:**
LLM Agent Config | `node_input` Type for Next Node
-------------------- | -----------------------------------
No `output_schema` | `str` (concatenated model text)
With `output_schema` | `dict` (parsed from Pydantic model)
**Prefer `output_schema` when downstream nodes need structured access.** Strings
are fine for pass-through text, but a typed dict is easier to consume and is
required when the predecessor feeds a `JoinNode` whose results land in a
persistent session service (raw text is fine; objects that aren't
JSON-serializable break `DatabaseSessionService`).
## Auto-Wrapping Behavior
When you place an `LlmAgent` in workflow edges, it is auto-wrapped as `_LlmAgentWrapper`. The wrapper:
- Defaults to `single_turn` mode (agent sees only current input, not session history)
- Sets `rerun_on_resume=True` (reruns after HITL interrupts)
- Creates a content branch for isolation between parallel LLM agents
The mode is set on the `LlmAgent` itself, not the wrapper:
```python
from google.adk.agents.llm_agent import LlmAgent
# single_turn (default when auto-wrapped): isolated, no session history
classifier = LlmAgent(
name="classifier",
model="gemini-2.5-flash",
instruction="Classify the input as positive, negative, or neutral.",
output_schema=ClassificationResult,
)
# task mode: supports HITL, multi-turn within the task
task_agent = LlmAgent(
name="task_agent",
model="gemini-2.5-flash",
mode="task",
instruction="Process the request.",
)
```
## LlmAgent Configuration
### Instructions
Dynamic instructions with placeholders resolved from session state. **`{var}` templates only resolve from `ctx.state``node_input` is NOT available in templates.** To use predecessor data in instructions, store it in state first (via `Event(state={...})` or `output_key`):
```python
agent = LlmAgent(
name="personalized",
model="gemini-2.5-flash",
instruction="""You are helping {user_name}.
Their preferences are: {preferences}.
Respond in {language}.""",
)
# {user_name}, {preferences}, {language} resolved from session state
# Missing variables raise KeyError at runtime — use {var?} for optional:
# instruction="Current mood: {mood?}" # empty string if 'mood' not in state
```
**Template variable behavior:**
| Syntax | Missing Key Behavior |
|--------|---------------------|
| `{var}` | Raises `KeyError` at LLM call time |
| `{var?}` | Substitutes empty string, logs debug message |
| `{not.an" identifier}` | Left as-is (not substituted) |
Instruction provider function for fully dynamic instructions:
```python
from google.adk.agents.readonly_context import ReadonlyContext
def build_instruction(ctx: ReadonlyContext) -> str:
agents = ctx.state.get("active_agents", [])
return f"Coordinate these agents: {', '.join(agents)}"
agent = LlmAgent(
name="coordinator",
model="gemini-2.5-flash",
instruction=build_instruction,
)
```
### Output Schema
Structure LLM output with Pydantic models:
```python
from pydantic import BaseModel
class ReviewResult(BaseModel):
score: int
feedback: str
approved: bool
reviewer = LlmAgent(
name="reviewer",
model="gemini-2.5-flash",
instruction="Review the code and provide structured feedback.",
output_schema=ReviewResult,
)
```
When used as a workflow node, the output becomes a `dict` (via `model_dump()`) as `node_input` for the next node.
### Output Key
Store agent output in session state:
```python
agent = LlmAgent(
name="writer",
model="gemini-2.5-flash",
instruction="Write a draft.",
output_key="draft", # Stores output in state['draft']
)
```
### include_contents
Control conversation history:
```python
agent = LlmAgent(
name="stateless",
model="gemini-2.5-flash",
instruction="Process this input independently.",
include_contents="none", # Don't include session history
)
```
## Tools
Add tools to LLM agents:
```python
def search_database(query: str) -> str:
"""Search the database for relevant records."""
return f"Results for: {query}"
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to the specified address."""
return "Email sent"
agent = LlmAgent(
name="assistant",
model="gemini-2.5-flash",
instruction="Help the user with their request.",
tools=[search_database, send_email],
)
```
Tools can be:
- Python functions (auto-wrapped as `FunctionTool`)
- `BaseTool` instances
- `BaseToolset` instances (e.g., MCP toolsets)
## Callbacks
### Before Model Callback
Intercept or modify LLM requests. Return an `LlmResponse` to skip the LLM call; return `None` to proceed:
```python
from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest
from google.adk.models.llm_response import LlmResponse
def guard_callback(
callback_context: CallbackContext,
llm_request: LlmRequest,
) -> LlmResponse | None:
for content in llm_request.contents:
if content.parts:
for part in content.parts:
if part.text and "unsafe" in part.text:
return LlmResponse(
content=types.ModelContent("I cannot process that.")
)
return None # Proceed with normal LLM call
agent = LlmAgent(
name="guarded",
model="gemini-2.5-flash",
before_model_callback=guard_callback,
)
```
### After Model Callback
Transform LLM responses. Return an `LlmResponse` to replace; return `None` to keep original:
```python
def log_response(
callback_context: CallbackContext,
llm_response: LlmResponse,
) -> LlmResponse | None:
print(f"LLM responded: {llm_response.content}")
return None # Use original response
agent = LlmAgent(
name="logged",
model="gemini-2.5-flash",
after_model_callback=log_response,
)
```
### Before/After Tool Callbacks
Intercept tool calls. Return a `dict` to use as tool response (skipping actual execution); return `None` to proceed:
```python
from google.adk.tools.base_tool import BaseTool
from google.adk.tools.tool_context import ToolContext
def audit_tool(
tool: BaseTool,
args: dict[str, Any],
tool_context: ToolContext,
) -> dict | None:
print(f"Calling tool {tool.name} with args: {args}")
return None # Proceed with tool call
def validate_tool_result(
tool: BaseTool,
args: dict[str, Any],
tool_context: ToolContext,
tool_response: dict,
) -> dict | None:
if "error" in tool_response:
return {"result": "Tool execution failed, please try again."}
return None # Use original result
agent = LlmAgent(
name="audited",
model="gemini-2.5-flash",
tools=[my_tool],
before_tool_callback=audit_tool,
after_tool_callback=validate_tool_result,
)
```
### Multiple Callbacks
Pass a list of callbacks. They execute in order until one returns non-None:
```python
agent = LlmAgent(
name="multi_callback",
model="gemini-2.5-flash",
before_model_callback=[safety_check, rate_limiter, logger],
)
```
### Error Callbacks
Handle LLM or tool errors gracefully:
```python
def handle_model_error(
callback_context: CallbackContext,
llm_request: LlmRequest,
error: Exception,
) -> LlmResponse | None:
return LlmResponse(
content=types.ModelContent("Service temporarily unavailable.")
)
def handle_tool_error(
tool: BaseTool,
args: dict[str, Any],
tool_context: ToolContext,
error: Exception,
) -> dict | None:
return {"error": str(error), "fallback": True}
agent = LlmAgent(
name="resilient",
model="gemini-2.5-flash",
on_model_error_callback=handle_model_error,
on_tool_error_callback=handle_tool_error,
)
```
## All Callback Types
| Callback | Signature | Return to Override |
|----------|-----------|-------------------|
| `before_model_callback` | `(CallbackContext, LlmRequest) -> LlmResponse?` | Return `LlmResponse` to skip LLM |
| `after_model_callback` | `(CallbackContext, LlmResponse) -> LlmResponse?` | Return `LlmResponse` to replace |
| `on_model_error_callback` | `(CallbackContext, LlmRequest, Exception) -> LlmResponse?` | Return `LlmResponse` to suppress error |
| `before_tool_callback` | `(BaseTool, dict, ToolContext) -> dict?` | Return `dict` to skip tool |
| `after_tool_callback` | `(BaseTool, dict, ToolContext, dict) -> dict?` | Return `dict` to replace result |
| `on_tool_error_callback` | `(BaseTool, dict, ToolContext, Exception) -> dict?` | Return `dict` to suppress error |
All callbacks can be sync or async. All accept a single callback or a list.
## Generate Content Config
Fine-tune LLM generation:
```python
from google.genai import types
agent = LlmAgent(
name="creative",
model="gemini-2.5-flash",
instruction="Write creative stories.",
generate_content_config=types.GenerateContentConfig(
temperature=0.9,
top_p=0.95,
max_output_tokens=2048,
),
)
```
## Agent Transfer
Agents can transfer control to sub-agents:
```python
specialist = LlmAgent(
name="specialist",
model="gemini-2.5-flash",
instruction="Handle specialized requests.",
)
coordinator = LlmAgent(
name="coordinator",
model="gemini-2.5-flash",
instruction="Route requests to the specialist when needed.",
sub_agents=[specialist],
)
```
Control transfer behavior:
```python
agent = LlmAgent(
name="isolated",
model="gemini-2.5-flash",
disallow_transfer_to_parent=True,
disallow_transfer_to_peers=True,
)
```
@@ -0,0 +1,142 @@
# Multi-Agent Patterns
## 📋 Agent Verification Checklist (Multi-Agent)
Use this checklist when setting up multi-agent systems:
- [ ] **Description**: Does every sub-agent have a clear `description`? (Used by LLM for routing or tool generation)
- [ ] **Model Inheritance**: Did you let sub-agents inherit the model from the coordinator to avoid duplication?
- [ ] **Loop Termination**: If using `LoopAgent`, is there a clear way to call `exit_loop` to prevent infinite loops?
## 💡 Quick Reference
- **Sequential**: `SequentialAgent(sub_agents=[a, b, c])`
- **Parallel**: `ParallelAgent(sub_agents=[a, b, c])`
- **Loop**: `LoopAgent(sub_agents=[a, b], max_iterations=5)`
## LLM-Based Multi-Agent (Chat Transfer)
```python
from google.adk.agents.llm_agent import Agent
researcher = Agent(
name='researcher',
description='Researches topics.',
instruction='You research topics and provide findings.',
tools=[search_tool],
)
writer = Agent(
name='writer',
description='Writes content.',
instruction='You write content based on research.',
)
root_agent = Agent(
model='gemini-2.5-flash',
name='coordinator',
instruction=(
'Delegate research to the researcher and '
'writing to the writer.'
),
sub_agents=[researcher, writer],
)
```
**Key rules:**
- Only the root agent needs `model=`. Sub-agents inherit it.
- Each sub-agent needs a `description` (used for routing).
- Transfer between agents is automatic via LLM reasoning.
- `disallow_transfer_to_parent=True` prevents back-transfer.
- `disallow_transfer_to_peers=True` prevents peer-transfer.
## Task-Based Multi-Agent (Structured Delegation)
For structured input/output, use task mode instead of chat transfer. See **`task-mode.md`** for full details.
```python
from google.adk import Agent
worker = Agent(
name='worker',
mode='task', # or 'single_turn'
input_schema=WorkerInput,
output_schema=WorkerOutput,
instruction='Do work, then call finish_task.',
description='Performs structured work.',
)
root_agent = Agent(
name='coordinator',
model='gemini-2.5-flash',
sub_agents=[worker],
instruction='Delegate to worker via request_task_worker.',
)
```
## Non-LLM Orchestration Agents
### SequentialAgent
Runs sub-agents in order, one after another:
```python
from google.adk.agents.sequential_agent import SequentialAgent
root_agent = SequentialAgent(
name='pipeline',
sub_agents=[step1_agent, step2_agent, step3_agent],
)
```
### ParallelAgent
Runs sub-agents concurrently:
```python
from google.adk.agents.parallel_agent import ParallelAgent
root_agent = ParallelAgent(
name='fan_out',
sub_agents=[task_a, task_b, task_c],
)
```
### LoopAgent
Repeats sub-agents until `exit_loop` is called:
```python
from google.adk.tools import exit_loop
from google.adk.agents.loop_agent import LoopAgent
looping_agent = Agent(
name='checker',
tools=[exit_loop],
instruction='Check the result and call exit_loop if done.',
)
root_agent = LoopAgent(
name='retry_loop',
sub_agents=[worker_agent, looping_agent],
max_iterations=5,
)
```
## Model Configuration
- Default model: `gemini-2.5-flash`
- Override globally: `Agent.set_default_model('gemini-2.5-pro')`
- Model inheritance: sub-agents inherit parent's model if not set
- Non-Gemini models via LiteLlm:
```python
from google.adk.models.lite_llm import LiteLlm
root_agent = Agent(model=LiteLlm(model='anthropic/claude-sonnet-4-20250514'), ...)
```
## Common Pitfalls
- **Agent stuck in sub-agent:** Sub-agent has no path back to parent.
Set `disallow_transfer_to_parent=False` (default) or add explicit
transfer instructions.
- **Wrong agent handles request:** Ambiguous `description` fields. Make
each agent's description clearly differentiate its scope.
- **Circular imports:** Define all agents in a single `agent.py` file,
or use a shared module for sub-agents.
@@ -0,0 +1,227 @@
# Parallel Execution, Fan-Out, and Fan-In Reference
Execute multiple nodes concurrently and collect their results.
## 📋 Agent Verification Checklist (Parallel & Fan-Out)
Use this checklist when implementing parallel patterns:
- [ ] **JoinNode Serialization**: If LLM agents feed into a `JoinNode`, did you set `output_schema` on them to prevent JSON serialization errors?
- [ ] **ParallelWorker Usage**: Did you avoid using `parallel_worker=True` on fan-out nodes? (It expects a list input)
- [ ] **Multi-Trigger vs Join**: Do you understand that Multi-Trigger fires downstream once per branch, while JoinNode waits and fires once with merged dict?
## 💡 Quick Reference
- **Fan-Out (Tuple)**: `('START', (node_a, node_b))`
- **Fan-In (JoinNode)**: `((node_a, node_b), join_node)`
- **List Worker**: `@node(parallel_worker=True)` (Takes list, outputs list)
## Imports
```python
from google.adk.workflow import Workflow, JoinNode, node
```
Parallel-worker behavior is opted into via the `parallel_worker=True` flag on
`@node` or `LlmAgent`. The underlying wrapper class is internal — don't import
it directly.
## Fan-Out: Multiple Branches
Send output to multiple nodes simultaneously using tuple syntax:
```python
def analyze_text(node_input: str) -> str:
return f"Analysis: {node_input}"
def translate_text(node_input: str) -> str:
return f"Translation: {node_input}"
def summarize_text(node_input: str) -> str:
return f"Summary: {node_input}"
agent = Workflow(
name="fan_out",
edges=[
('START', (analyze_text, translate_text, summarize_text)),
],
)
```
Each branch receives the same input and runs concurrently.
## Fan-In: JoinNode
Collect outputs from multiple branches before continuing:
```python
join = JoinNode(name="collect_results")
agent = Workflow(
name="fan_out_fan_in",
edges=[
('START', (analyze_text, translate_text, summarize_text)),
((analyze_text, translate_text, summarize_text), join),
(join, final_processor),
],
)
```
### JoinNode Output Format
JoinNode outputs a dictionary mapping predecessor names to their outputs:
```python
# JoinNode output:
# {
# "analyze_text": "Analysis: hello",
# "translate_text": "Translation: hello",
# "summarize_text": "Summary: hello",
# }
def final_processor(node_input: dict) -> str:
analysis = node_input["analyze_text"]
translation = node_input["translate_text"]
summary = node_input["summarize_text"]
return f"Combined: {analysis}, {translation}, {summary}"
```
### JoinNode Behavior
- Waits for **all** predecessor nodes to complete
- Emits intermediate events while still waiting (downstream not triggered until all inputs received)
- Only triggers downstream when all inputs are received
- Stores partial inputs in workflow state
**Serialization warning:** JoinNode stores partial inputs in session state while waiting. If predecessors are LLM agents without `output_schema`, the stored values are `types.Content` objects which are **not JSON-serializable**. This causes `TypeError` with SQLite/database session services. Fix: use `output_schema` on LLM agents feeding into a JoinNode.
## Parallel workers: process lists in parallel
Apply the same node to each item in a list concurrently by setting the
`parallel_worker=True` flag. The framework wraps the node internally — there is
no public `ParallelWorker` class to import.
```python
from google.adk.workflow import node, Workflow
@node(parallel_worker=True)
def process_item(node_input: int) -> int:
return node_input * 2
def produce_list(node_input: str) -> list:
return [1, 2, 3, 4, 5]
agent = Workflow(
name="parallel_processing",
edges=[
('START', produce_list),
(produce_list, process_item),
],
)
# Output: [2, 4, 6, 8, 10]
```
### Behavior
- Input: a **list** (or single item, which gets wrapped in a list)
- Output: a **list** of results in the same order as inputs
- Empty list input produces empty list output
- Each item is processed by a dynamically created worker node
- Default `rerun_on_resume=True`
### Parallel workers with Agents
Set `parallel_worker=True` directly on an Agent — no extra wrapping needed:
```python
from google.adk import Agent
explain_topic = Agent(
name="explain_topic",
instruction="Explain how this topic relates to the original topic: \"{topic}\".",
output_schema=TopicExplanation,
parallel_worker=True, # Each list item processed by a cloned agent
)
agent = Workflow(
name="parallel_analysis",
edges=[
('START', process_input, find_related_topics, explain_topic, aggregate),
],
)
```
**Do NOT use `parallel_worker=True` on fan-out nodes.** Fan-out edges `(a, (b, c, d))` already run nodes in parallel. Adding `parallel_worker=True` makes the node expect a list input and iterate over it — if it receives a single value or None, it produces no output and the JoinNode gets nothing.
## Multi-Trigger (Fan-Out to Shared Downstream)
Fan-out branches that all feed a single downstream node. The downstream node is triggered once per branch:
```python
async def send_message(node_input: Any):
yield Event(message=f"Triggered for input: {node_input}")
agent = Workflow(
name="root_agent",
edges=[(
"START",
(make_uppercase, count_characters, reverse_string),
send_message,
)],
input_schema=str,
)
```
This differs from JoinNode: here `send_message` fires 3 times (once per branch), while JoinNode waits for all branches and fires once with a merged dict.
## Diamond Pattern
Fan-out then fan-in (diamond shape):
```python
def splitter(node_input: str) -> str:
return node_input
def branch_a(node_input: str) -> str:
return f"A: {node_input}"
def branch_b(node_input: str) -> str:
return f"B: {node_input}"
join = JoinNode(name="merge")
def combiner(node_input: dict) -> str:
return f"Combined: {node_input['branch_a']} + {node_input['branch_b']}"
agent = Workflow(
name="diamond",
edges=[
('START', splitter),
(splitter, (branch_a, branch_b)),
((branch_a, branch_b), join),
(join, combiner),
],
)
```
## SequentialAgent and ParallelAgent
Convenience subclasses for common patterns:
```python
from google.adk.agents.sequential_agent import SequentialAgent
from google.adk.agents.parallel_agent import ParallelAgent
# Sequential: runs sub_agents in order
pipeline = SequentialAgent(
name="pipeline",
sub_agents=[writer_agent, reviewer_agent, editor_agent],
)
# Equivalent to: START -> writer -> reviewer -> editor
# Parallel: runs sub_agents concurrently
parallel = ParallelAgent(
name="concurrent",
sub_agents=[analyzer_agent, translator_agent, summarizer_agent],
)
# Equivalent to: START -> (analyzer, translator, summarizer)
```
@@ -0,0 +1,232 @@
# Routing and Conditional Branching Reference
Route workflow execution along different paths based on node outputs.
## 📋 Agent Verification Checklist (Routing)
Use this checklist when implementing routing logic:
- [ ] **Syntax**: Is the preferred dict syntax used for mapping routes to targets? (Avoid verbose individual edges)
- [ ] **Loops**: Are cycles (loops) routed? (Unconditional cycles are rejected during validation)
- [ ] **Triggering**: If a node has conditional routing, do ALL outgoing edges have routes? (To avoid unintended triggering by unconditional edges)
## 💡 Quick Reference
- **Dict Routing**: `(source_node, {"route_a": target_a, "route_b": target_b})`
- **Sequence**: `("START", step_a, step_b, step_c)`
- **Default**: `"__DEFAULT__"` (Fallback route)
## Basic Routing
A node emits an `Event` with a `route` value. Use **dict syntax** to map routes to target nodes:
```python
from google.adk import Event, Workflow
def classify(node_input: str):
if "error" in node_input:
return Event(output=node_input, route="error")
return Event(output=node_input, route="success")
def handle_success(node_input: str) -> str:
return f"Success: {node_input}"
def handle_error(node_input: str) -> str:
return f"Error: {node_input}"
agent = Workflow(
name="router",
edges=[
('START', classify),
(classify, {"success": handle_success, "error": handle_error}),
],
)
```
## Routing Map (Dict Syntax) — Preferred
The dict syntax is the idiomatic way to express routing. It maps route values to target nodes in a single edge tuple:
```python
edges = [
("START", process_input, classifier, route_on_category),
(route_on_category, {
"question": answer_question,
"statement": comment_on_statement,
"other": handle_other,
}),
]
```
This replaces verbose individual routed edges:
```python
# ❌ Verbose — avoid
(classifier, answer_question, "question"),
(classifier, comment_on_statement, "statement"),
(classifier, handle_other, "other"),
# ✅ Preferred — dict syntax
(classifier, {"question": answer_question, "statement": comment_on_statement, "other": handle_other}),
```
## Sequence Shorthand (Tuple Chains)
A tuple with more than 2 elements creates a sequential chain:
```python
# Shorthand: tuple creates chain edges
edges = [("START", step_a, step_b, step_c)]
# Equivalent to: [("START", step_a), (step_a, step_b), (step_b, step_c)]
```
Combine with dict routing:
```python
edges = [
("START", process_input, classify, route_on_result),
(route_on_result, {"approved": send, "rejected": discard}),
]
```
## Route Value Types
Routes can be `str`, `bool`, or `int`:
```python
# String routes (most common)
(decision_node, {"approve": path_a, "reject": path_b})
# Boolean routes
(decision_node, {True: yes_path, False: no_path})
# Integer routes
(decision_node, {0: path_0, 1: path_1})
```
## Default Route
Use `'__DEFAULT__'` as a fallback when no other route matches:
```python
edges = [
("START", classify),
(classify, {
"success": handler_a,
"error": handler_b,
"__DEFAULT__": fallback_handler,
}),
]
```
Only one default route per node is allowed.
**No duplicate edges:** Two edges from the same source to the same target are rejected, even with different routes. If you need both a named route and `__DEFAULT__` to reach the same destination, use a thin wrapper function for the default path.
## Dynamic Routing with Functions
A function node that emits different routes based on runtime data:
```python
from google.adk import Context, Event
def route_on_score(ctx: Context, node_input: dict):
score = node_input.get("score", 0)
if score > 0.8:
return Event(output=node_input, route="high")
elif score > 0.5:
return Event(output=node_input, route="medium")
else:
return Event(output=node_input, route="low")
agent = Workflow(
name="scored_router",
edges=[
("START", compute_score, route_on_score),
(route_on_score, {
"high": premium_handler,
"medium": standard_handler,
"low": basic_handler,
}),
],
)
```
## Multi-Route (Fan-Out via Route)
A node can output multiple routes to trigger multiple downstream paths simultaneously:
```python
def fan_out_router(node_input: str):
return Event(output=node_input, route=["path_a", "path_b"])
agent = Workflow(
name="multi_route",
edges=[
("START", fan_out_router),
(fan_out_router, {"path_a": branch_a, "path_b": branch_b}),
],
)
```
## List of Routes on a Single Edge
An edge can match multiple routes by passing a list as the route value. The edge fires if the node output matches **any** route in the list:
```python
edges = [
("START", classifier),
(classifier, {"route_z": handler_b}),
# handler_a fires on either route_x or route_y
(classifier, handler_a, ["route_x", "route_y"]),
]
```
This is useful when multiple route values should lead to the same downstream node without duplicating edges. Note: list-of-routes on a single edge uses the 3-tuple syntax since dict syntax maps one route to one target.
## Self-Loop
A node can route back to itself:
```python
def guess_number(target_number: int):
guess = random.randint(0, 10)
yield Event(message=f'Guessing {guess}...')
if guess == target_number:
yield Event(message='Correct!')
else:
yield Event(route='guessed_wrong')
agent = Workflow(
name='root_agent',
edges=[
('START', validate_input, guess_number),
(guess_number, {'guessed_wrong': guess_number}),
],
)
```
## Revision Loop
A common pattern: route back to an earlier node for revision, or forward for approval:
```python
edges = [
("START", process_input, draft_email, human_review),
(human_review, {
"revise": draft_email,
"approved": send,
"rejected": discard,
}),
]
```
**Important**: Cycles must have at least one routed edge (unconditional cycles are rejected during graph validation).
## Unconditional Edges
Edges without a route value are unconditional — they always fire:
```python
edges = [
('START', node_a), # Unconditional
(node_a, node_b), # Unconditional (always fires)
]
```
**Important**: Unrouted edges always fire, regardless of whether the output event has a route. If a node has conditional routing, ALL outgoing edges should have routes to avoid unintended triggering.
@@ -0,0 +1,101 @@
# Session, Memory, and Artifact Patterns
## 📋 Agent Verification Checklist (Session & State)
Use this checklist when managing state and artifacts:
- [ ] **State Mutation**: Did you use `ctx.state['key'] = value` instead of reassigning `state = {...}`?
- [ ] **Instruction Placeholders**: Did you use `{var?}` for variables that might not be in state yet?
- [ ] **Key Collisions**: In parallel workflows, do state keys have unique names or appropriate prefixes (e.g., `app:`) to prevent overwrites?
## 💡 Quick Reference (State Keys)
- **Required**: `{key}` in instructions (raises error if missing).
- **Optional**: `{key?}` in instructions (empty string if missing).
- **App Scope**: `app:key` (Shared across agents).
- **Agent Scope**: `key` (Default, scoped to current agent).
## Session State
Session state is a dict that persists across turns within a session.
Access via `tool_context.state` or instruction placeholders:
```python
# In instruction (template variable substitution)
instruction = 'Current user: {user_name}'
# In tool
def my_tool(tool_context: ToolContext):
tool_context.state['user_name'] = 'Alice'
# In callback
def before_agent(callback_context):
callback_context.state['_time'] = datetime.now().isoformat()
```
**State key conventions:**
- `app:key` -- app-level state (shared across agents)
- `key` -- agent-level state (scoped to current agent)
- `_key` -- convention for internal/framework state
- `{key?}` in instruction -- optional placeholder (empty if missing)
- `{key}` in instruction -- required placeholder (error if missing)
## Session Services
| Service | Use Case |
|---------|----------|
| `InMemorySessionService` | Local dev, testing (default) |
| `DatabaseSessionService` | Production (SQLite, PostgreSQL) |
| `VertexAiSessionService` | Vertex AI Agent Engine |
```python
from google.adk import Runner
from google.adk.sessions import InMemorySessionService
runner = Runner(
agent=root_agent,
app_name='my_app',
session_service=InMemorySessionService(),
)
```
## Artifacts
Artifacts store non-textual data (files, images) associated with sessions:
```python
from google.genai import types
# Save from tool
async def save_chart(tool_context: ToolContext):
chart_bytes = generate_chart()
part = types.Part.from_bytes(data=chart_bytes, mime_type='image/png')
version = await tool_context.save_artifact('chart.png', part)
# Load from tool
async def get_chart(tool_context: ToolContext):
part = await tool_context.load_artifact('chart.png')
return part.inline_data.data
```
## Memory Services
Long-term recall across sessions:
```python
from google.adk.memory.in_memory_memory_service import InMemoryMemoryService
runner = Runner(
agent=root_agent,
memory_service=InMemoryMemoryService(),
...
)
```
Use `load_memory` and `preload_memory` tools to access memory from
within agents.
## Common Pitfalls
- **State not persisting:** Assigning to `state` instead of mutating.
Use `tool_context.state['key'] = value` (not `state = {'key': value}`).
- **State overwritten by parallel tools:** Multiple tools modifying same
key concurrently. Use unique keys per tool, or `app:` prefix for shared
state.
@@ -0,0 +1,187 @@
# State and Events Reference
Manage shared state across workflow nodes and understand the event system.
## 📋 Agent Verification Checklist (State & Events)
Use this checklist when working with state and events:
- [ ] **State Updates**: Did you use `Event(state=...)` for state updates? (Captures delta in event history)
- [ ] **Parameter Resolution**: Are custom parameters named after keys in `ctx.state`?
- [ ] **Output Serialization**: Is `event.output` JSON-serializable? (Required for DB session services)
- [ ] **Web UI Display**: Did you use `Event(message=...)` for output meant for users?
## 💡 Quick Reference (Resolution Order)
1. **`ctx`**: Workflow `Context` object.
2. **`node_input`**: Predecessor output.
3. **Other names**: Looked up from `ctx.state[param_name]`.
## Workflow Context
Every node receives a `Context` object (when declaring a `ctx` parameter):
```python
from google.adk.agents.context import Context
def my_node(ctx: Context, node_input: str) -> str:
# Access shared state
value = ctx.state.get("key", "default")
# Write to state
ctx.state["key"] = "new_value"
# Access session info
session_id = ctx.session.id
invocation_id = ctx.invocation_id
# Get node metadata
node_path = ctx.node_path # e.g., "MyWorkflow/my_node"
run_id = ctx.run_id # this node-run's identifier
attempt = ctx.attempt_count # 1 on first attempt, ≥1 thereafter
return f"Processed: {value}"
```
## Context Properties
### Common Properties (available everywhere)
| Property | Type | Description |
|----------|------|-------------|
| `state` | `State` | Delta-aware session state (read/write like a dict) |
| `session` | `Session` | Current session (with local events merged in workflows) |
| `invocation_id` | `str` | Current invocation ID |
| `user_content` | `types.Content` | The user content that started this invocation (read-only) |
| `agent_name` | `str` | Name of the agent currently running |
| `user_id` | `str` | The user ID (read-only) |
| `run_config` | `RunConfig \| None` | Run configuration for this invocation (read-only) |
| `actions` | `EventActions` | Event actions for state/artifact deltas |
### Workflow-Only Properties
| Property | Type | Description |
| --------------- | ---------------- | ------------------------------------- |
| `node_path` | `str` | Full path of current node (e.g., |
: : : "WorkflowA/node1") :
| `run_id` | `str` | Identifier for this node-run (e.g., |
: : : `"1"`, `"2"`) :
| `attempt_count` | `int` | Retry attempt number (1 on first try) |
| `resume_inputs` | `dict[str, Any]` | Inputs for resuming (keyed by |
: : : interrupt_id) :
### Workflow-Only Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `run_node(node, node_input, *, name)` | `Any` | Execute a node dynamically (requires `rerun_on_resume=True`) |
## State Management
State is shared across all nodes in a workflow invocation. **Prefer `Event(state=...)` over `ctx.state[...] =`** for setting state:
```python
# ✅ Preferred: set state via Event (persisted in event history, replayable)
def node_a(node_input: str):
return Event(
output="done",
state={"user_data": {"name": "Alice", "score": 95}},
)
# ❌ Avoid: direct ctx.state mutation (not captured in event history)
def node_a(ctx: Context, node_input: str) -> str:
ctx.state["user_data"] = {"name": "Alice", "score": 95}
return "done"
```
**Why `Event(state=...)` is preferred:**
- State deltas are persisted in event history as `event.actions.state_delta`
- Non-resumable HITL can reconstruct state by replaying events
- Makes state changes explicit and traceable
- `ctx.state` mutations are side effects that may be lost on replay
Reading state is always done via `ctx.state`:
```python
def node_b(ctx: Context, node_input: str) -> str:
user = ctx.state["user_data"]
return f"User {user['name']} scored {user['score']}"
```
The `state` dict is stored as `event.actions.state_delta` and applied to the session.
## State as Function Parameters
FunctionNode automatically resolves parameters from state:
```python
# If ctx.state["user_name"] = "Alice" and ctx.state["threshold"] = 0.5
def my_node(node_input: str, user_name: str, threshold: float) -> str:
# user_name = "Alice" (from state)
# threshold = 0.5 (from state)
return f"{user_name}: {node_input} (threshold={threshold})"
```
Resolution order:
1. `ctx` -> Context object
2. `node_input` -> predecessor output
3. Other names -> `ctx.state[param_name]` (with auto type conversion)
4. Default values if not in state
## Event Fields
| Field | Type | Description |
|-------|------|-------------|
| `output` | `Any` | Output data passed to downstream nodes |
| `route` | `str\|bool\|int\|list` | Routing signal for conditional edges (convenience kwarg → `actions.route`) |
| `state` | `dict` (constructor only) | State delta to apply (convenience kwarg → `actions.state_delta`) |
| `message` | `ContentUnion` (constructor only) | User-facing content (convenience kwarg → `content`) |
| `content` | `types.Content` | Content for display (set directly or via `message=`) |
| `node_path` | `str` | Set by workflow (convenience kwarg → `node_info.path`) |
## Workflow Data Rules
- **`Event.output` must be JSON-serializable.** FunctionNode auto-converts Pydantic `BaseModel` returns via `model_dump()`, so returning a model is safe. But `types.Content` and other non-serializable objects will fail with SQLite/database session services.
- **`output_key` stores dicts, not BaseModel instances.** LLM agents with `output_schema` use `validate_schema()``model_dump()` internally, so `ctx.state[output_key]` is always a plain dict.
- **`ctx.state.get(key)` returns a dict.** Use dict access (`data["field"]`) or reconstruct the model (`MyModel(**data)`) if you need typed access.
```python
# Reading output_key from state — it's a dict, not a BaseModel
def use_plan(ctx: Context, node_input: Any) -> str:
plan = ctx.state.get('task_plan', {}) # dict, not TaskPlan
return plan['project_name'] # dict access
# Or reconstruct if you need typed access:
plan_model = TaskPlan(**plan)
return plan_model.project_name
```
## Content Events (User-Visible Output)
In the ADK web UI, only `event.content` is rendered — `event.output` is internal and not displayed. Emit content events for any user-facing output:
```python
# Simple text message
yield Event(message="Processing step 1...")
# Multimodal message (text + image)
from google.genai import types
yield Event(
message=[
types.Part.from_text(text="Here is the result:"),
types.Part.from_bytes(data=image_bytes, mime_type="image/png"),
]
)
# Streaming: multiple messages from same node
async def verbose_node(ctx: Context, node_input: str):
yield Event(message="Processing step 1...")
await asyncio.sleep(1.0)
yield Event(message="Processing step 2...")
yield Event(output="final result")
```
## Workflow Output
The Workflow emits its own output Event in `_finalize_workflow` after all nodes complete. Terminal nodes (nodes with no outgoing edges) have their data collected and emitted as the workflow's output. This output event has `author=workflow.name` and `node_path=workflow's own path`.
@@ -0,0 +1,275 @@
# Task Mode: Structured Delegation
Delegate structured tasks to sub-agents with typed input/output schemas.
## 📋 Agent Verification Checklist (Task Mode)
Use this checklist to verify your Task Mode configuration:
- [ ] **Mode Setting**: Did you explicitly set `mode='task'` or `mode='single_turn'` on the sub-agent?
- [ ] **Description**: Does the sub-agent have a clear `description`? (Crucial for the auto-generated tool's description)
- [ ] **Schemas**: Are `input_schema` and `output_schema` defined as Pydantic models? (If not, defaults are used)
- [ ] **Completion**: Does the sub-agent know it must call `finish_task` to return results to the coordinator?
## 💡 Quick Reference (Generated Tools)
- **`request_task_{agent_name}`**: Generated on the **coordinator** to delegate tasks.
- **`finish_task`**: Generated on the **sub-agent** to return results and complete the task.
## Overview
ADK agents support three delegation modes via the `mode` parameter on `Agent`:
| Mode | Tool Generated | User Interaction | Completion |
|------|---------------|------------------|------------|
| `chat` (default) | `transfer_to_agent` | Full conversational | Agent transfers back |
| `task` | `request_task_{name}` | Multi-turn (can chat with user) | Calls `finish_task` |
| `single_turn` | `request_task_{name}` | None (autonomous) | Calls `finish_task` |
## Imports
```python
from google.adk import Agent
from pydantic import BaseModel
```
**Note**: Task mode uses `Agent` (aliased from `LlmAgent`) from `google.adk`. Both task sub-agents and coordinators use the same `Agent` class — set `mode='task'` or `mode='single_turn'` on sub-agents.
## Task Mode (`mode='task'`)
A task agent receives structured input via `request_task_{name}`, can interact with the user for clarification, and returns structured output via `finish_task`.
### Delegation Lifecycle
1. User asks the coordinator to do something
2. Coordinator calls `request_task_{agent_name}(...)` with structured input
3. Task agent receives the input, works on it (may use tools, may chat with user)
4. Task agent calls `finish_task(...)` with structured output
5. Coordinator receives the result and responds to the user
### Example
```python
from google.adk import Agent
from pydantic import BaseModel
class ResearchInput(BaseModel):
topic: str
depth: str = 'standard'
class ResearchOutput(BaseModel):
summary: str
key_findings: str
confidence: str
def search_web(query: str) -> str:
"""Search the web for information."""
return f'Results for "{query}": ...'
def analyze_sources(sources: str) -> str:
"""Analyze and synthesize source material."""
return f'Analysis of {len(sources.split())} words complete.'
researcher = Agent(
name='researcher',
mode='task',
input_schema=ResearchInput,
output_schema=ResearchOutput,
instruction=(
'You are a research assistant. When given a topic:\n'
'1. Use search_web to find information.\n'
'2. Use analyze_sources to synthesize findings.\n'
'3. If the user asks for changes, adjust your research.\n'
'4. Call finish_task with summary, key_findings, and confidence.'
),
description='Researches topics using web search and analysis.',
tools=[search_web, analyze_sources],
)
root_agent = Agent(
name='coordinator',
model='gemini-2.5-flash',
sub_agents=[researcher],
instruction=(
'When the user asks you to research something, delegate to'
' the researcher using request_task_researcher. After the'
' researcher completes, summarize the results for the user.'
),
)
```
## Single-Turn Mode (`mode='single_turn'`)
A single-turn agent completes autonomously with no user interaction. It receives input, does its work, and returns a result.
### Example
```python
class SummaryOutput(BaseModel):
summary: str
word_count: int
key_points: str
def extract_text(url: str) -> str:
"""Extract text from a URL."""
return f'Extracted content from {url}: ...'
summarizer = Agent(
name='summarizer',
mode='single_turn',
output_schema=SummaryOutput,
instruction=(
'Summarize the document:\n'
'1. Use extract_text to get content.\n'
'2. Call finish_task with summary, word_count, key_points.\n'
'Complete autonomously without user interaction.'
),
description='Summarizes documents autonomously.',
tools=[extract_text],
)
root_agent = Agent(
name='coordinator',
model='gemini-2.5-flash',
sub_agents=[summarizer],
instruction='Delegate summarization to summarizer via request_task_summarizer.',
)
```
## Input and Output Schemas
### Custom Schemas (Pydantic Models)
Define `input_schema` and/or `output_schema` with Pydantic `BaseModel`:
```python
class TaskInput(BaseModel):
query: str
max_results: int = 10
format: str = 'text'
class TaskOutput(BaseModel):
results: str
count: int
status: str
agent = Agent(
name='worker',
mode='task',
input_schema=TaskInput, # Validates request_task_worker args
output_schema=TaskOutput, # Validates finish_task args
...
)
```
### Default Schemas
When no custom schema is provided:
**Default input** (used by `request_task_{name}`):
```python
class _DefaultTaskInput(BaseModel):
goal: str | None = None
background: str | None = None
```
**Default output** (used by `finish_task`):
```python
class _DefaultTaskOutput(BaseModel):
result: str
```
## Auto-Generated Tools
### `request_task_{agent_name}`
Auto-generated on the **coordinator** for each `mode='task'` or `mode='single_turn'` sub-agent. The tool name is `request_task_{agent.name}`.
- Parameters come from `input_schema` (or default: `goal`, `background`)
- Description includes the agent's `description` field
- Validates input against the schema before delegating
### `finish_task`
Auto-generated on the **task agent** itself. Called by the task agent when work is complete.
- Parameters come from `output_schema` (or default: `result`)
- Validates output against the schema before signaling completion
- Sets `tool_context.actions.finish_task` with a `TaskResult`
## Mixed-Mode Patterns
Combine task and single-turn agents under one coordinator:
```python
# Interactive: user can discuss options
flight_searcher = Agent(
name='flight_searcher',
mode='task',
input_schema=FlightSearchInput,
output_schema=FlightSearchOutput,
instruction='Search flights, discuss with user, then finish_task.',
description='Searches and books flights interactively.',
tools=[search_flights, book_flight],
)
# Autonomous: no user interaction
weather_checker = Agent(
name='weather_checker',
mode='single_turn',
output_schema=WeatherOutput,
instruction='Check weather and call finish_task. No user interaction.',
description='Checks weather for a destination.',
tools=[get_weather],
)
# Autonomous: no user interaction
hotel_finder = Agent(
name='hotel_finder',
mode='single_turn',
output_schema=HotelOutput,
instruction='Find hotels and call finish_task. No user interaction.',
description='Finds hotels for a destination.',
tools=[find_hotels],
)
root_agent = Agent(
name='travel_planner',
model='gemini-2.5-flash',
sub_agents=[flight_searcher, weather_checker, hotel_finder],
instruction=(
'Help users plan trips:\n'
'- request_task_weather_checker: autonomous weather check\n'
'- request_task_hotel_finder: autonomous hotel search\n'
'- request_task_flight_searcher: interactive flight booking'
),
)
```
## Key Rules
- Both task sub-agents and coordinators use `Agent` from `google.adk`
- Each sub-agent needs a `description` (used in the auto-generated tool description)
- `input_schema` and `output_schema` are optional; defaults are provided
- Sub-agents inherit model from the coordinator if not set
- `finish_task` instructions are auto-injected into the task agent's LLM context
- Single-turn agents receive an extra instruction telling them no user replies will come
## Task Mode vs Chat Mode
| Feature | Chat (`transfer_to_agent`) | Task (`request_task`) |
|---------|---------------------------|----------------------|
| Input | Free-form conversation | Structured (schema-validated) |
| Output | Free-form conversation | Structured (schema-validated) |
| Control flow | Agent decides when to transfer back | Agent calls `finish_task` |
| User interaction | Full chat | `task`: multi-turn; `single_turn`: none |
| Tool name | `transfer_to_agent` | `request_task_{name}` |
| Parallel delegation | Not supported | Supported (multiple `request_task` calls) |
## Source File Locations
| Component | File |
|-----------|------|
| Agent/LlmAgent (mode, schemas) | `src/google/adk/agents/llm_agent.py` |
| BaseLlmFlow (base flow class) | `src/google/adk/flows/llm_flows/base_llm_flow.py` |
| RequestTaskTool | `src/google/adk/agents/llm/task/_request_task_tool.py` |
| FinishTaskTool | `src/google/adk/agents/llm/task/_finish_task_tool.py` |
| TaskRequest, TaskResult | `src/google/adk/agents/llm/task/_task_models.py` |
| Task samples | `contributing/task_samples/` |
@@ -0,0 +1,315 @@
# Testing Workflow Agents Reference
Write unit tests for workflow agents using `pytest` with async support and the
public `InMemoryRunner` from `google.adk.runners`.
## Setup
```bash
# Install ADK + pytest + pytest-asyncio
pip install "google-adk>=2.0" pytest pytest-asyncio
# Or with uv
uv add "google-adk>=2.0" pytest pytest-asyncio
```
`pyproject.toml`:
```toml
[tool.pytest.ini_options]
asyncio_mode = "auto"
```
`asyncio_mode = "auto"` removes the need to mark every test with
`@pytest.mark.asyncio`; if you'd rather mark each test explicitly, omit it.
## Imports
All imports below are from the published `google-adk` package — no test-internal
helpers required.
```python
import pytest
from google.genai import types
from google.adk import Workflow
from google.adk.agents import LlmAgent
from google.adk.apps import App
from google.adk.apps.app import ResumabilityConfig
from google.adk.events import Event, RequestInput
from google.adk.runners import InMemoryRunner
```
## A small `run` helper
Tests are tidier with a helper that drives one turn and collects events:
```python
async def run(agent, text="hi", app_name="test_app"):
runner = InMemoryRunner(agent=agent, app_name=app_name)
session = await runner.session_service.create_session(
app_name=app_name, user_id="u1"
)
msg = types.Content(role="user", parts=[types.Part(text=text)])
events = []
async for event in runner.run_async(
user_id="u1", session_id=session.id, new_message=msg,
):
events.append(event)
return runner, session, events
def node_name(event):
"""Extract the node name from event.node_info.path.
e.g. 'workflow@1/step@1' -> 'step'.
"""
if not event.node_info:
return None
return event.node_info.path.split("/")[-1].split("@")[0]
```
In ADK 2.x, `event.author` is the enclosing workflow's name; the per-node
identifier lives in `event.node_info.path`. Use `node_name(event)` to filter by
the node that emitted an event.
## Basic Workflow Test
```python
async def test_simple_workflow():
def step_one(node_input: str) -> str:
return "step 1 done"
def step_two(node_input: str) -> str:
return "step 2 done"
agent = Workflow(
name="test_workflow",
edges=[
("START", step_one),
(step_one, step_two),
],
)
_, _, events = await run(agent)
final = [e for e in events if node_name(e) == "step_two" and e.output][-1]
assert final.output == "step 2 done"
```
## Testing Conditional Routing
```python
async def test_routing():
def router(node_input: str):
if "error" in node_input:
return Event(output=node_input, route="error")
return Event(output=node_input, route="success")
def success_handler(node_input: str) -> str:
return f"OK: {node_input}"
def error_handler(node_input: str) -> str:
return f"ERR: {node_input}"
agent = Workflow(
name="routing_test",
edges=[
("START", router),
(router, {"success": success_handler, "error": error_handler}),
],
)
_, _, evs_ok = await run(agent, text="all good")
assert any(node_name(e) == "success_handler" for e in evs_ok)
_, _, evs_err = await run(agent, text="error case")
assert any(node_name(e) == "error_handler" for e in evs_err)
```
## Testing HITL (Pause and Resume)
```python
async def test_hitl_workflow():
async def ask_user(ctx, node_input: str):
yield RequestInput(message="Approve?", interrupt_id="ask")
def after_approval(node_input) -> str:
return f"Approved: {node_input}"
agent = Workflow(
name="hitl_test",
edges=[
("START", ask_user),
(ask_user, after_approval),
],
)
app = App(
name="hitl_test_app",
root_agent=agent,
resumability_config=ResumabilityConfig(is_resumable=True),
)
runner = InMemoryRunner(app=app)
session = await runner.session_service.create_session(
app_name="hitl_test_app", user_id="u1"
)
# First turn: should pause with a RequestInput function call
msg = types.Content(role="user", parts=[types.Part(text="start")])
pause_events = []
async for event in runner.run_async(
user_id="u1", session_id=session.id, new_message=msg,
):
pause_events.append(event)
fc_events = [e for e in pause_events if e.get_function_calls()]
assert fc_events, "expected an interrupt function call"
fc = fc_events[-1].get_function_calls()[0]
# Resume by responding to the function call
response = types.Content(
role="user",
parts=[types.Part(function_response=types.FunctionResponse(
id=fc.id, name=fc.name, response={"result": "yes"},
))],
)
resumed = []
async for event in runner.run_async(
user_id="u1", session_id=session.id, new_message=response,
):
resumed.append(event)
final = [e for e in resumed if node_name(e) == "after_approval"][-1]
assert final.output == "Approved: yes"
```
## Testing State Updates
Prefer asserting on the post-run session's state rather than reading state
mid-flight:
```python
async def test_state_management():
def writer(node_input: str):
return Event(output=node_input, state={"counter": 1})
def reader(ctx, node_input):
return f"counter={ctx.state['counter']}"
agent = Workflow(
name="state_test",
edges=[("START", writer, reader)],
)
runner, session, events = await run(agent)
final = [e for e in events if node_name(e) == "reader" and e.output][-1]
assert final.output == "counter=1"
# Or read state directly off the session after the run
final_session = await runner.session_service.get_session(
app_name="test_app", user_id="u1", session_id=session.id
)
assert final_session.state["counter"] == 1
```
## Testing Parallel Execution
```python
from google.adk.workflow import node
async def test_parallel_worker():
def produce(node_input: str) -> list:
return [1, 2, 3]
@node(parallel_worker=True)
def double(node_input: int) -> int:
return node_input * 2
def collect(node_input: list) -> str:
return f"results: {node_input}"
agent = Workflow(
name="parallel_test",
edges=[("START", produce, double, collect)],
)
_, _, events = await run(agent)
final = [e for e in events if node_name(e) == "collect" and e.output][-1]
assert final.output == "results: [2, 4, 6]"
```
## Mocking LLM Agents
For unit tests that don't hit the real API, pass a fake `BaseLlm` to the
`LlmAgent` constructor. The framework only requires the abstract
`generate_content_async` method.
```python
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.genai import types
class FakeLlm(BaseLlm):
def __init__(self, *, responses: list[str]):
super().__init__(model="fake")
self._responses = list(responses)
async def generate_content_async(self, llm_request, stream=False):
text = self._responses.pop(0)
yield LlmResponse(content=types.Content(
role="model", parts=[types.Part(text=text)],
))
async def test_llm_agent_with_fake():
agent = LlmAgent(
name="x",
model=FakeLlm(responses=["ok"]),
instruction="Help.",
)
_, _, events = await run(agent, text="hi")
final = events[-1]
assert final.content and final.content.parts[0].text == "ok"
```
If you only need to assert call shapes, `monkeypatch` the agent's
`canonical_model.generate_content_async` with a mock instead.
## Integration tests with a real model
Tag tests that hit a real model and skip them by default:
```python
import os
import pytest
@pytest.fixture(scope="session", autouse=True)
def adk_env():
if "GOOGLE_API_KEY" not in os.environ:
pytest.skip("GOOGLE_API_KEY not set; skipping integration tests")
os.environ.setdefault("GOOGLE_GENAI_USE_VERTEXAI", "FALSE")
@pytest.mark.integration
async def test_real_model():
...
```
Then `pytest -m integration` to run them, or `pytest -m "not integration"` to
skip.
## Testing Tips
- Create a fresh `InMemoryRunner` and session per test — runners hold state
and reuse causes cross-test interference.
- Use a unique `app_name` per test (e.g. `request.node.name`) to avoid
collisions across parallel pytest workers.
- Assert on `event.node_info.path`, not `event.author`. `event.author` is the
enclosing workflow's name; `event.node_info.path` identifies the exact node
that emitted the event.
- Use `event.is_final_response()` to filter for "the agent's final message"
events.
- For workflows with a `JoinNode`, make sure every LLM agent feeding into it
has `output_schema=` set — otherwise the join buffer fails JSON
serialization in tests that use `DatabaseSessionService`.
- Run with `pytest -xvs` while iterating (`-x` stop on first failure, `-v`
verbose, `-s` show prints) to debug event flow.
@@ -0,0 +1,177 @@
# ADK Tool Catalog
## 📋 Agent Verification Checklist (Tools)
Use this checklist when creating or binding tools:
- [ ] **Python Functions**: Do they have both **type hints** and a **docstring**? (Required for schema generation)
- [ ] **Context Injection**: Is the special parameter named `tool_context` or `ctx` used for accessing state?
- [ ] **MCP Tools**: Did you verify that `pip install mcp` is run if using MCP tools?
- [ ] **Class Names**: Are you using `McpToolset` (the non-deprecated name)?
## 💡 Quick Reference (Built-in Tools)
- **Google Search**: `from google.adk.tools import google_search`
- **Load Artifacts**: `from google.adk.tools import load_artifacts`
- **Agent Transfer**: `from google.adk.tools import transfer_to_agent`
## Python Function Tools (Most Common)
Any Python function with type annotations and a docstring becomes a tool:
```python
def get_weather(city: str, unit: str = 'celsius') -> str:
"""Get the current weather for a city.
Args:
city: The city name to look up.
unit: Temperature unit, 'celsius' or 'fahrenheit'.
Returns:
A string with the weather information.
"""
return f"Sunny, 22 degrees {unit} in {city}"
root_agent = Agent(tools=[get_weather], ...)
```
**Rules:**
- Type hints required (they generate the JSON schema)
- Docstring required (becomes the tool description)
- Both sync and async functions supported
- Special parameter `tool_context: ToolContext` is auto-injected (not in schema)
## ToolContext
`ToolContext` is a backward-compatible alias for `Context`. Both work identically.
```python
from google.adk.tools.tool_context import ToolContext
async def my_tool(query: str, tool_context: ToolContext) -> str:
tool_context.state['key'] = 'value' # Session state
await tool_context.save_artifact('f.txt', part) # Save artifact
part = await tool_context.load_artifact('f.txt') # Load artifact
results = await tool_context.search_memory('q') # Search memory
return 'done'
```
## MCP Tools (Model Context Protocol)
```python
from google.adk.tools.mcp_tool.mcp_toolset import McpToolset
from google.adk.tools.mcp_tool import StdioConnectionParams
from mcp import StdioServerParameters
root_agent = Agent(
tools=[
McpToolset(
connection_params=StdioConnectionParams(
server_params=StdioServerParameters(
command='npx',
args=['-y', '@modelcontextprotocol/server-filesystem', '/path'],
),
timeout=5,
),
tool_filter=['read_file', 'list_directory'],
)
], ...
)
```
Connection types: `StdioConnectionParams`, `SseConnectionParams`,
`StreamableHTTPConnectionParams`.
**Pitfalls:** Requires `pip install mcp`. Use `McpToolset` (not deprecated
`MCPToolset`). `StdioServerParameters` is from the `mcp` package, not ADK.
## OpenAPI Tools
```python
from google.adk.tools.openapi_tool import OpenAPIToolset
toolset = OpenAPIToolset(spec_str=open('openapi.yaml').read(), spec_str_type='yaml')
root_agent = Agent(tools=[toolset], ...)
```
Also: `from google.adk.tools.openapi_tool import RestApiTool` for individual endpoints.
## Google API Tools
```python
from google.adk.tools.google_api_tool.google_api_toolsets import BigQueryToolset
bigquery = BigQueryToolset(client_id='...', client_secret='...',
tool_filter=['bigquery_datasets_list'])
root_agent = Agent(tools=[bigquery], ...)
```
## Built-in Tools
| Tool | Import |
|------|--------|
| `google_search` | `from google.adk.tools import google_search` |
| `load_artifacts` | `from google.adk.tools import load_artifacts` |
| `load_memory` | `from google.adk.tools import load_memory` |
| `exit_loop` | `from google.adk.tools import exit_loop` |
| `transfer_to_agent` | `from google.adk.tools import transfer_to_agent` |
| `get_user_choice` | `from google.adk.tools import get_user_choice` |
| `url_context` | `from google.adk.tools import url_context` |
## LongRunningFunctionTool
```python
from google.adk.tools.long_running_tool import LongRunningFunctionTool
def approve_expense(amount: float) -> dict:
"""Submit expense for approval."""
return {"status": "pending", "id": "exp-123"}
root_agent = Agent(tools=[LongRunningFunctionTool(approve_expense)], ...)
```
## Code Execution
```python
from google.adk.code_executors.built_in_code_executor import BuiltInCodeExecutor
root_agent = Agent(code_executor=BuiltInCodeExecutor(), ...)
```
Note: `code_executor` is a separate parameter from `tools`.
## Custom BaseTool
```python
from google.adk.tools.base_tool import BaseTool
from google.genai import types
class MyTool(BaseTool):
def __init__(self):
super().__init__(name='my_tool', description='Does something.')
def _get_declaration(self):
return types.FunctionDeclaration(
name=self.name, description=self.description,
parameters_json_schema={
'type': 'object',
'properties': {'param': {'type': 'string'}},
'required': ['param'],
},
)
async def run_async(self, *, args, tool_context):
return {'result': args['param']}
```
## BaseToolset (Tool Collections)
```python
from google.adk.tools.base_toolset import BaseToolset
class MyToolset(BaseToolset):
async def get_tools(self, readonly_context=None):
return [ToolA(), ToolB()]
async def process_llm_request(self, *, tool_context, llm_request):
llm_request.append_instructions(['Custom instruction'])
```
Toolsets support `tool_filter`, `tool_name_prefix`, and `process_llm_request`.
+25
View File
@@ -0,0 +1,25 @@
---
name: adk-architecture
description: ADK architectural knowledge — graph orchestration, resumption, execution flow, node contracts, observability, and LLM context orchestration. 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. Triggers on "how does X work", "design of", "architecture of", "event flow", "resumption state", "checkpoint", "BaseNode", "NodeRunner".
---
# ADK Architecture Guide
## Core Interfaces (references/interfaces/)
- [BaseNode](references/interfaces/base-node.md) — node contract, output/streaming, state/routing, HITL, configuration
- [Workflow](references/interfaces/workflow.md) — graph orchestration, dynamic nodes (tracking/dedup/resume), transitive dynamic nodes, interrupt propagation, design rules for node authors
- [Runner](references/interfaces/runner.md) — The public interface for executing workflows and agents. Documents entrance methods `run` and `run_async`.
- [Agent](references/interfaces/agent.md) — Blueprint defining identity, instructions, and tools. Documents that `run` is the preferred entrance method.
- [BaseAgent](references/interfaces/base-agent.md) — Base class for all agents. Defines the contract for subclassing with `_run_impl` as the primary override point.
- [Event](references/interfaces/event.md) — Core data structure for state reconstruction and communication. Represents a conversation turn, action, and state lifecycle immutability.
## Key Principles (references/principles/)
- [API Principles](references/principles/api-principles.md) — stability, backward compatibility, and self-containment. Use when making design choices that affect the public API surface.
## Runtime Knowledge (references/architecture/)
- [Context](references/architecture/context.md) — 1:1 node-context mapping, InvocationContext singleton, property reference
- [NodeRunner](references/architecture/node-runner.md) — two communication channels, execution flow, output delegation. Internal runtime details.
- [Runner Roles](references/architecture/runner-roles.md) — Runner vs NodeRunner vs Workflow separation. Explains why they are separate to avoid deadlocks.
- [Checkpoint and Resume](references/architecture/checkpoint-resume.md) — HITL lifecycle, `rerun_on_resume`, `run_id`
- [Observability](references/architecture/observability.md) — span-on-Context design, NodeRunner integration, correlated logs, metrics
- [LLM Context Orchestration](references/architecture/llm-context-orchestration.md) — relationship between events and LLM context, task delegation translation, branch isolation. Use when modifying event processing, context preparation for LLMs, or debugging context pollution issues.
@@ -0,0 +1,69 @@
# Checkpoint and Resume Lifecycle
HITL (Human-in-the-Loop) follows this pattern:
1. **Interrupt**: Node yields an event with `long_running_tool_ids`.
Each ancestor propagates the interrupt upward via `ctx.interrupt_ids`.
2. **Persist**: Only the leaf node's interrupt event is persisted to
session. Workflow sets `ctx._interrupt_ids` directly (no internal
event needed).
3. **Resume**: User sends a `FunctionResponse` message. The Runner
scans session events to find the matching `invocation_id`, then
reconstructs node state from persisted events.
4. **Continue**: The interrupted node receives the FR and continues
execution. Downstream nodes receive the resumed node's output.
## run_id on resume
Resumed nodes reuse the same `run_id` from the original
execution. From the node's perspective, the execution never paused
— events before and after the resume share the same run_id.
Fresh dispatches (first run, loop re-trigger) get a new run_id.
## Resume behavior by `rerun_on_resume`
A node with multiple interrupt IDs may receive partial FRs (only
some resolved). The behavior depends on `rerun_on_resume`:
**`rerun_on_resume=True`** (Workflow, orchestration nodes):
| FRs received | Status | Behavior |
|---|---|---|
| Partial | PENDING | Re-execute immediately with partial `resume_inputs`. Node handles remaining interrupts internally (e.g., Workflow dispatches resolved children, keeps unresolved as WAITING). |
| All | PENDING | Re-execute with all `resume_inputs`. |
This is critical for Workflow — when one child's FR arrives, it
re-runs immediately to dispatch that resolved child. It doesn't
wait for all children's FRs.
**`rerun_on_resume=False`** (leaf nodes, simple HITL):
| FRs received | Status | Behavior |
|---|---|---|
| Partial | WAITING | Stay waiting. Need all FRs. |
| All | COMPLETED | Auto-complete. Output = aggregated `resolved_responses`. No re-execution. |
## Resume with prior output and interrupts
A node can produce output AND interrupt in the same execution (e.g.,
a Workflow where child A completes with output and child B interrupts).
On resume:
- Some interrupt IDs are resolved (provided in `resume_inputs`)
- Remaining interrupt IDs carry forward via `prior_interrupt_ids`
- Prior output carries forward via `prior_output`
- NodeRunner pre-populates ctx with these values before re-executing
```python
runner = NodeRunner(
node=node, parent_ctx=ctx,
run_id=prior_run_id, # reuse
prior_output=cached_output,
prior_interrupt_ids={'fc-2'}, # still unresolved
)
child_ctx = await runner.run(
node_input=input,
resume_inputs={'fc-1': response},
)
```
@@ -0,0 +1,104 @@
# Context
## Architecture
The runtime uses two scoping objects:
- **InvocationContext** — singleton per invocation. Holds shared
state (session, services, event queue) accessible by all nodes.
Pydantic model at `agents/invocation_context.py`.
- **Context** — one per node execution. Holds per-node results
(output, route, interrupt_ids) and provides the API surface for
node code. At `agents/context.py`.
Every Context holds a reference to the same InvocationContext
(`_invocation_context`). Service access (artifacts, memory, auth)
is delegated through it.
```
Root Context ← created by Runner from IC
└── Context [runner.node] ← the root node (e.g., Workflow)
├── Context [child_a] ← child node A
└── Context [child_b] ← child node B
└── Context [grandchild] ← nested child
```
The Runner creates `root_ctx = Context(ic)` as the tree root and
passes it as `parent_ctx` to `NodeRunner(node=self.node)`. The
root Context has no node_path or run_id — it exists solely
as the parent for the Runner's root node. All Contexts in the tree
share the same InvocationContext singleton.
InvocationContext contents:
- `session`, `agent`, `user_content`
- `invocation_id`, `app_name`, `user_id`
- Services: `artifact_service`, `memory_service`, `credential_service`
- `run_config`, `live_request_queue`
- `process_queue` — shared event queue consumed by the main loop
## 1:1 node-context mapping
Every node execution gets its own Context instance. The relationship
is strictly 1:1: one node, one Context. The Context tree mirrors the
node execution tree.
**NodeRunner** creates the child Context from the parent's Context
via `_create_child_context()`. The child inherits:
- `_invocation_context` — same singleton (shared session, services)
- `node_path` — parent path + node name (e.g., `wf/child_a`)
- `run_id` — unique per execution (reused on resume)
- `event_author` — inherited from parent
- `schedule_dynamic_node_internal` — inherited from parent
The child does NOT inherit output, route, or interrupt_ids — those
are per-execution results, starting fresh (unless resume carries
forward `prior_output` / `prior_interrupt_ids`).
## Node result properties
These properties on Context are the primary mechanism for
communicating results between nodes:
- **`ctx.output`** — the node's result value. Set once per
execution. Can be set via `yield value` (framework sets it) or
`ctx.output = X` directly. Second write raises `ValueError`.
- **`ctx.route`** — routing value for conditional edges. Set
independently of output. Workflow-specific.
- **`ctx.interrupt_ids`** — accumulated interrupt IDs. Read-only
for user code. Set by framework when node yields an Event with
`long_running_tool_ids`.
Output and interrupts can coexist — the orchestrator's `_finalize`
decides what to propagate. The orchestrator reads these properties
after the child node finishes.
## Class hierarchy
```
ReadonlyContext (agents/readonly_context.py)
└── Context (agents/context.py)
```
**ReadonlyContext** — read-only view used in callbacks and plugins:
- `user_content`, `invocation_id`, `agent_name`
- `state` (returns `MappingProxyType` — immutable view)
- `session`, `user_id`, `run_config`
**Context(ReadonlyContext)** — full read-write context for node
execution. Extends ReadonlyContext with mutable state, node results,
workflow metadata, and service methods. See property reference below.
## Property reference
| Category | Properties |
|---|---|
| State & actions | `state` (mutable `State`), `actions` (EventActions) |
| Node results | `output`, `route`, `interrupt_ids` (read-only) |
| Workflow | `node_path`, `run_id`, `triggered_by`, `in_nodes`, `resume_inputs`, `retry_count`, `event_author` |
| Methods | `run_node()`, `get_next_child_run_id()` |
| Artifacts | `load_artifact()`, `save_artifact()`, `list_artifacts()` |
| Memory | `search_memory()`, `add_session_to_memory()`, `add_events_to_memory()`, `add_memory()` |
| Auth | `request_credential()`, `load_credential()`, `save_credential()` |
| Tools | `request_confirmation()`, `function_call_id` |
@@ -0,0 +1,42 @@
# LLM Context Orchestration from Events
## Core Principle
In ADK, there is a clear distinction between the **Event Stream** and the **LLM Context**:
- **Events are the Ground Truth**: They are immutable records of what has happened in a session (user messages, model responses, tool calls, results). They serve as the audit log and persistence state.
- **LLM Context is an Orchestrated View**: The context passed to an LLM is not merely a dump of the raw event log. It is a carefully orchestrated view, filtered and transformed to match the specific role, task, and branch of the agent currently executing.
## Orchestration Strategies
The framework orchestrates the translation of events into LLM context using several strategies:
### 1. Task Delegation Translation
When a coordinator agent delegates a task to a sub-agent (Task Agent) via a tool call:
- **Source Event**: Coordinator calls a tool like `request_task_<sub_agent_name>(args...)`.
- **Orchestrated Context**:
- The arguments in the `request_task_<sub_agent_name>` tool call are extracted and placed in the **System Instruction (SI)** or treated as the core instruction for the sub-agent.
- The first user message presented to the sub-agent is synthesized to represent the goal (e.g., "Finish task of [sub_agent_name] with arguments [args]").
- **Goal**: Isolate the sub-agent from the coordinator's full history and give it a crisp, clear starting point.
### 2. Branch Isolation
In complex workflows with parallel execution:
- **Source Events**: Events from all nodes and branches are stored in the same session chronologically.
- **Orchestrated Context**: The framework filters events by `branch` (e.g., `node:path.name`). An agent only sees events that belong to its own execution path.
- **Goal**: Prevent cross-node event pollution and ensure deterministic behavior in isolated tasks.
### 3. History Trimming and Compaction
To prevent context window overflow and stale instruction loops:
- **Source Events**: A long history of retries, tool calls, and interactions.
- **Orchestrated Context**: The framework may trim older events or summarize them (event compaction). In task mode, it might keep only the essential setup events, ignoring stale retry loops that would otherwise confuse the LLM.
- **Goal**: Maintain a focused and efficient context window for the LLM.
## Summary
The relationship is one of **Source vs. View**. Events are the source of truth for the session, while LLM context is a highly orchestrated view of that truth, tailored for the active agent.
@@ -0,0 +1,76 @@
# NodeRunner
NodeRunner is the per-node executor. It drives `BaseNode.run()`,
creates the child Context, enriches events, and writes results
to ctx.
## Two communication channels
The runtime has two distinct channels for data flow:
- **Context** — parent ↔ child communication. Output, route, state,
resume_inputs, and interrupt_ids flow through ctx. The orchestrator
reads ctx after the child completes to decide what to do next.
- **Event** — persistence and streaming. Events are appended to the
session and streamed to the caller. They carry message, state
deltas, function calls, and interrupt markers.
A node writes to **ctx** to communicate with its parent. A node
yields **Events** to persist data and stream messages to the user.
## Execution flow
```
Orchestrator
├─ NodeRunner(node=child, parent_ctx=ctx)
│ │
│ ├─ _create_child_context() → child Context
│ ├─ _execute_node() → iterate node.run()
│ │ ├─ _track_event_in_context() → write to ctx
│ │ └─ _enqueue_event() → enrich + persist
│ ├─ _flush_output_and_deltas() → emit deferred output
│ └─ return child ctx
└─ reads ctx.output, ctx.route, ctx.interrupt_ids
```
1. **Create child Context** — inherits `_invocation_context` (shared
singleton), builds `node_path` from parent, assigns `run_id`.
2. **Iterate `node.run()`** — for each yielded Event:
**Track in context**`_track_event_in_context` writes output,
route, and interrupt_ids from the event to ctx (source of truth).
**Enrich**`_enrich_event` stamps metadata before persistence:
- `event.author` — node name (or `event_author` override)
- `event.invocation_id` — from InvocationContext
- `event.node_info.path` — full path (e.g., `wf/child_a`)
- `event.node_info.run_id` — unique per execution
- `event.node_info.output_for` — ancestor paths when
`use_as_output=True`
**Flush deltas** — for non-partial events, `_flush_deltas` moves
pending state/artifact deltas from `ctx.actions` onto the event
before enqueueing.
**Enqueue**`ic.enqueue_event` puts the event on the shared
process queue for session persistence.
3. **Flush deferred output** — if `ctx.output` was set directly
(not via yield), `_flush_output_and_deltas` emits the output
Event after `_run_impl` returns. Bundles any remaining
state/artifact deltas onto the same Event.
4. **Return child ctx** — the orchestrator reads `ctx.output`,
`ctx.route`, and `ctx.interrupt_ids`.
## Output delegation (`use_as_output`)
When a child is scheduled with `use_as_output=True`, its output
Event also counts as the parent's output. NodeRunner:
- Sets `ctx._output_delegated = True` on the parent
- Skips emitting the parent's own output Event
- Stamps `event.node_info.output_for` with ancestor paths
@@ -0,0 +1,164 @@
# Observability
## Design: span on Context
Each Context carries a `_span` field. Since Context forms a 1:1
parent-child tree with node executions (see [Context](context.md)),
span hierarchy follows naturally — no separate span management
needed.
```
Root Context._span (invocation) ← Runner sets this
└── ctx[workflow]._span ← NodeRunner creates
├── ctx[child_a]._span ← NodeRunner creates
│ ├── (call_llm span) ← auto-parented
│ └── (execute_tool span) ← auto-parented
├── ctx[child_b]._span ← NodeRunner creates
│ └── ctx[grandchild]._span ← nested
└── ctx[child_c]._span ← ctx.run_node()
```
**Runner** creates `root_ctx` and the `invocation` span, storing
it as `root_ctx._span`. This becomes the parent for all node spans.
**NodeRunner** creates each node's span, explicitly parented to
`parent_ctx._span`, stores it on `child_ctx._span`, and closes it
before returning (see [NodeRunner](node-runner.md) for the
execution flow).
**Always use `ctx._span` explicitly** — never rely on OTel's
implicit "current span" context. In a concurrent asyncio.Task
runtime, implicit context can be unreliable across concurrent
nodes. All tracing operations (attributes, logs, child spans)
should go through `ctx._span`. When attaching or detaching OTel context explicitly (e.g., using `context.attach()` and `context.detach()`), **always pair them inside a `try...finally` block** to prevent context leaks across requests.
**Span lifecycle:**
1. `NodeRunner.run()` creates span via `tracer.start_span()`,
parented to `parent_ctx._span`, stored on `ctx._span`
2. Node executes; all tracing goes through `ctx._span` explicitly
3. `NodeRunner.run()` calls `ctx._span.end()` before returning
4. `BatchSpanProcessor` buffers ended spans, exports periodically
5. `OTLPSpanExporter` sends batch to the OTLP endpoint
**Interrupted nodes:** Span ends immediately when NodeRunner
returns — not left open waiting for resume. Otherwise the span
would be invisible to the backend until resume (which could be
minutes, hours, or never). The resumed execution starts a fresh
span in a new `Runner.run_async()` call (same invocation_id,
different trace — possibly on a different server).
## NodeRunner integration
**Context changes** — add `_span` field:
```python
class Context(ReadonlyContext):
_span: Span | None = None
```
**NodeRunner.run():**
**NodeRunner.run() lifecycle:**
1. Create child ctx
2. Create span, parented to `parent_ctx._span`
3. Store on `ctx._span`
4. Set node attributes (name, path, run_id, type)
5. Execute node
- Node can add custom attributes to `ctx._span` during
execution (e.g., SingleAgentReactNode adds
`gen_ai.agent.name`, `gen_ai.request_model`)
- On interrupt: mark span `node.interrupted = True`
- On error: set span status `ERROR`, record exception
6. Set result attributes (has_output, interrupted, resumed)
7. **Close span** (`ctx._span.end()`) — always, even on interrupt
8. Return ctx
Key points:
- Use `tracer.start_span()` with explicit parent context from
`parent_ctx._span` — never rely on implicit OTel context in
concurrent async code
- Span always ends before `run()` returns, even on interrupt
## Span attributes and semantic conventions
Set at span creation (available for sampling decisions):
| Attribute | Source | Example |
|---|---|---|
| `node.name` | `self._node.name` | `"call_llm"` |
| `node.path` | `ctx.node_path` | `"wf/child_a"` |
| `node.run_id` | `self._run_id` | `"child_a_abc123"` |
| `node.type` | `type(self._node).__name__` | `"CallLlmNode"` |
Set after execution (result attributes):
| Attribute | Source | Example |
|---|---|---|
| `node.has_output` | `ctx.output is not None` | `true` |
| `node.interrupted` | `bool(ctx.interrupt_ids)` | `false` |
| `node.resumed` | `bool(resume_inputs)` | `false` |
GenAI semantic conventions for node spans:
- `gen_ai.operation.name` = `"invoke_agent"` for agent nodes
- `gen_ai.operation.name` = `"execute_tool"` for tool nodes
- `gen_ai.agent.name`, `gen_ai.tool.name` as appropriate
- Span kind: `INTERNAL` (in-process orchestration)
## Correlated logs
Use the OTel Logs API for point-in-time occurrences within a
node's span. Context provides `emit_log()` for better DX —
wraps `set_span_in_context(self._span)` internally so callers
don't manage OTel context:
```python
# On Context:
def emit_log(self, body: str, **attributes):
span_ctx = set_span_in_context(self._span)
otel_logger.emit(
LogRecord(body=body, attributes=attributes),
context=span_ctx,
)
# Usage:
ctx.emit_log('node.event.yielded',
has_output=event.output is not None,
has_message=event.content is not None,
)
```
## Python logging
Use the `google_adk` logger namespace:
| Level | What to log |
|---|---|
| `DEBUG` | Node started, node completed, event enqueued |
| `INFO` | Node interrupted, node resumed, dynamic node scheduled |
| `WARNING` | Node timeout, retry triggered |
| `ERROR` | Node failed, unhandled exception |
```python
logger = logging.getLogger("google_adk." + __name__)
logger.debug(
'Node %s started (run_id=%s, path=%s)',
node.name, run_id, ctx.node_path,
)
```
Use `%`-style formatting (lazy evaluation) for logging, not
f-strings.
## Metrics (future)
| Metric | Type | Description |
|---|---|---|
| `node.execution.duration` | Histogram | Per node type |
| `node.execution.count` | Counter | Per node type and status |
| `node.interrupt.count` | Counter | HITL interrupts |
| `node.resume.count` | Counter | Resumed executions |
| `workflow.active_nodes` | UpDownCounter | Currently executing |
@@ -0,0 +1,12 @@
# Runner vs NodeRunner vs Workflow
These three are deliberately separate:
- **Runner** = lifecycle orchestrator (InvocationContext, session,
plugins, invocation boundaries)
- **NodeRunner** = task scheduler (asyncio tasks, node execution,
completions)
- **Workflow** = graph engine (edges, traversal, node sequencing)
Merging Runner and NodeRunner would deadlock on nested workflows
(inner workflow's NodeRunner would block the outer's Runner).
@@ -0,0 +1,38 @@
# Agent
The `Agent` (represented by `BaseAgent` in code) is a public interface in ADK that serves as a blueprint defining identity, instructions, and tools for an agentic entity. It inherits from `BaseNode` and can be part of a larger workflow or act as a standalone agent.
## Key Characteristics
- **Name**: Unique identifier within the agent tree. Must be a valid Python identifier and cannot be "user".
- **Description**: Capability description used by the model for delegation.
- **Sub-agents**: Support for hierarchical agent structures.
- **Callbacks**: Supports `before_agent_callback` and `after_agent_callback` for intercepting lifecycle events.
## Entrance Methods
> [!IMPORTANT]
> Since agents now extend `BaseNode`, the original `run_async` entrance method is considered **deprecated**. Developers should rely on the new `run` method from `BaseNode` to execute agents as workflow nodes.
### `run` (Preferred Entrance)
The method inherited from `BaseNode` to execute the agent.
### `run_async` (Deprecated)
Legacy entry method to run an agent via text-based conversation.
**Arguments:**
- `parent_context`: `InvocationContext`, the invocation context of the parent agent.
**Yields:**
- `Event`: The events generated by the agent.
### `run_live`
Entry method to run an agent via video/audio-based conversation.
**Arguments:**
- `parent_context`: `InvocationContext`, the invocation context of the parent agent.
**Yields:**
- `Event`: The events generated by the agent.
### `from_config`
Class method to create an agent from a configuration object.
@@ -0,0 +1,31 @@
# BaseAgent
`BaseAgent` is the base class for all agents in the ADK. Developers subclass `BaseAgent` to create custom agentic entities. It inherits from `BaseNode` and provides the core structure and lifecycle management for agents.
## Core Contract for Subclasses
> [!IMPORTANT]
> Since agents now extend `BaseNode`, the original `run_async` entrance method is considered **deprecated**. Developers should rely on the new `run` method from `BaseNode` and use `_run_impl` as the primary override point for custom logic.
When creating a custom agent by subclassing `BaseAgent`, you should focus on the following:
### `_run_impl` (Preferred Override Point)
Core logic to run the agent as a workflow node.
**Arguments:**
- `ctx`: `Context`, the node execution context.
- `node_input`: `Any`, the input to the node.
**Yields:**
- The results generated by the agent.
### Legacy Methods (Deprecated for Node Execution)
The following methods were used for text and live conversations but are being superseded by the node-based execution model:
- `_run_async_impl`: Core logic for text-based conversation.
- `_run_live_impl`: Core logic for live conversation.
## Key Attributes to Configure
- **`name`**: The agent's name. Must be a valid Python identifier and unique within the agent tree. Cannot be "user".
- **`description`**: A description of the agent's capability, used by the model for delegation choices.
- **`sub_agents`**: A list of child agents to support hierarchical delegation.
@@ -0,0 +1,137 @@
# BaseNode
BaseNode is the primitive unit of execution in the workflow runtime.
Every computation — LLM calls, tool execution, orchestration — is
a node. It is a Pydantic `BaseModel` subclass.
## The node contract
Every node follows a two-method pattern:
- `run()` is `@final` — normalizes yields to Events. Never override.
- `_run_impl()` is the extension point — subclasses implement their
logic here as an async generator.
```python
class MyNode(BaseNode):
async def _run_impl(self, *, ctx, node_input):
result = do_work(node_input)
yield result # becomes Event(output=result)
```
**Why this split:** `run()` guarantees consistent normalization
regardless of what the subclass does. The subclass only thinks
about its domain logic.
**Normalization rules** (`run()` applies these to each yield):
- `None` → skipped
- `Event` → pass through
- `RequestInput` → interrupt Event
- any other value → `Event(output=value)`
**Generator conventions:**
A node can yield three types of data:
- **Output** — the node's result value. Flows between nodes
(parent reads `ctx.output` after child completes). At most one
per execution (second raises `ValueError`).
- **Message** — user-visible content streamed to the end user
(e.g., progress text, partial responses). Multiple allowed.
- **Route** — Workflow-specific concept. Triggers conditional
edges in the graph. Set via `ctx.route` or `event.actions.route`.
Additional rules:
- Yielding nothing produces no output event
- `yield None` is silently skipped
A custom node interacts with the runtime through two arguments:
- **`ctx`** (Context) — communicate results to the parent node
- **`node_input`** — data passed by the parent/orchestrator
## Output and streaming
Three ways to produce output (pick one per execution):
```python
# 1. Yield a value (most common)
async def _run_impl(self, *, ctx, node_input):
yield compute(node_input)
# 2. Set ctx.output directly
async def _run_impl(self, *, ctx, node_input):
ctx.output = compute(node_input)
return
yield # generator contract
# 3. Yield an Event with output
async def _run_impl(self, *, ctx, node_input):
yield Event(output=compute(node_input))
```
A second output raises `ValueError` — at most one per execution.
**Streaming messages** — yield Events with `message` to send
user-visible text (`message` is an alias for `content` on Event):
```python
async def _run_impl(self, *, ctx, node_input):
yield Event(message='working...')
yield final_result # this is the output
```
## State and routing
**Mutating state:**
```python
async def _run_impl(self, *, ctx, node_input):
ctx.state['key'] = 'value' # recorded as state_delta
yield result
```
**Setting route for conditional edges:**
```python
async def _run_impl(self, *, ctx, node_input):
ctx.route = 'approve' if score > 0.8 else 'reject'
yield node_input
```
## Advanced: child nodes and HITL
**Running child nodes** via `ctx.run_node()`:
```python
async def _run_impl(self, *, ctx, node_input):
child_result = await ctx.run_node(some_node, node_input)
yield f'child said: {child_result}'
```
Requires `rerun_on_resume = True` on the calling node.
**Requesting interrupt (HITL):**
```python
async def _run_impl(self, *, ctx, node_input):
if ctx.resume_inputs and 'fc-1' in ctx.resume_inputs:
yield f'approved: {ctx.resume_inputs["fc-1"]}'
return
yield Event(long_running_tool_ids={'fc-1'})
```
## Configuration reference
| Field | Type | Default | Purpose |
|---|---|---|---|
| `name` | `str` | required | Unique identifier |
| `description` | `str` | `''` | Human-readable description |
| `rerun_on_resume` | `bool` | `False` | Re-execute on resume (required for `ctx.run_node()`) |
| `wait_for_output` | `bool` | `False` | Stay WAITING until output is yielded (for join nodes) |
| `retry_config` | `RetryConfig \| None` | `None` | Retry on failure |
| `timeout` | `float \| None` | `None` | Max execution time in seconds |
| `input_schema` | `SchemaType \| None` | `None` | Validate/coerce input data |
| `output_schema` | `SchemaType \| None` | `None` | Validate/coerce output data |
@@ -0,0 +1,30 @@
# Event
The `Event` class represents a single event in the conversation history or workflow execution in the ADK. It is the core data structure used for state reconstruction, communication, and persistence.
## Purpose
- Stores conversation content between users and agents.
- Captures actions taken by agents (e.g., function calls, function responses, state updates).
- Holds metadata for workflow nodes, such as execution paths and run IDs.
## Key Fields
- **`invocation_id`**: The ID of the invocation this event belongs to. Non-empty before appending to a session.
- **`author`**: 'user' or the name of the agent, indicating who created the event.
- **`content`**: The actual content of the message (text, parts, etc.), inheriting from `LlmResponse`.
- **`actions`**: `EventActions` containing function calls, responses, or state changes.
- **`output`**: Generic data output from a workflow node.
- **`node_info`**: `NodeInfo` containing the execution path in the workflow (e.g., "A/B").
- **`branch`**: Used for branch-aware isolation when peer sub-agents shouldn't see each other's history.
- **`id`**: Unique identifier for the event.
- **`timestamp`**: The timestamp of the event.
## Methods of Interest
- `get_function_calls()`: Returns function calls in the event.
- `get_function_responses()`: Returns function responses in the event.
- `is_final_response()`: Returns whether the event is the final response of an agent.
## State Lifecycle & Immutability
- **Event Immutability**: Event history is immutable. Never assume that events are mutated or cleared after they are saved to a session.
- **Signal & Action Persistence**: When checking if a signal or action is "pending" versus "resolved", do not rely on events being modified in place.
- **Compaction Side-Effects**: Be aware that storing stateful flags on events (such as requested actions or transient status) can have permanent unintended effects on background compaction when those events age but remain in history.
@@ -0,0 +1,35 @@
# Runner
The `Runner` is the public interface for executing agents and workflows in ADK. It manages the execution lifecycle, handling message processing, event generation, and interaction with services like artifacts, sessions, and memory.
## Entrance Methods
### `run_async`
This is the main asynchronous entry method to run the agent in the runner. It should be used for production usage.
**Key Features:**
- Supports event compaction if enabled in configuration.
- Does not block subsequent concurrent calls for new user queries.
- Yields events as they are generated.
**Arguments:**
- `user_id`: The user ID of the session.
- `session_id`: The session ID of the session.
- `invocation_id`: Optional, set to resume an interrupted invocation.
- `new_message`: A new message to append to the session.
- `state_delta`: Optional state changes to apply to the session.
- `run_config`: The run config for the agent.
- `yield_user_message`: If True, yields the user message event before agent/node events.
### `run`
This is a synchronous entry point provided for local testing and convenience purposes.
**Key Features:**
- Runs the asynchronous execution in a background thread and re-yields events.
- Production usage should prefer `run_async`.
**Arguments:**
- `user_id`: The user ID of the session.
- `session_id`: The session ID of the session.
- `new_message`: A new message to append to the session.
- `run_config`: The run config for the agent.
@@ -0,0 +1,326 @@
# Workflow
Workflow is a graph-based orchestration node. It extends BaseNode
and implements `_run_impl()` as a scheduling loop that drives static
graph nodes and tracks dynamic nodes spawned by `ctx.run_node()`.
## Two kinds of child nodes
Workflow manages two kinds of child nodes:
- **Static (graph) nodes** — declared in `edges`, compiled into a
`WorkflowGraph`. Scheduled by the orchestration loop via triggers
and `asyncio.Task`s. Tracked in `_LoopState.nodes` by node name.
- **Dynamic nodes** — spawned at runtime via `ctx.run_node()` from
inside a graph node's `_run_impl`. Tracked in
`_LoopState.dynamic_nodes` by full `node_path`. Managed by
`DynamicNodeScheduler`.
Static and dynamic nodes share the same `_LoopState.interrupt_ids`
set, so the Workflow sees a unified view of all pending interrupts.
## Implementing a graph node
A graph node is a regular BaseNode placed in a Workflow's edges.
The Workflow wraps it in a NodeRunner, creates a child Context, and
reads `ctx.output`, `ctx.route`, and `ctx.interrupt_ids` after it
completes.
**Output** — two paths. At most one per execution. The Workflow
reads the output to pass downstream.
```python
# Yield (persisted immediately)
async def _run_impl(self, *, ctx, node_input):
yield compute(node_input)
# ctx (deferred until node end)
async def _run_impl(self, *, ctx, node_input):
ctx.output = compute(node_input)
return
yield
```
**Routing** — two paths. The Workflow uses the route to select
conditional edges.
```python
# Yield (persisted immediately)
async def _run_impl(self, *, ctx, node_input):
yield Event(route='approve' if node_input > 0.8 else 'reject')
# ctx (deferred until node end)
async def _run_impl(self, *, ctx, node_input):
ctx.route = 'approve' if node_input > 0.8 else 'reject'
yield node_input
```
**State** — two paths. `ctx.state` deltas are flushed onto the next
yielded Event, or a final Event at node end.
```python
# Yield (persisted immediately)
async def _run_impl(self, *, ctx, node_input):
yield Event(state={'count': 1})
# ctx (flushed onto next/final Event)
async def _run_impl(self, *, ctx, node_input):
ctx.state['count'] = 1
yield result
```
**Interrupts** — yield only (`ctx.interrupt_ids` is read-only). The
Workflow marks the node WAITING and propagates the interrupt IDs
upward. On resume, if `rerun_on_resume=True` (default for Workflow),
the node is re-executed with `ctx.resume_inputs` populated.
```python
async def _run_impl(self, *, ctx, node_input):
if ctx.resume_inputs and 'fc-1' in ctx.resume_inputs:
yield f'approved: {ctx.resume_inputs["fc-1"]}'
return
yield Event(long_running_tool_ids={'fc-1'})
```
## Dynamic nodes via ctx.run_node()
A graph node can spawn child nodes at runtime:
```python
class Orchestrator(BaseNode):
rerun_on_resume: bool = True # required
async def _run_impl(self, *, ctx, node_input):
result = await ctx.run_node(some_node, input_data)
yield f'child returned: {result}'
```
### Requirements
- The calling node **must** have `rerun_on_resume = True`. Without
this, the Workflow cannot re-execute the node on resume to let it
re-acquire its dynamic children's results.
### Tracking
Dynamic nodes are tracked by **full node_path**, not by name alone.
The path is `parent_path/child_name`:
```
wf/graph_node_a/dynamic_child ← dynamic node under graph_node_a
wf/graph_node_a/dynamic_child/inner ← transitive dynamic node
```
The `child_name` comes from either:
- The `name` parameter on `ctx.run_node(node, name='explicit')`
- The node's own `name` field (default)
Each unique `node_path` is tracked exactly once in
`_LoopState.dynamic_nodes`. This enables:
- **Dedup** — if the same path is encountered again (after resume),
the cached output is returned without re-execution.
- **Resume** — if the node was interrupted, its state is
reconstructed from session events via lazy scan.
### Dedup and resume protocol (DynamicNodeScheduler)
When `ctx.run_node()` is called, the scheduler checks three cases:
1. **Fresh** — no prior events for this `node_path`. Execute via
NodeRunner, record output or interrupts in `_LoopState`.
2. **Completed** — prior events show the node produced output.
Return cached output immediately. No re-execution.
3. **Waiting** — prior events show the node was interrupted:
- Unresolved interrupts → propagate interrupt IDs to the caller
(via `_LoopState.interrupt_ids`). The caller raises
`NodeInterruptedError`.
- All resolved → re-execute with `resume_inputs` from the
resolved function responses.
State reconstruction is **lazy**: the scheduler scans session events
only on the first `ctx.run_node()` call for a given path, not
upfront. This avoids scanning for dynamic nodes that won't be
re-invoked.
### Interrupt propagation
When a dynamic child interrupts:
1. `DynamicNodeScheduler._record_result` sets the child's status
to WAITING and adds its interrupt IDs to
`_LoopState.interrupt_ids`.
2. `ctx.run_node()` checks `child_ctx.interrupt_ids`. If non-empty,
it propagates them to the calling node's `ctx._interrupt_ids`
and raises `NodeInterruptedError`.
3. NodeRunner catches `NodeInterruptedError` in `_execute_node` and
records the interrupt on the calling node's Context.
4. The Workflow's `_handle_completion` sees the interrupt and marks
the graph node as WAITING.
On resume, the Workflow re-executes the graph node (because
`rerun_on_resume=True`). The graph node calls `ctx.run_node()`
again, which hits the scheduler. The scheduler lazily scans events,
finds the resolved FR, and either returns cached output or
re-executes the dynamic child with `resume_inputs`.
### Output delegation (use_as_output)
`ctx.run_node(node, use_as_output=True)` makes the dynamic child's
output count as the calling node's output:
```python
class Delegator(BaseNode):
rerun_on_resume: bool = True
async def _run_impl(self, *, ctx, node_input):
# child's output becomes this node's output
await ctx.run_node(worker, node_input, use_as_output=True)
```
- Sets `ctx._output_delegated = True` on the parent
- NodeRunner stamps `event.node_info.output_for` with ancestor paths
- Only one `use_as_output=True` per execution (second raises
`ValueError`)
## Dynamic nodes from dynamic nodes (transitive)
A dynamic node can itself call `ctx.run_node()`, creating a
transitive chain:
```python
class Outer(BaseNode):
rerun_on_resume: bool = True
async def _run_impl(self, *, ctx, node_input):
result = await ctx.run_node(Inner(name='inner'), 'data')
yield result
class Inner(BaseNode):
rerun_on_resume: bool = True
async def _run_impl(self, *, ctx, node_input):
sub = await ctx.run_node(Leaf(name='leaf'), node_input)
yield f'inner got: {sub}'
```
This works because:
- All dynamic nodes in the subtree are tracked by the **same**
enclosing Workflow. The scheduler is inherited down the Context
tree automatically.
- Each level gets a unique `node_path`:
`wf/graph_node/outer/inner/leaf`
- Nested interrupts are correctly attributed — the scheduler
matches events from any descendant under a given path.
- Only a nested **orchestration node** (another Workflow or
SingleAgentReactNode) takes over scheduling. Regular nodes
inherit the enclosing Workflow's scheduler.
### Scoping
Each Workflow has its own `DynamicNodeScheduler` and `_LoopState`.
A nested Workflow creates a new scheduler, so dynamic nodes within
it are scoped to that inner Workflow — not mixed with the outer
Workflow's state.
## event_author
Workflow sets `ctx.event_author = self.name` at the start of
`_run_impl`. This propagates to all child Contexts via NodeRunner.
All events emitted by children carry this author, giving the UI
consistent attribution.
An inner orchestration node (nested Workflow, SingleAgentReactNode)
overrides `event_author` with its own name, so events are attributed
to the nearest orchestration ancestor.
## Orchestration loop lifecycle
```
_run_impl
├─ SETUP: resume from events OR seed start triggers
├─ ctx._schedule_dynamic_node_internal = DynamicNodeScheduler
├─ LOOP:
│ ├─ _schedule_ready_nodes → pop triggers, create NodeRunners
│ ├─ asyncio.wait(FIRST_COMPLETED)
│ └─ _handle_completion → update state, buffer downstream
├─ await dynamic_pending_tasks
├─ _collect_remaining_interrupts
└─ FINALIZE: set ctx.output or ctx._interrupt_ids
```
Key behaviors:
- **Concurrency** — `max_concurrency` limits parallel graph nodes.
Dynamic nodes are excluded (they run inline, throttling would
deadlock).
- **Terminal output** — nodes with no outgoing edges are terminal.
Their output is delegated to the Workflow's own output via
`output_for`. Only one terminal node may produce output.
- **Loop edges** — a completed node can be re-triggered by a
downstream edge pointing back to it. Its status resets to PENDING.
## Resume from session events
On resume (`ctx.resume_inputs` is non-empty), the Workflow
reconstructs static node states from session events:
1. **Scan** — single forward pass through events for this
invocation. For each direct child, track output, interrupts,
and resolved FRs.
2. **Derive status per child:**
- Unresolved interrupts → WAITING
- All interrupts resolved → PENDING (re-run with `resume_inputs`)
- Has output → COMPLETED
- **Partial resume across children:** if child A's interrupt is
resolved but child B's is not, A becomes PENDING (re-runs)
while B stays WAITING. The Workflow re-interrupts with B's
remaining IDs.
- **Partial resume within a child:** if a single child emitted
multiple interrupts (e.g., fc-1 and fc-2) and only fc-1 is
resolved:
- `rerun_on_resume=True` (e.g., nested Workflow): re-run with
partial `resume_inputs` so it can dispatch resolved
grandchildren internally. Remaining interrupts propagate
back up.
- `rerun_on_resume=False`: stay WAITING until all interrupts
are resolved.
3. **Seed triggers** — PENDING nodes get triggers so the loop
re-executes them with `resume_inputs`.
Dynamic node state is **not** scanned upfront — it's lazily
reconstructed by `DynamicNodeScheduler` when `ctx.run_node()` is
called during the re-execution.
## Key design rules for node authors
1. **Set `rerun_on_resume = True`** if your node calls
`ctx.run_node()`. The Workflow must be able to re-execute your
node so it can re-acquire dynamic children's results.
2. **Use deterministic names** for dynamic children. The `name`
parameter on `ctx.run_node()` determines the `node_path`, which
is the dedup/resume key. Non-deterministic names break resume.
3. **Always `await` ctx.run_node() directly.** Do not wrap in
`asyncio.create_task()` — the task won't be tracked by the
scheduler, errors are swallowed, and cancellation on interrupt
won't work.
4. **Yield output after all dynamic children complete.** If your
node calls `ctx.run_node()` and then yields, the output is
emitted only after all children finish. This is the expected
pattern.
5. **Handle `NodeInterruptedError` only if you need custom logic.**
Normally, `ctx.run_node()` raises `NodeInterruptedError` when a
child interrupts. NodeRunner catches it automatically. Only
catch it yourself if you need to clean up or adjust state before
the interrupt propagates.
6. **Don't set `ctx.event_author`** unless your node is an
orchestration node (like Workflow or SingleAgentReactNode). The
Workflow sets it for you and it propagates to all descendants.
@@ -0,0 +1,42 @@
# API Principles
Guidelines for designing and maintaining the ADK public API surface.
## Public API Surface
The public API surface of ADK includes:
- All public classes, methods, and functions in the `google.adk` namespace.
- The names, required parameters, and expected behavior of all built-in Tools.
- The structure and schema of persisted data (Sessions, Memory, Evaluation datasets).
- The JSON request/response format of the ADK API server.
- The command-line interface (CLI) commands, arguments, and flags.
- The expected file structure for agent definitions (e.g., `agent.py` convention loaded by CLI).
## Design Principles
### 1. Stability and Backward Compatibility
- ADK adheres to Semantic Versioning 2.0.0.
- Any change that forces a developer to alter their existing code to upgrade is a **breaking change** and necessitates a MAJOR version bump.
- Avoid breaking changes whenever possible by using optional parameters and deprecation cycles.
### 2. Self-Containment
- Each package should be as self-contained as possible to reduce coupling.
- Within the ADK framework, importing from a package's `__init__.py` is **not allowed**. Import from the specific module directly.
### 3. Explicit Exports
- The public API of a package must be explicitly exported in `__init__.py`.
- **Only public names** should be imported into `__init__.py`. This keeps `__init__.py` minimal and prevents accidental exposure of internal implementation details.
### 4. Intuitive Naming
- Public method and class names should be concise and intuitive.
- Private method names can be longer and more self-explanatory to reduce the need for comments.
#### Examples
**Public Naming**
- **Good**: `Runner.run()`, `Session.get_events()`
- **Bad**: `Runner.orchestrate_agent_invocation_loop()`, `Session.retrieve_all_events_from_storage()`
**Private Naming**
- **Good**: `_prepare_context_for_llm()`, `_should_trim_history()`
- **Bad**: `_prep()`, `_trim()`
+367
View File
@@ -0,0 +1,367 @@
---
name: adk-debug
description: Use when debugging ADK agents, inspecting sessions, testing agent behavior, troubleshooting tool calls, event flow issues, or diagnosing LLM/model problems.
---
# Debugging ADK Agents
Two debugging modes: `adk web` (browser UI + API) and `adk run` (CLI).
> [!NOTE]
> **Preference**: For most development and debugging tasks, `adk run` (CLI) is preferred as it is faster and more convenient. **Within `adk run`, query mode is preferred over interactive mode** because it requires less human intervention. However, `adk web` is still required for UI-specific issues, session management visualization, or debugging the API server itself.
---
## Mode 1: adk web (Browser UI + REST API)
Best for: visual inspection, session management, multi-turn testing.
### Dev server workflow
Before starting a server, ask the user:
1. **Is there already a running `adk web` server?** If yes, use it
(check with `curl -s http://localhost:8000/health`).
2. **If not**, start one. Use `run_in_background` so it doesn't
block. **Remember to shut it down when debugging is done.**
```bash
# Check if server is already running
curl -s http://localhost:8000/health
# Start server (if not running)
adk web path/to/agents_dir # default: http://localhost:8000
adk web -v path/to/agents_dir # verbose (DEBUG level)
adk web --reload_agents path/to/agents_dir # auto-reload on file changes
# Shut down when done (if you started it)
# Kill the background process or Ctrl+C
```
> [!TIP]
> **Coding Agent Friendly Setup**: To allow a coding agent to read the server logs, recommend the user to start the server and redirect output to a file in a location the agent can read (e.g., the conversation's artifact directory or a shared workspace folder):
> ```bash
> adk web -v path/to/agents_dir 2>&1 | tee path/to/agent_readable_log.log
> ```
> This ensures both the user and the agent can inspect the full debug logs.
Web UI: `http://localhost:8000/dev-ui/`
### Session inspection via curl
```bash
# List sessions
curl -s http://localhost:8000/apps/{app_name}/users/{user_id}/sessions | python3 -m json.tool
# Get full session with events
curl -s http://localhost:8000/apps/{app_name}/users/{user_id}/sessions/{session_id} | python3 -m json.tool
```
Do NOT delete sessions after debugging — the user may want to
inspect them in the web UI.
### Summarize events
Fetch the session JSON and write a Python script to summarize
it. Do NOT use hardcoded inline scripts — the JSON schema may
change. Instead, fetch the raw JSON first:
```bash
curl -s http://localhost:8000/apps/{app_name}/users/{user_id}/sessions/{session_id} | python3 -m json.tool
```
Then write a script based on the actual structure you see.
Key fields to look for in each event: `author`, `branch`,
`content.parts` (text, functionCall, functionResponse),
`output`, `actions` (transferToAgent, requestTask, finishTask),
`nodeInfo.path`.
### Send test messages via curl
```bash
SESSION=$(curl -s -X POST http://localhost:8000/apps/{app_name}/users/test/sessions \
-H "Content-Type: application/json" -d '{}' | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
curl -N -X POST http://localhost:8000/run_sse \
-H "Content-Type: application/json" \
-d "{\"app_name\":\"{app_name}\",\"user_id\":\"test\",\"session_id\":\"$SESSION\",
\"new_message\":{\"role\":\"user\",\"parts\":[{\"text\":\"your message here\"}]},
\"streaming\":false}"
```
### Debug endpoints (traces)
```bash
# Trace for a specific event
curl -s http://localhost:8000/debug/trace/{event_id} | python3 -m json.tool
# All traces for a session
curl -s http://localhost:8000/debug/trace/session/{session_id} | python3 -m json.tool
# Health check
curl -s http://localhost:8000/health
```
### Extract LLM content history
Fetch trace data and inspect the `call_llm` spans. The LLM
request/response are in span attributes:
```bash
curl -s http://localhost:8000/debug/trace/session/{session_id} | python3 -m json.tool
```
Look for spans with `name: "call_llm"` and inspect their
`attributes.gcp.vertex.agent.llm_request` (JSON string of the
full request including `contents`, `config`, `model`).
### Key span attributes
| Attribute | Description |
|-----------|-------------|
| `gcp.vertex.agent.llm_request` | Full LLM request JSON (contents, config, model) |
| `gcp.vertex.agent.llm_response` | Full LLM response JSON |
| `gcp.vertex.agent.event_id` | Event ID — correlate with session events |
| `gen_ai.request.model` | Model name |
| `gen_ai.usage.input_tokens` | Input token count |
| `gen_ai.usage.output_tokens` | Output token count |
| `gen_ai.response.finish_reasons` | Stop reason |
---
## Mode 2: adk run (CLI)
Best for: quick testing, scripting, CI/CD, headless debugging.
### Run interactively
```bash
adk run path/to/my_agent # interactive prompts
adk run -v path/to/my_agent # verbose logging
```
### Run with query (automated)
```bash
adk run path/to/my_agent "query" # run with query
adk run --jsonl path/to/my_agent "query" # output structured JSONL (noise reduced)
```
### When to use automated query mode
- **Fast & Lightweight**: Run tests quickly without starting the `adk web` dev server.
- **Easy Automation**: Perfect for CI/CD pipelines and regression scripts.
- **Highly Composable**: You can pipe the `--jsonl` output to standard tools like `jq`, `grep`, or `diff`.
- **Parallel Execution**: Each run is an isolated process. You can run multiple tests concurrently without port conflicts.
- **State Isolation**: Use `--in_memory` for fast, side-effect-free testing (no database updates).
- **Multi-Turn Support**: Remember to set a session ID if you need to maintain conversation state across turns.
> [!TIP]
> Always read the sample's `README.md` first to understand expected inputs and behaviors!
### Unit Tests vs. Sample Agents (When to use which)
Choosing the right testing strategy is crucial for efficiency and coverage:
- **Use Unit Tests when**:
- Testing **isolated logic**, specific methods, or edge cases of a single component.
- Verifying **data schemas**, Pydantic validations, or utility functions.
- *Location*: `tests/unittests/`.
- **Use Sample Agents (Integration Testing) when**:
- Developing features with **multi-level integration** (Runner + Agent + Workflow) or changes with wide impact.
- Testing complex scenarios like **Human-in-the-Loop (HITL)** or long-running tools.
- You need to verify the **real behavior** of the agent in a simulated environment.
- *Location*: Create a sample under `contributing/agent_samples/` (refer to `adk-sample-creator`).
> [!IMPORTANT]
> **AI Assistant Reminder**: If you create a temporary sample agent for testing, you **MUST delete it** after verification is complete, unless the user explicitly asks to keep it.
### Exit Codes & Details
- **Exit Code 0**: Success.
- **Exit Code 1**: Error (e.g., API key missing, agent load failure).
- **Exit Code 2**: Paused (Workflow is waiting for human input/HITL).
For more options and flags, run:
```bash
adk run --help
```
### Event printing utility
```python
from google.adk.utils._debug_output import print_event
print_event(event, verbose=False) # text responses only
print_event(event, verbose=True) # tool calls, code execution, inline data
```
Location: `src/google/adk/utils/_debug_output.py`
### Programmatic debugging
```python
from google.adk import Agent, Runner
from google.adk.sessions import InMemorySessionService
agent = Agent(name="test", model="gemini-2.5-flash", instruction="...")
runner = Runner(app_name="test", agent=agent, session_service=InMemorySessionService())
session = runner.session_service.create_session_sync(app_name="test", user_id="u")
for event in runner.run(user_id="u", session_id=session.id, new_message="hello"):
print(f"{event.author}: {event.content}")
if event.actions.transfer_to_agent:
print(f" -> transfer to {event.actions.transfer_to_agent}")
if event.output:
print(f" -> output: {event.output}")
```
---
## Logging
Shared across both modes.
Set log level with `--log_level` (DEBUG, INFO, WARNING, ERROR, CRITICAL) or `-v` for DEBUG.
Logs write to `/tmp/agents_log/`. Tail latest: `tail -F /tmp/agents_log/agent.latest.log`
Logger name: `google_adk`. Setup: `src/google/adk/cli/utils/logs.py`
| Env Variable | Effect |
|---|---|
| `ADK_CAPTURE_MESSAGE_CONTENT_IN_SPANS` | Include prompt/response in traces (default: `true`) |
| `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT` | Enable prompt/response in OTEL spans |
| `GOOGLE_CLOUD_PROJECT` | Required for `--trace_to_cloud` |
---
## Common Issues
### 1. Agent outputs raw JSON instead of calling tools
**Symptom:** Agent with `output_schema` dumps JSON text instead of calling tools.
**Cause:** `output_schema` sets `response_schema` on the LLM config, activating controlled generation (JSON-only mode).
**Check:** Look for `response_mime_type: "application/json"` in the LLM request.
**Location:** `src/google/adk/flows/llm_flows/basic.py`
### 2. Events missing from session / not visible to plugins
**Symptom:** Events from sub-agents don't appear in plugin callbacks or runner event stream.
**Cause:** Direct `append_event` calls inside components bypass the runner's event loop.
**Check:** Only the runner (`runners.py`) should call `append_event`. Components should yield events.
### 3. `NameError: name 'X' is not defined` at runtime
**Symptom:** `{"error": "name 'SomeClass' is not defined"}`
**Cause:** Class imported under `TYPE_CHECKING` but used at runtime (e.g., `isinstance()`).
**Fix:** Move import outside `TYPE_CHECKING` or use a local import.
### 4. Sub-agent doesn't have context from parent conversation
**Symptom:** Sub-agent only sees its own input, not the parent's history.
**Cause:** Branch isolation — sub-agents on a branch only see events on that branch.
**Fix:** Write the sub-agent's `description` to prompt the parent to include context in delegation input.
### 5. Agent validation errors at startup
**Symptom:** `ValueError` on agent construction.
**Common causes:**
- `"All tools must be set via LlmAgent.tools."` — Don't pass tools via `generate_content_config`
- `"System instruction must be set via LlmAgent.instruction."` — Don't set via `generate_content_config`
- `"Response schema must be set via LlmAgent.output_schema."` — Don't set via `generate_content_config`
**Location:** `src/google/adk/agents/llm_agent.py``validate_generate_content_config`
### 6. LLM calls exceeding limit
**Symptom:** `LlmCallsLimitExceededError: Max number of llm calls limit of N exceeded`
**Cause:** `run_config.max_llm_calls` limit reached.
**Fix:** Increase `max_llm_calls` in `RunConfig`, or investigate why the agent is looping.
**Location:** `src/google/adk/agents/invocation_context.py`
### 7. Tool errors silently swallowed
**Symptom:** Tool call fails but agent continues without expected result.
**Cause:** Errors are caught and returned as function response text. Set `on_tool_error_callback` to customize.
**Check:** Look for error text in function response events.
### 8. Agent not loading / not discovered
**Symptom:** `adk web` doesn't list the agent, or returns 404.
**Cause:** Agent directory must follow convention:
```
my_agent/
__init__.py # MUST contain: from . import agent
agent.py # MUST define: root_agent = Agent(...) OR app = App(...)
```
### 9. Sync tool blocking the event loop
**Symptom:** Agent hangs or becomes very slow.
**Cause:** Sync tools run in a thread pool (max 4 workers). All workers busy → new tool calls block.
**Fix:** Make tools async if they do I/O.
---
## LLM Finish Reasons
- `STOP` — normal completion
- `MAX_TOKENS` — output truncated (increase `max_output_tokens`)
- `SAFETY` — blocked by safety filters
- `RECITATION` — blocked for recitation
---
## Event Flow Architecture
```
User message
-> Runner.run_async()
-> Runner._exec_with_plugin() # persists events, runs plugins
-> agent.run_async() # yields events
-> LlmAgent._run_async_impl()
-> BaseLlmFlow.run_async() # Execution flow
-> _AutoFlow or _SingleFlow # Flow implementations
-> call_llm # LLM request + response
-> execute_tools # tool dispatch (functions.py)
```
---
## Callback Chain
**Before model call:** PluginManager `run_before_model_callback()` → agent `canonical_before_model_callbacks`
**After model call:** PluginManager `run_after_model_callback()` → agent `canonical_after_model_callbacks`
**Before/after tool call:** PluginManager `run_before_tool_callback()` / `run_after_tool_callback()` → agent callbacks
---
## Key Files for Debugging
| Area | File |
|---|---|
| Runner event loop | `src/google/adk/runners.py` |
| LLM request building | `src/google/adk/flows/llm_flows/basic.py` |
| Tool dispatch | `src/google/adk/flows/llm_flows/functions.py` |
| Multi-agent orchestration | `src/google/adk/workflow/` |
| Content/context building | `src/google/adk/flows/llm_flows/contents.py` |
| Task support | `src/google/adk/agents/llm/task/` |
| Agent config + validation | `src/google/adk/agents/llm_agent.py` |
| Event model | `src/google/adk/events/event.py` |
| Session services | `src/google/adk/sessions/` |
| Invocation context | `src/google/adk/agents/invocation_context.py` |
| Web server + debug endpoints | `src/google/adk/cli/adk_web_server.py` |
| Debug output printer | `src/google/adk/utils/_debug_output.py` |
---
## Debugging Checklist
1. **Start with logs**`-v` flag, check `/tmp/agents_log/agent.latest.log`
2. **Inspect the session** — curl endpoints (`adk web`) or print events (`adk run`)
3. **Check event actions**`transfer_to_agent`, `request_task`, `finish_task`, `escalate`
4. **Check event.output** — single_turn and task agents set output here
5. **Check traces**`/debug/trace/session/{id}` for model/token usage
6. **Verify agent structure**`__init__.py` imports, `root_agent` or `app` defined
7. **Check tool responses** — look for error text in function response events
8. **Check LLM finish reason**`STOP`, `MAX_TOKENS`, `SAFETY`
9. **Test in isolation** — create a minimal agent with just the problem tool/config
+90
View File
@@ -0,0 +1,90 @@
---
name: adk-git
description: Use for any git operation (commit, push, pull, rebase, branch, PR, cherry-pick, etc.). Provides commit message format and conventions.
---
# Git Operations for adk-python
## Commit Message Format
Use **Conventional Commits**:
```
<type>(<scope>): <description>
```
### Types
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation only
- `style`: Formatting, no code change
- `refactor`: Code restructure without behavior change
- `perf`: Performance improvement
- `test`: Adding/updating tests
- `chore`: Build, config, dependencies
- `ci`: CI/CD changes
### Description Phrasing
**CRITICAL**: The subject line must answer **why**, not just **what**.
A reviewer reading only the subject should understand the motivation.
- **State the outcome**, not the mechanics:
- Good: `Fix race condition when two agents write to same session`
- Bad: `Update session.py to add lock`
- **Name the capability added**, not the implementation:
- Good: `Support parallel tool execution in workflows`
- Bad: `Add asyncio.gather call in execute_tools_node`
- **For refactors, state the reason**, not just the action:
- Good: `Make graph public for dev UI serialization`
- Bad: `Make graph a public field on new Workflow`
- **For bug fixes, state what was broken**:
- Good: `Prevent duplicate events when resuming HITL`
- Bad: `Check interrupt_id before appending`
### Detailed Commit Messages
Promote detailed commit messages by including a short, concrete explanation in the body:
- For **features**: Give a sample usage or explain the new capability.
- For **fixes**: Explain what caused the error and how the fix addresses it.
**Example (Feature):**
```
feat(workflow): Support JSON string parsing in schema validation
Automatically parse JSON strings into dicts or Pydantic models when input_schema or output_schema is defined on a node.
```
**Example (Fix):**
```
fix(sessions): Prevent duplicate events when resuming HITL
The interrupt_id was not checked before appending, causing duplicates if the user resumed multiple times. Added a check to ignore already processed interrupts.
```
Self-check before committing: read your subject line and ask "does this tell me _why_ someone made this change?" If it only describes _what_ changed, rewrite it.
### Rules
1. **Imperative mood** - "Add feature" not "Added feature".
2. **Capitalize** first letter of description (for release-please changelog).
3. **No period** at end of subject line.
4. **50 char limit** on subject line when possible, max 72.
5. **Use body for context** - Add a blank line then explain _why_,
not _how_, when the subject alone isn't enough.
6. **Reference GitHub issues** - If the commit fixes a GitHub issue, include "Fixes #<issue-number>" or "Closes #<issue-number>" (or the full issue URL if cross-repository) in the commit message body.
### Examples
```
feat(agents): Support App pattern with lifecycle plugins
fix(sessions): Prevent memory leak on concurrent session cleanup
refactor(tools): Unify env var checks across tool implementations
docs: Add contributing guide for first-time contributors
```
## Pre-commit Hooks
> [!IMPORTANT]
> Before performing any commit, check if `pre-commit` is installed and configured with the expected hooks (`isort`, `pyink`, `addlicense`, `mdformat`). If not, remind the user to set up pre-commit hooks using the `adk-setup` skill.
+83
View File
@@ -0,0 +1,83 @@
---
name: adk-review
description: Reviews all local changes in the repository for errors, styling compliance, unintended outcomes, and necessary documentation/test/sample updates. Generates a report and assists in fixing identified issues on-demand. Triggers on "adk-review", "review changes", "pr review", "check code style", "verify changes".
---
# ADK Change Reviewer (adk-review)
This skill guides AI assistants in performing a comprehensive, rigorous review of local repository changes before they are committed or submitted. It evaluates code correctness, style guidelines, architectural impact, and checks if associated tests, samples, and documentation need updates. It generates a detailed report and, upon explicit user request, assists in automatically fixing the identified issues.
> [!NOTE]
> Always read this skill and follow its steps when asked to review local changes or before finalizing a PR/commit.
---
## Review Checklist Dimensions
### 1. Code Correctness & Errors
- **Syntax & Types**: Ensure the code is free of syntax errors and conforms to strong typing guidelines. Avoid using `Any`, and prefer specific/abstract types. Use `X | None` instead of `Optional[X]`.
- **Imports**: Verify there are no circular imports. Ensure absolute imports are used where appropriate.
- **Exception Handling**: Avoid bare `except:`. Always catch specific exceptions and log them properly with context.
- **Visibility**: Ensure internal modules and package-private attributes use proper naming (e.g., prefixed with `_`) per ADK rules.
- **Edge Cases & Defensive Programming**:
- **Type & Attribute Discrimination**: Explicitly verify an object's type (e.g., using `isinstance`) before checking type-specific or custom attributes (e.g., checking if a node is an `LlmAgent` before inspecting its `mode`), avoiding errors on unexpected types.
- **Boundary and Null Conditions**: Ensure robust handling for boundary conditions and null values (e.g., `None`, empty collections, zero, or empty strings) using validation or fallback defaults.
- **Preconditions & Invariants**: Validate that preconditions and state invariants are checked before performing core logic.
### 2. Code Quality & Design
- **Complexity & Readability**: Identify overly complex functions or classes. Suggest refactoring (e.g., splitting functions, extracting helper classes) to improve readability and maintainability. Ensure code is self-documenting.
- **Design Patterns**: Check if appropriate design patterns are used. Avoid anti-patterns. Ensure high cohesion and low coupling.
- **Performance & Efficiency**: Look for performance bottlenecks, such as unnecessary database queries, redundant computations, inefficient loops, or excessive memory allocation.
- **Security & Privacy**: Verify that inputs are validated, sensitive data is handled securely, and there are no potential security vulnerabilities (like injection, resource exhaustion, or exposure of internal state).
### 3. Style and Convention Compliance
- **ADK Style Guide**: Cross-reference all code changes with the guidelines in the `adk-style` skill (including Pydantic v2 patterns, lazy logging evaluation, and file structure).
- **Pre-commit Hooks**: Ensure changed files are formatted and linted. Remind the user to run `pre-commit run --files <files>` if hooks like `isort`, `pyink`, `addlicense`, or `mdformat` are not configured automatically.
### 4. Architectural Integrity & Unintended Outcomes
- **Public API Stability**: Verify whether changes modify, remove, or restrict public-facing interfaces, classes, methods, argument lists, or CLI structures (e.g., in the public package namespaces under `src/google/adk/`). Breaking changes are unacceptable without a formal deprecation cycle under Semantic Versioning.
- **Execution & Resumption**: If changing workflows, nodes, or state management, ensure compatibility with the ADK 2.0 event execution lifecycle and session resumption (HITL/checkpoints).
- **Concurrency & Safety**: Check for race conditions or resource leaks. Ensure long-running or shared resources (like plugins, exporters, and connections) are closed/disposed of safely.
### 5. Documentation Impact (`docs/design` and `docs/guides`)
- **Design & Architecture**: Determine if the change updates a core design contract. If so, check if design docs under `docs/design/` require updates or new documents need to be written.
- **Guides**: If the changes introduce a new feature or change a public API/workflow pattern, check if the guides under `docs/guides/` need updates.
### 6. Sample Compatibility & Updates
- **Sample Integrity**: Verify if existing samples under `contributing/samples/` are affected by the change.
- **New Samples**: If the changes introduce a key new capability, assess whether a new sample should be added to demonstrate the feature (following `adk-sample-creator` conventions).
### 7. Test Coverage & Quality
- **Coverage**: Ensure that all modified or new code paths have corresponding unit or integration tests under `tests/`.
- **ADK Test Rules**: Ensure test implementations adhere to the 9 rules in the `adk-style` testing reference (e.g., using deterministic IDs, event normalization, and clean up utilities).
---
## Execution Workflow
When the `adk-review` skill is triggered, you MUST execute the following steps:
### Step 1: Retrieve Local Changes
Run `git status` and `git diff` to identify exactly which files have been modified, added, or deleted.
### Step 2: Perform the Multi-Dimensional Review
Analyze the retrieved diffs file-by-file against the seven dimensions in the Checklist. Identify any errors, deviations, or missing files (such as docs, tests, or samples).
### Step 3: Generate and Present a Review Report
Generate a clear, beautifully formatted Markdown report categorized by priority:
- 🔴 **Critical Errors, Bugs, & Security**: Syntax, type safety violations, race conditions, resource leaks, or security vulnerabilities.
- 🟠 **Code Quality & Design**: High complexity, poor readability, performance bottlenecks, or architectural misalignment.
- 🟡 **Style & Conventions**: Lints, formatting issues, non-lazy logging, or minor typing mismatches.
- 🔵 **Documentation, Tests, & Samples**: Missing or stale test coverage, design docs, or user guides.
Include the specific filename and line number/context for each finding.
### Step 4: Present Findings and Stop
Stop execution here. Do **NOT** call any code editing tools or modify the codebase automatically. Present the generated review report clearly to the user, highlighting key takeaways, and stop.
Do **NOT** ask the user if they want you to fix the issues, and do **NOT** offer interactive fixing options by default. Simply stop and wait for the user to explicitly command or ask you to fix the changes.
### Step 5 (Optional): Implement Authorized Fixes & Verify
If, and only if, the user explicitly instructs or requests you to apply a fix for some or all of the identified findings:
1. Perform the necessary edits using precise code editing tools. Ensure all fixes strictly comply with the established `adk-style` and `adk-architecture` rules.
2. Verify correctness by running associated unit and integration tests (e.g., via `pytest` or pre-commit hooks) before concluding.
+157
View File
@@ -0,0 +1,157 @@
---
name: adk-sample-creator
description: Author new samples for the ADK Python repository. Use this skill when the user wants to create a new sample demonstrating a feature or agent pattern (e.g., dynamic nodes, standalone agents, fan-out/fan-in) or when adding examples to subdirectories under `contributing/`.
---
# ADK Sample Creator
This skill helps you create new samples for the ADK Python repository. You should search for subdirectories under `contributing` (such as `new_workflow_samples`, `workflow_samples`, etc.) and confirm with the user which folder they want to use before creating the sample.
> [!TIP]
> Before creating samples, you can use the `adk-style` skill to learn about ADK 2.0 architecture knowledge and best practices.
A sample consists of:
1. A directory per sample.
2. An `agent.py` file defining the agent or workflow logic.
3. A `README.md` file explaining the sample.
## Guidelines
### 1. Folder Name
Use snake_case for the folder name (e.g., `dynamic_nodes`, `fan_out_fan_in`).
### 2. `agent.py` Content
The `agent.py` should focus on demonstrating a specific feature or agent pattern. Use absolute imports for testing convenience.
> [!IMPORTANT]
> **Model Selection**: Do not set the `model` parameter explicitly (e.g., `model="gemini-2.5-flash"`) on `Agent` instances in sample agents. Instead, let them default to the system-configured model, unless a specific model is explicitly requested by the user.
Choose one of the following patterns:
#### Pattern A: Workflows (for complex graphs)
Use this when you need multiple nodes, routing, or parallel execution.
**Imports:**
```python
from google.adk import Agent
from google.adk import Context
from google.adk.workflow import node
from google.adk.workflow import JoinNode
from google.adk.workflow._workflow_class import Workflow
```
**Anatomy:**
```python
my_agent = Agent(name="my_agent", ...)
@node()
async def my_node(node_input: str):
return "result"
root_agent = Workflow(
name="root_wf",
edges=[("START", my_node)],
)
```
#### Pattern B: Standalone Agents (for single-agent or simple tool use)
Use this when you don't need a graph and the agent handles the loop.
**Imports:**
```python
from google.adk import Agent
from google.adk.tools import google_search # example
```
**Anatomy:**
```python
root_agent = Agent(
name="standalone_assistant",
instruction="You are a helpful assistant.",
description="An assistant that can help with queries.",
tools=[google_search],
)
```
### 3. `README.md` Content
Each sample should have a `README.md` with the following structure:
- **Overview**: What the sample does.
- **Sample Inputs**: Examples of inputs to test with. Each prompt must be wrapped in backticks. If a prompt has an explanation, always add a blank line between the prompt and the explanation, and indent the explanation by two spaces.
- **Graph**: Visualization of the graph flow (Mermaid recommended). For Workflow root agents, visualize the graph flow of nodes. For LlmAgent root agents that orchestrate tools or sub-agents, visualize the topology of the agent and its tools/sub-agents instead of internal workflow nodes.
- **How To**: Explanation of key techniques used (e.g., `ctx.run_node`).
- **Related Guides**: Links to relevant developer guides in `docs/guides/` that explain the concepts or classes used.
#### README Example Template:
````markdown
# ADK Sample Name
## Overview
Brief description.
## Sample Inputs
- `Prompt example 1`
- `Prompt example 2`
*Explanation or expected behavior*
## Graph
For Workflow root agents:
```mermaid
graph TD
START --> MyNode
```
For LlmAgent root agents:
```mermaid
graph TD
MyAgent[my_agent] -->|calls| MyTool(my_tool)
```
## How To
Explain the details.
## Related Guides
- [Guide Title](../../docs/guides/path/to/guide.md) - Brief description of what the guide covers.
````
## Examples
### Dynamic Nodes
Snippet from `dynamic_nodes/agent.py`:
```python
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
while True:
headline = await ctx.run_node(generate_headline)
# ...
````
### Fan Out Fan In
Snippet from `fan_out_fan_in/agent.py`:
```python
root_agent = Workflow(
name="root_agent",
edges=[("START", (node_a, node_b), join_node, aggregate)],
)
```
+84
View File
@@ -0,0 +1,84 @@
---
name: adk-setup
description: Set up a local development environment for the ADK Python project. Use when the user wants to get started developing, set up their environment, install dependencies, or prepare for contributing.
disable-model-invocation: true
---
Set up the local development environment for ADK Python.
## Prerequisites
Check the following before proceeding:
1. **Python 3.10+**
```bash
python3 --version
```
2. **uv package manager** (required — do not use pip/venv directly)
```bash
uv --version
```
If not installed:
```bash
curl -LsSf https://astral.sh/uv/install.sh | sh
```
## Setup Steps
Run these commands from the project root:
3. **Create and activate a virtual environment:**
```bash
uv venv --python "python3.11" ".venv"
source .venv/bin/activate
```
4. **Install all dependencies for development:**
```bash
uv sync --all-extras
```
5. **Install development tools:**
```bash
uv tool install pre-commit
uv tool install tox --with tox-uv
```
6. **Install addlicense (requires Go):**
```bash
go version && go install github.com/google/addlicense@latest
```
> [!NOTE]
> If Go is not installed, tell the user:
> "Go is required for the addlicense tool. Please install Go from https://go.dev/dl/ and then re-run the `adk-setup` skill to complete the setup."
7. **Set up pre-commit hooks:**
```bash
pre-commit install
```
8. **Verify everything works by running tests locally:**
```bash
pytest tests/unittests -n auto
```
## Key Commands Reference
| Task | Command |
| :----------------------------------- | :------------------------------------------------ |
| Run unit tests (Fast) | `pytest tests/unittests` |
| Run tests across all Python versions | `tox` |
| Format codebase | `pre-commit run --all-files` |
| Run tests in parallel | `pytest tests/unittests -n auto` |
| Run specific test file | `pytest tests/unittests/agents/test_llm_agent.py` |
| Launch web UI | `adk web path/to/agents_dir` |
| Run agent via CLI | `adk run path/to/my_agent` |
| Build wheel | `uv build` |
+20
View File
@@ -0,0 +1,20 @@
---
name: adk-style
description: ADK development style guide for routine nits — Python idioms, codebase conventions, imports, typing, Pydantic patterns, formatting, logging, async/concurrency, and file organization. Use this skill whenever writing code, tests, or reviewing PRs for the ADK project to ensure compliance with styling and coding conventions. Triggers on "code style", "how should I format", "naming convention", "lint", "nit", "imports", "typing", "Pydantic patterns", "testing rules", "async", "io".
---
# ADK Style Guide
## Style Guide (references/)
- [Visibility](references/visibility.md) — naming conventions for module-private, internal, and package-private visibility.
- [Imports](references/imports.md) — relative vs absolute imports, `TYPE_CHECKING` patterns.
- [Typing](references/typing.md) — strong typing, avoiding Any, bare type names, keyword-only arguments, `Optional` vs `| None`, abstract parameter types, mutable default avoidance, runtime type discrimination.
- [Pydantic Patterns](references/pydantic.md) — Pydantic v2 usage, `Field()` constraints, `field_validator`, `model_validator`, private attributes, deprecation migration, post-init setup.
- [Formatting](references/formatting.md) — indentation, line limits, and running pre-commit hooks.
- [Documentation](references/documentation.md) — comments and docstrings.
- [Logging](references/logging.md) — lazy evaluation and log levels.
- [Async and Concurrency](references/async.md) — async I/O requirements, avoiding blocking the event loop.
- [File Organization](references/file-organization.md) — file headers and class organization.
## Testing
[references/testing.md](references/testing.md) — core principles, 9 rules for writing ADK tests, test structure template
@@ -0,0 +1,19 @@
# Async and Concurrency Style Guide
- **All I/O operations must be in async functions**: Any operation that
performs I/O (network calls, file system access, database queries, etc.)
must be defined in an `async def` function.
- **Do not block the event loop**: Avoid calling blocking synchronous
functions directly from async code.
- **Wrap synchronous I/O**: If you must use a synchronous library for I/O
(e.g., standard `open()`, `pathlib` file operations, or synchronous
clients), wrap the blocking call in `asyncio.to_thread` to run it in a
separate thread and prevent blocking the main event loop.
Example:
```python
async def save_data(path: Path, data: bytes) -> None:
# Wrap blocking file write in asyncio.to_thread
await asyncio.to_thread(path.write_bytes, data)
```
@@ -0,0 +1,12 @@
# Documentation and Comments
## Public API Documentation
- **Clear Usage**: For public interfaces, explain the intended usage clearly, with concise examples.
- **Public Classes**: Explain all public attributes.
- **Public Methods/Functions**: Explain all arguments, return values, and raised exceptions.
## Internal Implementation Comments
- **Explain Why, Not What**: For internal code and private methods, explain **why**, not **what** — the code itself should be self-documenting.
- **Stale References**: Don't reference RFCs or design docs in source code (they become stale).
@@ -0,0 +1,15 @@
# File Organization
- One class per file in `workflow/`.
- Private modules prefixed with `_` (e.g., `_base_node.py`).
- Public API exported through `__init__.py`.
- Unit tests must be placed in the same folder hierarchy under `tests/unittests/` as the original file in `src/`.
- If a single source file has multiple test files (e.g. testing different classes or behaviors separately), use the source file name (without leading underscores or extension) as the prefix for the test file names.
- Example: `src/google/adk/tools/environment/_tools.py` -> `tests/unittests/tools/environment/test_tools_edit_file.py`
## File Headers
Every source file must have:
1. Apache 2.0 license header.
2. `from __future__ import annotations`.
3. Standard library imports, then third-party, then relative.
@@ -0,0 +1,20 @@
# Formatting Style Guide
- 2-space indentation (never tabs).
- 80-character line limit.
- `pyink` formatter (Google-style).
- `isort` with Google profile for import sorting.
- Enforced automatically by pre-commit hooks (`isort`, `pyink`, `addlicense`, `mdformat`). Use the `adk-setup` skill to install and configure these tools.
## Running Formatter Manually
```bash
# Format only staged files (runs automatically on commit)
pre-commit run
# Format all changed files (staged + unstaged)
pre-commit run --files $(git diff --name-only HEAD)
# Format all files in the repo
pre-commit run --all-files
```
@@ -0,0 +1,30 @@
# Imports Style Guide
## General Rules
- **Source code** (`src/`): Use relative imports.
`from ..agents.llm_agent import LlmAgent`
- **Tests** (`tests/`): Use absolute imports.
`from google.adk.agents.llm_agent import LlmAgent`
- **Import from module**: Import from the module file, not from `__init__.py`.
`from ..agents.llm_agent import LlmAgent` (not `from ..agents import LlmAgent`)
- **CLI package** (`cli/`):
- Treat as an external package.
- Use **relative imports** for files within the `cli/` package.
- Use **absolute imports** for files outside of the `cli/` package.
- **Dependency Direction**: Only `cli/` can import from the rest of the codebase. The other codebase must **STRICTLY NOT** import from `cli/`.
## TYPE_CHECKING Imports
Use `TYPE_CHECKING` for imports needed only by type hints to avoid circular imports at runtime:
```python
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from ..agents.invocation_context import InvocationContext
```
This works because `from __future__ import annotations` makes all annotations strings (deferred evaluation), so the import is never needed at runtime.
@@ -0,0 +1,16 @@
# Logging Style Guide
## General Rules
- **Lazy Evaluation**: Use lazy-evaluated `%`-based templates for logging to avoid overhead when the log level is not enabled.
- **Good**: `logging.info("Processing item %s", item_id)`
- **Bad**: `logging.info(f"Processing item {item_id}")`
- **Contextual Logging**: Leverage structured logging and trace IDs when available to correlate logs across operations.
- **No Secrets**: Never log sensitive information (API keys, user credentials, or PII).
## Log Levels
- **DEBUG**: Detailed information for diagnosing problems. Use generously in internal implementation but avoid cluttering production logs.
- **INFO**: Confirmation that things are working as expected (e.g., workflow started, node completed).
- **WARNING**: Indication that something unexpected happened or a problem might occur soon (e.g., retry triggered).
- **ERROR**: A serious problem that prevented a function or operation from completing.
@@ -0,0 +1,78 @@
# Pydantic Patterns
ADK models use Pydantic v2. This guide covers the key patterns used throughout the codebase.
## Basic Model Structure
- Use `Field()` for validation, defaults, and descriptions.
- Use `PrivateAttr()` for internal state that shouldn't be serialized.
- Use `model_post_init()` instead of `__init__` for setup logic.
- Prefer `model_dump()` over `dict()` (Pydantic v2).
## On-Wire Models
For Pydantic models that cross network or system boundaries (e.g., API payloads, WebSocket messages, event persistence), inherit from `SerializedBaseModel` located in `google.adk.utils._serialized_base_model`.
This ensures:
- camelCase serialization by default (via `alias_generator=to_camel`).
## Docstrings as Field Descriptions
To keep code Pythonic and ensure that generated schemas stay in sync with documentation, it is **strongly recommended** to use docstrings as field descriptions for all Pydantic models in the ADK codebase.
To enable this, add `use_attribute_docstrings=True` to your model's `ConfigDict`:
```python
from pydantic import BaseModel, ConfigDict
class MyModel(BaseModel):
model_config = ConfigDict(use_attribute_docstrings=True)
field_name: str
"""Description of the field."""
```
Note: If you are inheriting from `SerializedBaseModel`, this is already enabled by default.
## Summary of When to Use Each
| Need | Pattern |
|---|---|
| Simple numeric/string bounds | `Field(ge=0, le=100)` |
| Single-field business logic | `@field_validator('field', mode='after')` |
| Cross-field consistency | `@model_validator(mode='after')` |
| Field deprecation/migration | `@model_validator(mode='before')` |
| Internal mutable state | `PrivateAttr(default_factory=...)` |
| Post-construction setup | `model_post_init()` |
## `Field()` with Constraints
Use `Field()` constraints for declarative validation directly on the field definition. This keeps validation close to the data declaration and avoids custom validator boilerplate.
## `field_validator` — Single-Field Validation
Use `@field_validator` for validation logic that goes beyond simple constraints. This is heavily used in ADK (36+ instances). Always use `mode='after'` unless you need to intercept raw input before Pydantic coercion.
**Rules:**
- Decorate with `@field_validator(...)`. While `@classmethod` is automatically applied by Pydantic v2, adding it is recommended in ADK for explicit visibility.
- Return the (possibly transformed) value.
- Raise `ValueError` with a descriptive message on failure.
- Prefer `mode='after'` (validates after Pydantic's own parsing/coercion).
## `model_validator` — Cross-Field and Migration Validation
Use `@model_validator` when validation depends on multiple fields, or when handling deprecation/migration of field names.
### `mode='before'` — Deprecation and Field Migration
### `mode='after'` — Cross-Field Consistency
**Rules:**
- `mode='before'`: receives raw `data` (usually `dict`). Use for field renaming, deprecation, and input normalization. Must return the (modified) data.
- `mode='after'`: receives the fully constructed model instance (`self`). Use for cross-field consistency checks. Must return `self`.
- Always guard `mode='before'` validators with `isinstance(data, dict)` since data could also come as an existing model instance.
@@ -0,0 +1,232 @@
# ADK Testing Style Guide
## Core Principles
- **Test through the public interface** — call what users call, assert what users see.
- **Test behavior, not implementation** — verify outcomes (outputs, side effects, errors), not internal mechanics.
- **Refactor-proof** — if an internal refactor preserves the same behavior, all tests should still pass.
## Rules
### 1. Test names describe the behavior, not the mechanism
```python
# Good — describes what the caller observes
def test_empty_queue_returns_none():
def test_retry_stops_after_max_attempts():
def test_missing_key_raises_key_error():
# Bad — describes implementation details
def test_deque_popleft_called():
def test_retry_counter_incremented():
def test_dict_getitem_raises():
```
### 2. Docstring: one-line summary, then setup/act/assert
The first line describes the expected behavior from the caller's
perspective. For complex tests (multi-step, multi-invocation),
follow with a structured breakdown of Setup, Act, and Assert.
```python
# Good — simple test, one-liner is enough
"""Getting from an empty cache returns the default value."""
# Good — complex test with structured breakdown
"""Partial FR re-runs nested Workflow, resolved child completes
while unresolved stays interrupted.
Setup: outer_wf → inner_wf → (child_a, child_b) → join.
Both children interrupt on first run.
Act:
- Run 2: resolve only child_a's FR.
- Run 3: resolve child_b's FR.
Assert:
- Run 2: child_a produces output, invocation still interrupted.
- Run 3: child_b produces output, join completes, no interrupts.
"""
# Bad — restates the implementation
"""LRUCache._store.get returns sentinel when key missing."""
"""ThreadPool._accept_tasks flag checked in submit()."""
```
### 3. Each test covers one behavior
If a test checks multiple unrelated behaviors, split it. If you can't
describe the test in one sentence, it's testing too much.
```python
# Bad — tests capacity AND eviction AND default in one test
def test_cache_behavior():
assert cache.size == 0
assert cache.get('x') is None
cache.put('a', 1)
assert cache.size == 1
# Good — split into focused tests
def test_new_cache_is_empty():
"""A freshly created cache has no entries."""
def test_cache_evicts_oldest_when_full():
"""Adding to a full cache removes the least recently used entry."""
```
### 4. Don't test internal state
```python
# Bad — reaches into private attributes
assert pool._workers[0].is_alive
assert parser._state == 'HEADER'
assert isinstance(router._handler, _FastHandler)
# Good — tests through the public interface
assert pool.active_count == 1
assert parser.parse('data') == expected
assert router.route('/api') == handler
```
### 5. Use real components, mock only boundaries
ADK tests should use real implementations as much as possible
instead of mocking.
- Mock external dependencies: LLM APIs, cloud services, session stores
- Use real ADK components: BaseNode subclasses, Event, Context
- Mock InvocationContext when testing NodeRunner (it's a boundary)
### 6. Test fixtures should be minimal
Define the simplest possible setup that triggers the behavior:
```python
# Good — minimal fixture, one purpose
def make_user(role='viewer'):
return User(name='test', email='t@t.com', role=role)
# Bad — kitchen-sink fixture with unrelated setup
def make_full_test_env():
db = create_database()
user = create_user_with_billing()
setup_notifications()
...
```
### 7. Keep arrange logic close to the test
When a helper class or fixture is used by only one test, define it
inline inside the test function. This keeps the setup visible at the
point of use and avoids scrolling to distant module-level definitions.
Extract to module level only when 3+ tests share the same helper.
```python
# Good — helper defined inline, right next to the test
@pytest.mark.asyncio
async def test_state_delta_bundled_with_output():
"""State set before yield is flushed onto the output event."""
class _Node(BaseNode):
async def _run_impl(self, *, ctx, node_input):
ctx.state['color'] = 'blue'
yield 'result'
ctx, events = _make_ctx()
await NodeRunner(node=_Node(name='n'), parent_ctx=ctx).run()
assert events[0].output == 'result'
assert events[0].actions.state_delta['color'] == 'blue'
# Bad — helper defined 300 lines above, reader must scroll
class _StateThenOutputNode(BaseNode):
async def _run_impl(self, *, ctx, node_input):
ctx.state['color'] = 'blue'
yield 'result'
# ... 300 lines later ...
async def test_state_delta_bundled_with_output():
node = _StateThenOutputNode(name='n')
...
```
### 8. Assertions tell a story
```python
# Good — reads like a specification
assert queue.size == 0
assert config.get('timeout') == 30
assert response.status_code == 404
# Bad — overly defensive, tests framework behavior
assert isinstance(queue, Queue)
assert hasattr(config, 'get')
assert len(response.headers) > 0
```
### 9. Structure tests as arrange, act, assert
Every test has three distinct steps:
- **Arrange** — set up the external state specific to the scenario.
General setup shared by many tests belongs in fixtures.
- **Act** — call the system under test. Usually a single call.
- **Assert** — verify return values or visible state changes. No
further calls to the system under test here.
Keep steps distinct. Separate with blank lines. In simple tests where
each step is a single statement, blank lines can be omitted. In
complex tests, use descriptive comments like "Given [situation]",
"When [action]", "Then [expectation]" — avoid bare labels that add
no information.
```python
# Good — clear visual separation
def test_cache_returns_stored_value():
cache = Cache()
cache.put('key', 'value')
result = cache.get('key')
assert result == 'value'
# Good — simple test, blank lines omitted
def test_new_cache_is_empty():
assert Cache().size == 0
# Bad — steps interleaved
def test_cache_behavior():
cache = Cache()
cache.put('key', 'value')
result = cache.get('key')
assert result == 'value'
cache.put('key2', 'value2') # more setup after assert
assert cache.size == 2
```
### Test Structure Template
```python
"""Tests for <ComponentName>.
Verifies that <component> correctly <high-level behavior>.
"""
# --- Fixtures (minimal, one purpose each) ---
def _make_service():
...
# --- Tests (one behavior per test) ---
def test_<behavior_description>():
"""<One sentence: what the system does from the outside.>"""
# Given a service with default config
service = _make_service()
input_data = 'hello'
# When the operation is performed
result = service.do_something(input_data)
# Then the result matches expectations
assert result == expected
```
@@ -0,0 +1,54 @@
# Type Hints and Strong Typing
## General Rules
- **Prefer Strong Typing**: Use type hints for all function arguments and return types. Avoid leaving types unspecified.
- **Minimize `Any`**: Use specific types or `Generic` whenever possible. Avoid `Any` as it bypasses type checking.
- **No double-quoted type hints**: When `from __future__ import annotations` is present, use bare type names (e.g., `list[str]` instead of `"list[str]"`).
- **Always include `from __future__ import annotations`**: Every source file must include this immediately after the license header, before any other imports. This enables forward-referencing classes without quotes (PEP 563).
## `Optional[X]` vs `X | None`
The codebase uses both styles. Follow this convention:
- **New code** (especially in `workflow/`): Prefer `X | None` — it is more concise and modern.
- **Existing files**: Match the style already used in the file for consistency.
- **Both are acceptable** — do not refactor one to the other without reason.
## Abstract Types for Function Parameters
Use abstract types from `collections.abc` for function parameter annotations. This accepts the widest range of inputs while remaining type-safe. Use concrete types for return annotations to give callers the most useful information.
## Keyword-Only Arguments
Use `*` to force keyword-only arguments on functions with multiple parameters of the same type, or where argument order is error-prone. This is a widely used pattern in ADK (16+ files).
**When to use `*`:**
- Constructors (`__init__`) with 2+ non-self parameters
- Any function where swapping arguments would silently produce wrong results
- Methods with multiple `str` or `int` parameters
## Mutable Default Arguments
**Never use mutable default arguments.** Use `None` as a sentinel and initialize in the function body. This is a well-followed pattern throughout ADK.
This applies to `list`, `dict`, `set`, and any other mutable type.
## Runtime Type Discrimination with `isinstance()`
Use `isinstance()` for runtime type discrimination when handling polymorphic inputs. This is pervasive in ADK (700+ usages). Prefer exhaustive `if/elif` chains with a clear fallback.
**Guidelines:**
- Always include an `else` branch that raises `TypeError` or handles the unknown case.
- Prefer `isinstance(x, SomeType)` over `type(x) is SomeType` — it handles subclasses correctly.
- For checking multiple types: `isinstance(x, (TypeA, TypeB))`.
## No Asserts in Production Code
**Never use `assert` statements in production code.** They can be optimized away when Python runs with `-O` flags and provide poor error messages. Use specific exceptions like `ValueError`, `TypeError`, or `RuntimeError` instead.
@@ -0,0 +1,61 @@
# Visibility Style Guide
Python does not have native access modifiers (like `public`, `private`, or `package-private`). ADK relies on naming conventions and module structure to define visibility boundaries.
## Conventions
### 1. Module-Private / Internal Files
- **Private by Default**: All new `.py` module files under `src/google/adk/` must be private by default (prefixed with `_`). This is enforced by a pre-commit hook (`check-new-py-prefix`).
- Even if a file contains symbols intended for the public API, the file itself must have a leading underscore. The symbols are then exposed via the package's `__init__.py`.
- Files intended for internal use within a package or subsystem must also be prefixed with a leading underscore (e.g., `_task_models.py`).
- These files should **never** be imported directly by code outside of the ADK framework.
### 2. Class and Function Visibility
- **Public**: No leading underscore. Intended for use by consumers of the module or package.
- **Internal/Private**: Leading underscore (e.g., `_private_method()`). Intended only for use within the defining class or module.
### 3. Package-Private (Subsystem Visibility)
Since Python lacks true package-private access, we simulate it by:
- **Not exporting** the symbol in the package's `__init__.py`.
- Using `_`-prefixed modules for internal implementation details.
- Code within the same package can import from these `_` modules, but code outside should not.
- **Direct Imports Required**: Within the ADK framework, importing from `__init__.py` is **not allowed**. You must import from the specific module directly. This helps keep `__init__.py` minimal and keeps packages as self-contained as possible.
### 4. Public API Export
- The public API of a package must be explicitly exported in `__init__.py`.
- **Use `__all__`**: The `__init__.py` file should define `__all__` to explicitly list the symbols that are part of the public API.
- **Only public names** (symbols intended for use outside the package) should be imported into `__init__.py` and listed in `__all__`.
- Users should be able to import public symbols directly from the package level, rather than digging into internal modules.
## Examples
### Exposing a Public Interface
```python
# In src/google/adk/agents/llm/task/_task_agent.py (File is private by default)
class TaskAgent: # Public symbol
...
# In src/google/adk/agents/llm/task/__init__.py
from ._task_agent import TaskAgent
__all__ = [
'TaskAgent',
]
```
### Keeping Implementation Details Private
```python
# In src/google/adk/agents/llm/task/_task_models.py (Internal file)
class TaskRequest(BaseModel): # Public within the module, but module is private
...
# In src/google/adk/agents/llm/task/__init__.py
# We DO NOT export TaskRequest here if it is only for internal use within the task package.
```
+88
View File
@@ -0,0 +1,88 @@
---
name: adk-unit-design
description: Creates or updates code unit design documents for source code documentation.
---
# ADK Code Unit Design
This skill creates or updates a detailed software engineering design document for new or updated code file or specified code unit. The design document it generates is meant to explain the code to a developer who wants to modify or extend the code unit as part of the ADK development framework. Similar to a *unit test*, a *unit design* provides a generated software engineering design based on the *actual, implemented code* rather than any proposed code design or proposed software architecture.
## Input
- Code files containing new functionality
- Names of new methods and classes (optional)
- Code files for base classes or interfaces that the new functionality depends on (optional)
- Code unit tests (optional)
- Example code files (optional)
## Analysis
- Review specified code files for changes and named methods to determine:
- Purpose and intended use of the new or updated code units
- Any data flows handled by the new or updated code units
- Dependencies required by the new or updated code units
- Approaches for extending or customizing the code unit to add new capabilities
- Classes that depend on the new or updated code units
- Operational limitations of the new or updated code units
## Output
- Look for an existing design document in the `/docs/design/***` directory of this repository.
- If a design already exists, update the existing design incrementally and prioritize preserving the previous content as much as possible.
- If no design document exists, create a design file for the new code unit in the `/docs/design/***` directory of this repository, using the relative path of the code unit. For example, if the code unit is called `/topic/function/class.ext`, create a design document in the location `/docs/design/topic/function/class/index.md`.
- Any links to local code files should be translated to URL links to the `google/adk-python` repository on GitHub. For example, if the local code unit path is `***/adk-python/topic/function/class.ext#L93`, the URL to the code file should be `https://github.com/google/adk-python/blob/main/topic/function/class.ext#L93`.
### Design document structure and content
Use the following structure and instructions to create the design document for the code unit:
```
# (name of code unit or code file) - Code Unit Design
- 2-sentence summary of the code unit
## Introduction
- Paragraph(s) explaining:
- The purpose and application of the code unit, including intended use cases
- Developer problems solved by this code unit
- Agent capabilities enabled by this code unit
## High-level architecture
- Describe the software architecture of this code unit and how it fits into the larger ADK framework
- Explain general execution flow of this code unit
- Describe any data flows handled by the code unit including inputs and outputs
- Explain any cross-class dependencies of the code unit, including upstream dependencies and downstream dependencies
### Extension points
- Describe how the code unit could be extended or customized to add new features or capabilities
- Note specific parts of the code unit that are designed to be extended or customized, including:
- Abstract classes
- Interfaces
- Hooks
- Callbacks
- Configurable parameters
- Plugin architecture
- Other extension points
### Extension constraints
- Describe what parts of the code unit should not be modified, based on:
- architectural constraints
- implementation limitations
- cross-class dependencies
- other constraints
## Limitations
- Mention any limitations of the code unit, if known, such as:
- input constraints
- data structure constraints
- output constraints
- performance limitations
- memory limitations
- other limitations
```
+87
View File
@@ -0,0 +1,87 @@
---
name: adk-unit-guide
description: Creates detailed code unit guides for source code documentation.
---
# ADK code unit guide
This skill creates a detailed developer guide for new or updated code file or direct code input. The guide it generates is meant to explain the code to a developer who wants to use it in an application, but with a higher level of technical detail than what would appear in published developer documentation. Similar to a *unit test*, a *unit guide* provides generated, granular-level documentation for a unit of code, without worrying about bloating the actual developer documentation with too many details.
## Input
- Code files containing new functionality
- Code unit tests (optional)
- Code design files (optional)
- Names of new methods and classes (optional)
## Analysis
- Review the code design files, if provided. Make note of:
- Purpose and intended use of the new or updated code units
- Classes that depend on the new or updated code units
- Additional dependencies required by the new or updated code units
- Limitations of the new or updated code units
- Review specified code file for changes and named methods, if provided.
- Determine what classes and code files may depend on the new or updated code units.
## Output
- Look for an existing guide in the `/docs/guides/***` directory of this repository.
- If a guide already exists, update the existing guide incrementally and prioritize preserving the previous content as much as possible.
- If no guide exists, create a guide file for the new code unit in the `/docs/guides/***` directory of this repository, using the relative path of the code unit. For example, if the code unit is called `/topic/function/class.ext`, create a guide in the location `/docs/guides/topic/function/class/index.md`.
- **Update the Index**: Whenever a new guide is created, or an existing guide's title/summary changes, update the index file `/docs/guides/README.md`. Ensure the guide is listed under the correct category with a link and a brief summary.
### Guide structure and content
Use the following structure and instructions to create the guide for the code unit:
```
# Title: name of the code file or code unit
- 2-sentence summary of the code unit
## Introduction
- Paragraph(s) explaining:
- The purpose and application of the code unit
- Key classes that depend on this code unit
- Developer problems solved by this code unit
## Get started
- Present a single, minimum implementation of the code unit to demonstrate its use.
- Show enough of the containing classes to make it clear where the code could be used.
- Use unit test code as a starting point for the code example, if available.
- When writing a sample agent, do not set the `model` attribute.
- For workflow node samples, prefer using a simple Python function rather than extending `BaseNode` to demonstrate the node's logic, unless class extension is explicitly required for the use case.
- When wrapping Python functions as workflow nodes, prefer using the `@node` decorator instead of `FunctionNode` directly, whenever possible.
## How it works
- Explain how the code unit accomplishes its purpose or solves a problem.
- Mention key code classes that depend on this code unit.
- Mention code classes that this code unit depends on.
- Explain any cross-class dependencies of the code unit.
## Configuration options
- If the code unit has configuration options (e.g., settings, configuration objects), document them in a table detailing parameters, types, default values, and descriptions.
- **Do NOT** list options inherited from base classes. Focus only on options introduced by the code unit itself.
- Dive into each option to provide detailed description and usage patterns, rather than just repeating the type and a brief description.
- **Do NOT** list references of all attributes or methods of the classes. Exhaustive API references belong in auto-generated reference documentation, not in guides. Guides should focus on how to use the code unit.
## Advanced applications
- Determine if there are advanced use cases for the code unit.
- Add advanced applications of the code unit, including:
- Problem solved
- Implementations for special circumstances
## Limitations
- Mention any limitations of the code unit, if known.
## Related samples
- Link to relevant samples in the `contributing/` directory that demonstrate the use of this code unit.
```
+152
View File
@@ -0,0 +1,152 @@
---
name: adk-verify-snippets
description: >
Extracts and verifies the runnability and code coverage of all Python code blocks inside a Markdown file.
Generates a detailed compilation and execution report.
metadata:
author: Antigravity
version: 1.4.0
---
# Verify Markdown Snippets Skill
This skill extracts all ` ```python ` blocks from a Markdown file, executes each
one in a process-isolated environment using the bundled `run.py` harness, and
generates a structured report covering load status, run status, and line
coverage.
> [!CAUTION] **STRICT READ-ONLY CONSTRAINT — READ THIS BEFORE DOING ANYTHING
> ELSE**
>
> This skill is **read-only**. The agent **MUST NOT**: - **Modify** any file in
> the repository (source, test, config, docs, or skill files — including this
> SKILL.md). - **Delete** any file in the repository. - **Create** any new file
> in the repository.
>
> The **only two write operations permitted** are: 1. Writing temporary `.py`
> snippet files to a **system temp directory outside the repository**. 2.
> Writing the final `<filename>_REPORT.md` into the **same directory as the
> source Markdown file**.
>
> If in doubt, do not write. Any other mutation is a violation of this skill's
> contract.
--------------------------------------------------------------------------------
## 🔧 Prerequisites
1. **ADK Python environment**: Run from the repository root with the `uv`
virtual environment active.
2. **`coverage` package** *(optional)*: Enables per-snippet coverage reporting.
Without it, coverage columns show `—`.
```bash
uv pip install coverage
```
3. **Gemini API key**: Required only for snippets that instantiate an `Agent`,
`App`, or `Workflow` (which make live Gemini API calls). Set one of:
```bash
export GEMINI_API_KEY="your-key-here"
# or
export GOOGLE_API_KEY="your-key-here"
```
If both are set, `GEMINI_API_KEY` takes precedence.
--------------------------------------------------------------------------------
## 🛠️ Usage
```bash
uv run --no-sync python .agents/skills/adk-verify-snippets/scripts/verify_md.py <path_to_markdown_file.md>
```
The script prints progress for each snippet, then writes a report to
**`<filename>_REPORT.md`** in the same directory as the source file and prints
the full path on completion.
**Report contents:** :- **Executive Summary table** — one row per snippet:
preceding heading, Load phase status, Run phase status, coverage %, and error
detail.
- **Detailed section** — for each snippet: the extracted code block, full
execution logs (stdout + stderr/traceback), and the coverage report.
--------------------------------------------------------------------------------
## 📝 How Snippets Are Classified
Each ` ```python ` block falls into one of these categories:
### 1. Runnability Test (has a module-level ADK component)
If the snippet assigns a `Workflow`, `Agent`, or `App` to a **module-level
variable**, the runner executes it against the Gemini API.
- The variable name does not matter — the runner finds it automatically via
`vars(module)`.
- For multi-agent snippets, the runner identifies the root agent by excluding
any agent that appears in another agent's `sub_agents` list.
- To use a custom test prompt instead of the default `"Test input topic"`,
define a module-level `test_input` string in the snippet.
If no module-level ADK component is found, the run phase is skipped and the
report shows ` NO ADK COMPONENT`.
### 2. Loadability-Only (no ADK component)
The runner verifies the snippet compiles and imports without error. No API call
is made.
### 3. Skipped (annotated with ignore)
Place `<!-- verify-snippets: ignore -->` immediately before the opening
` ```python ` fence to exclude a block entirely. Use this for pseudo-code,
illustrative examples, or snippets that require external setup.
````markdown
<!-- verify-snippets: ignore -->
```python
# pseudo-code — not runnable as-is
my_agent = Agent(model="gemini-ultra-hypothetical", ...)
```
````
The report shows these as `⏭️ SKIPPED`.
--------------------------------------------------------------------------------
## ⚠️ Known Limitations
- **No shared state between snippets**: Each snippet runs in a fresh
subprocess with no imports or variables carried over from previous snippets.
A snippet that depends on code from an earlier block will fail with
`NameError` or `ImportError`. Make each snippet self-contained, or annotate
it with `<!-- verify-snippets: ignore -->`.
- **120-second timeout**: Each snippet is killed after 120 seconds. Annotate
long-running or blocking snippets with `<!-- verify-snippets: ignore -->`.
- **Ignore annotation placement**: The `<!-- verify-snippets: ignore -->`
annotation applies to the next ` ```python ` fence encountered. Blank lines
between the annotation and the fence are tolerated, but any non-blank line
(prose or a heading) cancels the annotation.
- **Bare ` ``` ` closes the block**: The parser closes a Python block on the
first bare ` ``` ` line (no language tag). A bare ` ``` ` appearing as
content inside a snippet (e.g. to demonstrate Markdown syntax) will
prematurely close the block. Annotate such snippets with
`<!-- verify-snippets: ignore -->`.
--------------------------------------------------------------------------------
## ⚠️ Behavioral Constraints (For AI Agents)
- **Read-only**: See the caution block at the top. The constraint is absolute.
- **Report only, do not fix**: The agent MUST NOT rewrite the source Markdown,
modify code blocks, or generate patches. Present the summary table to the
user and stop.
- **Present the summary table verbatim**: After the script completes, read the
generated `_REPORT.md` and copy the Executive Summary table to the user
**exactly as written** — same six columns, same order, no renaming or
dropping: `Snippet | Preceding Heading | Load Phase | Run Phase | Coverage |
Details`
@@ -0,0 +1,356 @@
# 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 __future__ import annotations
import argparse
import asyncio
import importlib.util
import os
from pathlib import Path
import sys
import traceback
# Sentinel string used by verify_md.py to locate and split the coverage section
# out of run.py's stdout. Keep in sync with verify_md.py:COV_SECTION_HEADER.
COV_SECTION_HEADER = "📊 Phase 4: Code Coverage Report"
# Structured exit codes — consumed by verify_md.py to classify results without
# fragile string/emoji matching. Keep in sync with verify_md.py:EXIT_* constants.
EXIT_SUCCESS = 0 # All phases passed
EXIT_LOAD_FAILURE = 1 # Failed to compile / load the snippet
EXIT_RUN_FAILURE = 2 # Loaded OK but the ADK component failed at runtime
EXIT_NO_COMPONENT = 3 # Loaded OK, no runnable ADK component found (load-only)
# --- Optional Coverage Integration ---
try:
import coverage
HAS_COVERAGE = True
except ImportError:
HAS_COVERAGE = False
# --- Imports for ADK Inspection ---
from google.adk.agents.base_agent import BaseAgent
from google.adk.apps import App
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from google.adk.workflow import Workflow
from google.genai import types
def load_target_module(file_path: Path):
"""Dynamically loads a Python file as a module, catching import/compilation/definition errors."""
# Use the absolute path string as the key to avoid collisions when multiple
# snippets share the same file stem or when the stem matches an installed package.
module_name = str(file_path)
spec = importlib.util.spec_from_file_location(module_name, file_path)
if spec is None or spec.loader is None:
raise ImportError(
f"Could not resolve module spec for file '{file_path.name}'"
)
module = importlib.util.module_from_spec(spec)
sys.modules[module_name] = module
# Executing the module runs all top-level code, which will catch:
# - SyntaxError / IndentationError
# - ImportError (e.g. from google.adk.workflow import build_node)
# - ValidationError (e.g. instantiating Workflow with invalid edges)
try:
spec.loader.exec_module(module)
except Exception:
# Remove the partially-initialised module so a broken entry is never
# left in sys.modules for the lifetime of this process. This matters
# when run.py is imported in-process (e.g. from a test harness) rather
# than invoked as a subprocess.
sys.modules.pop(module_name, None)
raise
return module
def discover_adk_component(module):
"""Scans the module namespace to discover runnable ADK components, prioritizing root components.
Uses two passes to correctly identify root agents regardless of the order
in which names appear in ``vars(module)``:
* Pass 1 — collect every Workflow, Agent, and App in the module namespace.
* Pass 2 — build the full set of sub-agent IDs from *all* collected agents,
then filter to find agents that are not sub-agents of any other agent.
Without the two-pass approach, a root agent whose variable name is seen
before its sub-agents (e.g. ``root`` defined above ``child`` in the file)
would be encountered first, before ``child``'s own sub-agents are registered,
causing incorrect root detection.
"""
workflows = []
agents = []
apps = []
# Pass 1: collect all candidate components.
#
# Use vars(module) rather than inspect.getmembers(module) because
# getmembers() invokes every attribute getter and silently swallows any
# Exception raised by broken descriptors or properties — a snippet that
# defines an Agent behind a faulty @property would simply be missing from
# the scan with no error or log entry. vars(module) reads the module's
# __dict__ directly, which never triggers descriptors and never suppresses
# exceptions, giving us an accurate view of module-level names.
for obj in vars(module).values():
if isinstance(obj, Workflow):
workflows.append(obj)
elif isinstance(obj, BaseAgent):
agents.append(obj)
elif isinstance(obj, App):
apps.append(obj)
# 1. Prefer Workflow
if workflows:
return workflows[0], "Workflow"
# Pass 2: build the complete sub-agent ID set now that all agents are known,
# then select the root (any agent not listed as a sub-agent of another).
#
# Read sub_agents into a local snapshot rather than calling the attribute
# twice. Calling it twice is unsafe when sub_agents is a non-idempotent
# property: the first call (guard) and the second call (iteration) could
# return different objects, causing id() values to diverge and root
# detection to silently misfire.
sub_agent_ids: set[int] = set()
for agent in agents:
children = getattr(agent, "sub_agents", None) or []
for sub in children:
sub_agent_ids.add(id(sub))
# 2. Find root Agent (not a sub-agent of any other agent in the module)
root_agents = [a for a in agents if id(a) not in sub_agent_ids]
if root_agents:
return root_agents[0], "Agent"
# 3. Fall back to App
if apps:
return apps[0], "App"
return None, None
async def run_component(component, component_type, test_input):
"""Unified runner to execute the discovered component."""
print(f"\n🔍 Discovered ADK {component_type} in target file.")
print(f"🚀 Running execution test with input: '{test_input}'...\n")
if component_type == "App":
runnable_node = getattr(component, "root_agent", None)
if runnable_node is None:
raise AttributeError(
f"App instance has no 'root_agent' attribute. "
f"Ensure the App is constructed with a root_agent argument."
)
else:
runnable_node = component
session_service = InMemorySessionService()
runner = Runner(
app_name="runnability_test",
node=runnable_node,
session_service=session_service,
)
session = await session_service.create_session(
app_name="runnability_test", user_id="tester"
)
user_message = types.Content(
parts=[types.Part(text=str(test_input))], role="user"
)
async for event in runner.run_async(
user_id="tester", session_id=session.id, new_message=user_message
):
print(f"🎬 [Event] Author: {event.author}")
if event.output:
print(f"🔹 Output: {event.output}")
if hasattr(event, "content") and event.content and event.content.parts:
text = "".join(p.text for p in event.content.parts if p.text)
if text:
print(f"📝 Content Output:\n{'-'*40}\n{text}\n{'-'*40}")
def main():
parser = argparse.ArgumentParser(
description="Generalized ADK Runnability & Loadability Tester"
)
parser.add_argument(
"file",
type=str,
help="Path to the python file containing the agent/workflow to test",
)
args = parser.parse_args()
file_path = Path(args.file).resolve()
if not file_path.exists():
print(f"❌ Error: File '{file_path}' does not exist.")
sys.exit(EXIT_LOAD_FAILURE)
print(f"🔬 Testing file: {file_path.name}")
print("=" * 60)
# Initialize coverage programmatically to track ONLY the target file.
#
# Implementation note: snippets are loaded via importlib/exec_module, which
# CPython's sys.settrace-based tracer instruments correctly *only* if the
# tracer is active before the module's code object is compiled and executed.
# Starting coverage here — before load_target_module() — satisfies that
# requirement. The `include` filter ensures no ADK library code is counted.
cov = None
if HAS_COVERAGE:
cov = coverage.Coverage(
branch=True,
data_file=None, # Keep coverage data in-memory only, no .coverage file needed
include=[str(file_path)], # Scope collection to the snippet file only
)
cov.start()
else:
print(
"️ Install 'coverage' package to enable automated code coverage"
" reporting."
)
# exit_code is set by each phase and consumed inside the finally block so
# that coverage reporting always runs before the process exits. Using a
# mutable list as a simple cell lets the finally clause read the value set
# by any code path (normal completion, early break-out via a flag, or an
# unexpected exception) without requiring nonlocal or a class wrapper.
exit_code = [EXIT_SUCCESS]
try:
# 1. Test Loadability (Imports, Syntax, Instantiation/Validation)
print("📋 Phase 1: Loading & Compiling...")
try:
module = load_target_module(file_path)
print(
f"✅ Load Success: '{file_path.name}' compiled and loaded without any"
" issues."
)
except Exception:
print(f"❌ Load Failure: Failed to compile/load '{file_path.name}':")
print("-" * 60)
traceback.print_exc(file=sys.stdout)
print("-" * 60)
exit_code[0] = EXIT_LOAD_FAILURE
# Do NOT return here. Fall through to the finally block so that
# coverage is reported and sys.exit() is called with the correct code.
# The module variable is not set, so we skip phases 23 via the flag.
else:
# 2. Discover Component (only reached when load succeeded)
print("\n📋 Phase 2: Component Discovery...")
component, comp_type = discover_adk_component(module)
if not component:
print(
" NO ADK COMPONENT: No module-level Workflow, Agent, or App"
f" instance found in '{file_path.name}'."
)
print(
" Runnability test skipped. To enable it, assign a Workflow,"
" Agent, or App"
)
print(
" to a module-level variable (e.g. `agent = Agent(...)`). The"
" variable name"
)
print(
" does not matter — the runner detects it automatically via"
" vars(module)."
)
print(
"️ Coverage below reflects load-time execution only (module-level"
" statements)."
)
exit_code[0] = EXIT_NO_COMPONENT
else:
# Get test input from module, or fallback
test_input = getattr(module, "test_input", "Test input topic")
# 3. Test Runnability
print(f"\n📋 Phase 3: Executing {comp_type}...")
try:
# asyncio.run() creates a fresh event loop each time, so it will raise
# RuntimeError if a loop is already running (e.g. the snippet called
# asyncio.run() at module level without an __main__ guard).
# We catch that specific case and report it clearly, rather than using
# the deprecated asyncio.get_event_loop() API (removed in Python 3.12+).
asyncio.run(run_component(component, comp_type, test_input))
print(
f"\n✅ Run Success: Component '{comp_type}' executed"
" successfully."
)
except RuntimeError as e:
if "event loop" in str(e).lower():
print(
f"\n❌ Run Failure: An event loop conflict was detected after"
f" module load."
)
print(
" The snippet likely called asyncio.run() at module level,"
" which"
)
print(
" conflicts with the runner's own event loop. Wrap top-level"
" async"
)
print(
" calls in an `if __name__ == '__main__':` guard, or"
" annotate the"
)
print(" snippet with <!-- verify-snippets: ignore -->.")
else:
print(f"\n❌ Run Failure: Component failed during execution:")
print("-" * 60)
traceback.print_exc(file=sys.stdout)
print("-" * 60)
exit_code[0] = EXIT_RUN_FAILURE
except Exception:
print(f"\n❌ Run Failure: Component failed during execution:")
print("-" * 60)
traceback.print_exc(file=sys.stdout)
print("-" * 60)
exit_code[0] = EXIT_RUN_FAILURE
finally:
# Coverage reporting runs here so it is guaranteed to execute on every
# code path: normal completion, load failure, no-component, run failure.
if cov:
cov.stop()
print(f"\n{COV_SECTION_HEADER} (Target File)")
print("=" * 60)
try:
# Report coverage of the target file directly to stdout
cov.report(morfs=[str(file_path)], file=sys.stdout)
except coverage.exceptions.NoDataError:
print(
"⚠️ No coverage data collected (compilation or execution failed"
" early)."
)
except Exception as ce:
print(f"⚠️ Failed to generate coverage report: {ce}")
print("=" * 60)
# Only call sys.exit for non-zero codes. If exit_code is EXIT_SUCCESS
# we return normally so that any exception currently propagating out of
# the try block is not silently replaced by a SystemExit raised here.
if exit_code[0] != EXIT_SUCCESS:
sys.exit(exit_code[0])
if __name__ == "__main__":
main()
@@ -0,0 +1,450 @@
# 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 __future__ import annotations
import argparse
from datetime import datetime
import os
from pathlib import Path
import re
import shutil
import subprocess
import sys
import tempfile
SKIP_ANNOTATION = "<!-- verify-snippets: ignore -->"
SNIPPET_TIMEOUT = 120 # seconds; adjust if snippets legitimately need longer
# Must match COV_SECTION_HEADER in run.py exactly — used to split coverage output
# from the main execution log when parsing run.py's stdout.
COV_SECTION_HEADER = "📊 Phase 4: Code Coverage Report"
# Structured exit codes from run.py — kept in sync with run.py:EXIT_* constants.
# Using exit codes (not string/emoji matching) makes classification robust to
# future changes in run.py's human-readable output text.
EXIT_SUCCESS = 0 # All phases passed
EXIT_LOAD_FAILURE = 1 # Failed to compile / load the snippet
EXIT_RUN_FAILURE = 2 # Loaded OK but the ADK component failed at runtime
EXIT_NO_COMPONENT = 3 # Loaded OK, no runnable ADK component found (load-only)
def extract_snippets(md_path: Path):
"""Parses a markdown file and extracts python code blocks along with their preceding headings.
A code block immediately preceded by the HTML comment
``<!-- verify-snippets: ignore -->`` is recorded but marked as skipped so
that illustrative / pseudo-code examples are excluded from execution.
"""
with open(md_path, "r", encoding="utf-8") as f:
content = f.read()
lines = content.splitlines()
snippets = []
current_heading = "Top Level"
in_code_block = False
code_lines = []
skip_next_block = False
for line in lines:
# If we are inside a code block, handle it first to preserve comments starting with '#'
if in_code_block:
stripped = line.strip()
# Close only on a bare closing fence (``` with no language specifier).
# A fenced block of another language (e.g. ```bash) appearing *inside*
# the Python block will not trigger this branch because it carries a
# language tag, so it is appended to code_lines as literal content.
if stripped == "```":
in_code_block = False
code_text = "\n".join(code_lines)
snippets.append({
"heading": current_heading,
"code": code_text,
"skip": skip_next_block,
})
skip_next_block = False
else:
code_lines.append(line)
continue
# If we are outside a code block, check for headings or code block starts
if line.startswith("#"):
# Clean up heading markers (e.g., "## Get started" -> "Get started")
current_heading = line.lstrip("#").strip()
# A heading between the annotation and the fence cancels the skip.
skip_next_block = False
continue
if line.strip() == SKIP_ANNOTATION:
skip_next_block = True
continue
if line.strip().startswith("```python"):
in_code_block = True
code_lines = []
continue
# Any other non-empty line (prose, blank-line-separated text, etc.) between
# the annotation and the fence cancels the skip.
if line.strip():
skip_next_block = False
return snippets
def run_snippet(run_py_path: Path, snippet_path: Path):
"""Executes run.py on the isolated snippet and returns the result."""
# Run using the same Python interpreter as this script (which will be the venv's python)
cmd = [sys.executable, str(run_py_path), str(snippet_path)]
# Ensure GEMINI_API_KEY is preferred if both keys are set in the environment
env = os.environ.copy()
if "GOOGLE_API_KEY" in env and "GEMINI_API_KEY" in env:
env.pop("GOOGLE_API_KEY", None)
try:
result = subprocess.run(
cmd, capture_output=True, text=True, env=env, timeout=SNIPPET_TIMEOUT
)
return {
"exit_code": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
}
except subprocess.TimeoutExpired:
return {
"exit_code": EXIT_RUN_FAILURE,
"stdout": (
"❌ Run Failure: Snippet execution timed out after"
f" {SNIPPET_TIMEOUT} seconds."
),
"stderr": (
"TimeoutExpired: The snippet process did not complete within the"
f" {SNIPPET_TIMEOUT}-second limit."
),
}
def extract_error_detail(stdout: str, stderr: str) -> str:
"""Extracts the most relevant error line from run.py's output.
Searches in order:
1. Last line in stderr that looks like a Python exception (``<Name>Error:``
or ``<Name>Exception:``). Scoping to stderr avoids matching runner prose
in stdout (e.g. "❌ Run Failure: ...") which contains words like "Failure"
but is not an exception line.
2. Last line in stdout with the same pattern, as a fallback for runtimes that
write tracebacks to stdout instead of stderr.
3. Last line in stderr matching the generic ``<ClassName>: <detail>`` format
(custom exception classes that don't end in Error/Exception).
4. Fallback string if nothing matches.
"""
# Matches standard Python exception class names: ends in 'Error' or 'Exception',
# followed by a colon and detail text. Anchored to the start of the stripped line
# so runner prose ("❌ Run Failure: ...") is not matched.
_exception_re = re.compile(r"^[A-Za-z]\w*(?:Error|Exception|Warning):\s*.+")
for source in (stderr, stdout):
for line in reversed(source.splitlines()):
if _exception_re.match(line.strip()):
return f"`{line.strip()}`"
# Pass 3: generic '<ClassName>: <detail>' in stderr only
for line in reversed(stderr.splitlines()):
if re.match(r"^[A-Za-z]\w*:.+", line.strip()):
return f"`{line.strip()}`"
return "Failed to compile/load."
def clean_name(name: str):
"""Sanitizes a string to be a safe filename."""
name = name.lower().replace(" ", "_")
return re.sub(r"[^a-z0-9_]", "", name)
def md_cell(value: str) -> str:
"""Escapes pipe characters so the value is safe inside a Markdown table cell."""
return value.replace("|", r"\|")
def safe_fence(content: str, language: str = "") -> str:
"""Returns a Markdown fenced code block that safely wraps *content*.
Picks the shortest fence (minimum three backticks) that is strictly longer
than any contiguous run of backticks found inside *content*, so the fence
cannot be prematurely closed by content that itself contains backtick runs.
This is the approach recommended by the CommonMark spec.
Example::
safe_fence("x = ```foo```", "python")
# returns:
# ````python
# x = ```foo```
# ````
"""
# Find the longest run of backticks inside the content
max_run = max(
(len(m.group()) for m in re.finditer(r"`+", content)), default=0
)
# The outer fence must be strictly longer, and at least 3 characters
fence_len = max(3, max_run + 1)
fence = "`" * fence_len
tag = f"{fence}{language}\n" if language else f"{fence}\n"
return f"{tag}{content}\n{fence}"
def main():
parser = argparse.ArgumentParser(description="Markdown Snippet Verifier")
parser.add_argument(
"file", type=str, help="Path to the markdown file to verify"
)
args = parser.parse_args()
md_path = Path(args.file).resolve()
if not md_path.exists():
print(f"❌ Error: Markdown file '{md_path}' does not exist.")
sys.exit(1)
# Locate run.py bundled inside the same scripts folder as verify_md.py (portable mode!)
run_py_path = Path(__file__).parent / "run.py"
if not run_py_path.exists():
print(f"❌ Error: Bundled runner 'run.py' not found at '{run_py_path}'.")
sys.exit(1)
print(f"🔬 Analyzing Markdown: {md_path.name}")
# 1. Extract snippets
snippets = extract_snippets(md_path)
if not snippets:
print(f"⚠️ No python code blocks found in '{md_path.name}'.")
sys.exit(0)
print(f"📋 Found {len(snippets)} python code snippets to verify.")
# Create a unique temp directory to avoid collisions with concurrent runs
temp_dir = Path(tempfile.mkdtemp(prefix="verify_snippets_"))
results = []
# 2. Execute each snippet, then write the report — both inside the try so
# the finally cleanup only runs after the report is fully written.
try:
for i, snippet in enumerate(snippets, start=1):
heading = snippet["heading"]
code = snippet["code"]
is_skipped = snippet.get("skip", False)
# Create a unique, sanitized filename for the snippet
safe_heading = clean_name(heading)
temp_file_name = f"snippet_{i}_{safe_heading}.py"
temp_file_path = temp_dir / temp_file_name
if is_skipped:
print(
f"⏭️ Skipping Snippet {i}/{len(snippets)} under heading"
f" '{heading}' (marked ignore)."
)
results.append({
"index": i,
"heading": heading,
"code": code,
"temp_file": temp_file_name,
"exit_code": 0,
"stdout": "",
"stderr": "",
"skipped": True,
})
continue
# Write snippet to file
with open(temp_file_path, "w", encoding="utf-8") as f:
f.write(code)
print(
f"🧪 Testing Snippet {i}/{len(snippets)} under heading '{heading}'..."
)
# Run the snippet
run_res = run_snippet(run_py_path, temp_file_path)
results.append({
"index": i,
"heading": heading,
"code": code,
"temp_file": temp_file_name,
"exit_code": run_res["exit_code"],
"stdout": run_res["stdout"],
"stderr": run_res["stderr"],
"skipped": False,
})
# 3. Generate Markdown Report — inside the try so finally runs after this completes.
# Use clean_name on the stem so the report path is safe on all filesystems.
# If clean_name strips everything (e.g. a fully non-ASCII filename), fall
# back to a hash of the original stem so two such files in the same
# directory never produce the same report path.
safe_stem = clean_name(md_path.stem) or f"report_{abs(hash(md_path.stem))}"
report_path = md_path.parent / f"{safe_stem}_REPORT.md"
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
with open(report_path, "w", encoding="utf-8") as f:
f.write(f"# 🔬 ADK Markdown Snippet Verification Report\n\n")
f.write(f"* **Source File**: [{md_path.name}](file://{md_path})\n")
f.write(f"* **Verified On**: `{timestamp}`\n\n")
# Write summary table
f.write("## 📈 Executive Summary\n\n")
f.write(
"| Snippet | Preceding Heading | Load Phase | Run Phase | Coverage |"
" Details |\n"
)
f.write("| :--- | :--- | :---: | :---: | :---: | :--- |\n")
for r in results:
# Handle explicitly skipped snippets
if r.get("skipped"):
f.write(
f"| **Snippet {r['index']}** | `{md_cell(r['heading'])}` | ⏭️"
f" **SKIPPED** | ⏭️ **SKIPPED** | — | Marked `{SKIP_ANNOTATION}`"
" — intentionally ignored. |\n"
)
continue
# Determine Phase 1 (Load) and Phase 3 (Run) statuses from the
# structured exit code emitted by run.py — no emoji/string matching.
exit_code = r["exit_code"]
load_status = "✅ **PASS**"
run_status = "✅ **PASS**"
coverage_pct = ""
stdout_and_stderr = r["stdout"] + "\n" + r["stderr"]
if exit_code == EXIT_LOAD_FAILURE:
load_status = "❌ **FAIL**"
run_status = " **SKIPPED**"
elif exit_code == EXIT_NO_COMPONENT:
run_status = " **NO ADK COMPONENT**"
elif exit_code == EXIT_RUN_FAILURE:
run_status = "❌ **FAIL**"
# EXIT_SUCCESS (0): both statuses remain ✅ **PASS**
# 3. Parse Coverage — anchor to line start to avoid matching prose.
# Handles both branch (5 numeric cols) and non-branch (3 cols) formats.
total_match = re.search(
r"^TOTAL(?:\s+\d+)+\s+(\d+)%", r["stdout"], re.MULTILINE
)
if total_match and load_status != "❌ **FAIL**":
coverage_pct = f"`{total_match.group(1)}`"
# 4. Formulate details and handle transient 503s
details = "All checks passed successfully."
if load_status == "❌ **FAIL**":
details = extract_error_detail(r["stdout"], r["stderr"])
elif run_status == " **NO ADK COMPONENT**":
details = (
"No module-level `Workflow`, `Agent`, or `App` instance found."
" Assign one to a top-level variable to enable runnability"
" testing."
)
elif run_status == "❌ **FAIL**":
if "503" in stdout_and_stderr and "UNAVAILABLE" in stdout_and_stderr:
details = (
"⚠️ **Transient 503 from Gemini API (overloaded)**. Code"
" structure is correct."
)
else:
details = extract_error_detail(r["stdout"], r["stderr"])
# Store statuses for reuse in the detailed section
r["load_status"] = load_status
r["run_status"] = run_status
f.write(
f"| **Snippet {r['index']}** | `{md_cell(r['heading'])}` |"
f" {load_status} | {run_status} | {coverage_pct} |"
f" {md_cell(details)} |\n"
)
f.write("\n---\n\n## 🔍 Detailed Snippet Reports\n\n")
for r in results:
if r.get("skipped"):
f.write(
f"### ⏭️ Snippet {r['index']}: `{r['heading']}` *(ignored)*\n\n"
)
f.write("#### 📝 Code Block\n")
f.write(safe_fence(r["code"], "python"))
f.write("\n\n")
f.write(
"> This snippet was skipped because it is annotated with"
f" `{SKIP_ANNOTATION}`.\n\n"
)
f.write("---\n\n")
continue
l_stat = r.get("load_status", "✅ **PASS**")
r_stat = r.get("run_status", "✅ **PASS**")
if l_stat == "❌ **FAIL**" or r_stat == "❌ **FAIL**":
status_icon = ""
elif r_stat == " **NO ADK COMPONENT**":
status_icon = ""
else:
status_icon = ""
f.write(f"### {status_icon} Snippet {r['index']}: `{r['heading']}`\n\n")
f.write("#### 📝 Code Block\n")
f.write(safe_fence(r["code"], "python"))
f.write("\n\n")
# Write stdout / stderr logs
# Split run.py stdout into main log and coverage section using the
# shared COV_SECTION_HEADER constant (kept in sync with run.py).
stdout_clean = r["stdout"]
cov_section_match = re.search(
rf"({re.escape(COV_SECTION_HEADER)}.*)", r["stdout"], re.DOTALL
)
cov_text = cov_section_match.group(1) if cov_section_match else None
if cov_text:
stdout_clean = r["stdout"].replace(cov_text, "").strip()
log_content = stdout_clean
if r["stderr"]:
log_content += "\n\n=== STDERR/TRACEBACK ===\n" + r["stderr"].strip()
f.write("#### 🖥️ Loadability & Runnability Logs\n")
f.write(safe_fence(log_content))
f.write("\n\n")
# Write coverage report if available
if cov_text:
f.write("#### 📊 Coverage Report\n")
f.write(safe_fence(cov_text))
f.write("\n\n")
f.write("---\n\n")
print(f"🎉 Verification complete! Report generated at: {report_path}")
finally:
# Always clean up the temp directory, even on Ctrl+C or unexpected errors.
# This runs after report generation completes (or if it raises), ensuring
# temp files are never left behind.
shutil.rmtree(temp_dir, ignore_errors=True)
if __name__ == "__main__":
main()
+3
View File
@@ -0,0 +1,3 @@
{
"contextFileName": "AGENTS.md"
}
+3
View File
@@ -0,0 +1,3 @@
{
".": "2.0.0-alpha.1"
}
+3
View File
@@ -0,0 +1,3 @@
{
".": "2.4.0"
}
+75
View File
@@ -0,0 +1,75 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
## 🔴 Required Information
*Please ensure all items in this section are completed to allow for efficient
triaging. Requests without complete information may be rejected / deprioritized.
If an item is not applicable to you - please mark it as N/A*
**Describe the Bug:**
A clear and concise description of what the bug is.
**Steps to Reproduce:**
Please provide a numbered list of steps to reproduce the behavior:
1. Install '...'
2. Run '....'
3. Open '....'
4. Provide error or stacktrace
**Expected Behavior:**
A clear and concise description of what you expected to happen.
**Observed Behavior:**
What actually happened? Include error messages or crash stack traces here.
**Environment Details:**
- ADK Library Version (pip show google-adk):
- Desktop OS:** [e.g., macOS, Linux, Windows]
- Python Version (python -V):
**Model Information:**
- Are you using LiteLLM: Yes/No
- Which model is being used: (e.g., gemini-2.5-pro)
---
## 🟡 Optional Information
*Providing this information greatly speeds up the resolution process.*
**Regression:**
Did this work in a previous version of ADK? If so, which one?
**Logs:**
Please attach relevant logs. Wrap them in code blocks (```) or attach a
text file.
```text
// Paste logs here
```
**Screenshots / Video:**
If applicable, add screenshots or screen recordings to help explain
your problem.
**Additional Context:**
Add any other context about the problem here.
**Minimal Reproduction Code:**
Please provide a code snippet or a link to a Gist/repo that isolates the issue.
```python
// Code snippet here
```
**How often has this issue occurred?:**
- Always (100%)
- Often (50%+)
- Intermittently (<50%)
- Once / Rare
+48
View File
@@ -0,0 +1,48 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
** Please make sure you read the contribution guide and file the issues in the right place. **
[Contribution guide.](https://google.github.io/adk-docs/contributing-guide/)
## 🔴 Required Information
*Please ensure all items in this section are completed to allow for efficient
triaging. Requests without complete information may be rejected / deprioritized.
If an item is not applicable to you - please mark it as N/A*
### Is your feature request related to a specific problem?
Please describe the problem you are trying to solve. (Ex: "I'm always frustrated
when I have to manually handle X...")
### Describe the Solution You'd Like
A clear and concise description of the feature or API change you want.
Be specific about input/outputs if this involves an API change.
### Impact on your work
How does this feature impact your work and what are you trying to achieve?
If this is critical for you, tell us if there is a timeline by when you need
this feature.
### Willingness to contribute
Are you interested in implementing this feature yourself or submitting a PR?
(Yes/No)
---
## 🟡 Recommended Information
### Describe Alternatives You've Considered
A clear and concise description of any alternative solutions or workarounds
you've considered and why they didn't work for you.
### Proposed API / Implementation
If you have ideas on how this should look in code, please share a
pseudo-code example.
### Additional Context
Add any other context or screenshots about the feature request here.
+28
View File
@@ -0,0 +1,28 @@
# 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.
allowedCopyrightHolders:
- 'Google LLC'
allowedLicenses:
- 'Apache-2.0'
- 'MIT'
- 'BSD-3'
sourceFileExtensions:
- 'ts'
- 'js'
- 'java'
- 'py'
- 'yaml'
- 'yml'
ignoreFiles:
- 'src/google/adk/cli/browser/**'
+52
View File
@@ -0,0 +1,52 @@
**Please ensure you have read the [contribution guide](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) before creating a pull request.**
### Link to Issue or Description of Change
**1. Link to an existing issue (if applicable):**
- Closes: #_issue_number_
- Related: #_issue_number_
**2. Or, if no issue exists, describe the change:**
_If applicable, please follow the issue templates to provide as much detail as
possible._
**Problem:**
_A clear and concise description of what the problem is._
**Solution:**
_A clear and concise description of what you want to happen and why you choose
this solution._
### Testing Plan
_Please describe the tests that you ran to verify your changes. This is required
for all PRs that are not small documentation or typo fixes._
**Unit Tests:**
- [ ] I have added or updated unit tests for my change.
- [ ] All unit tests pass locally.
_Please include a summary of passed `pytest` results._
**Manual End-to-End (E2E) Tests:**
_Please provide instructions on how to manually test your changes, including any
necessary setup or configuration. Please provide logs or screenshots to help
reviewers better understand the fix._
### Checklist
- [ ] I have read the [CONTRIBUTING.md](https://github.com/google/adk-python/blob/main/CONTRIBUTING.md) document.
- [ ] I have performed a self-review of my own code.
- [ ] I have commented my code, particularly in hard-to-understand areas.
- [ ] I have added tests that prove my fix is effective or that my feature works.
- [ ] New and existing unit tests pass locally with my changes.
- [ ] I have manually tested my changes end-to-end.
- [ ] Any dependent changes have been merged and published in downstream modules.
### Additional context
_Add any other context or screenshots about the feature request here._
+64
View File
@@ -0,0 +1,64 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"release-type": "python",
"versioning": "prerelease",
"prerelease": true,
"prerelease-type": "alpha",
"package-name": "google-adk",
"include-component-in-tag": false,
"skip-github-release": true,
"changelog-path": "CHANGELOG-v2.md",
"changelog-sections": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "perf",
"section": "Performance Improvements"
},
{
"type": "refactor",
"section": "Code Refactoring",
"hidden": true
},
{
"type": "docs",
"section": "Documentation"
},
{
"type": "test",
"section": "Tests",
"hidden": true
},
{
"type": "build",
"section": "Build System",
"hidden": true
},
{
"type": "ci",
"section": "CI/CD",
"hidden": true
},
{
"type": "style",
"section": "Styles",
"hidden": true
},
{
"type": "chore",
"section": "Miscellaneous Chores",
"hidden": true
}
]
}
},
"last-release-sha": "4af7cbb5c8319208337e18b0a6bc55288b51b0b1"
}
+61
View File
@@ -0,0 +1,61 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"packages": {
".": {
"release-type": "python",
"package-name": "google-adk",
"include-component-in-tag": false,
"skip-github-release": true,
"changelog-path": "CHANGELOG.md",
"changelog-sections": [
{
"type": "feat",
"section": "Features"
},
{
"type": "fix",
"section": "Bug Fixes"
},
{
"type": "perf",
"section": "Performance Improvements"
},
{
"type": "refactor",
"section": "Code Refactoring",
"hidden": true
},
{
"type": "docs",
"section": "Documentation"
},
{
"type": "test",
"section": "Tests",
"hidden": true
},
{
"type": "build",
"section": "Build System",
"hidden": true
},
{
"type": "ci",
"section": "CI/CD",
"hidden": true
},
{
"type": "style",
"section": "Styles",
"hidden": true
},
{
"type": "chore",
"section": "Miscellaneous Chores",
"hidden": true
}
]
}
},
"last-release-sha": "44d747ed5eaf543b5b8d22e0088f8a7c7eeee846"
}
@@ -0,0 +1,108 @@
# 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.
name: Analyze New Release for ADK Docs Updates
on:
# Runs on every new release.
release:
types: [published]
# Manual trigger for testing and retrying.
workflow_dispatch:
inputs:
resume:
description: 'Resume from the last failed/interrupted run'
required: false
type: boolean
default: false
start_tag:
description: 'Older release tag (base), e.g. v1.26.0'
required: false
type: string
end_tag:
description: 'Newer release tag (head), e.g. v1.27.0'
required: false
type: string
jobs:
analyze-new-release-for-adk-docs-updates:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Load adk-bot SSH Private Key
uses: webfactory/ssh-agent@v0.9.1
with:
ssh-private-key: ${{ secrets.ADK_BOT_SSH_PRIVATE_KEY }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests "google-adk[db]"
- name: Restore session DB from cache
if: ${{ github.event.inputs.resume == 'true' }}
uses: actions/cache/restore@v4
with:
path: contributing/samples/adk_team/adk_documentation/adk_release_analyzer/sessions.db
key: analyzer-session-db-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
analyzer-session-db-
- name: Run Analyzing Script
env:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY_FOR_DOCS_AGENTS }}
GOOGLE_GENAI_USE_VERTEXAI: 0
DOC_OWNER: 'google'
CODE_OWNER: 'google'
DOC_REPO: 'adk-docs'
CODE_REPO: 'adk-python'
INTERACTIVE: 0
PYTHONPATH: contributing/samples/adk_team
ANALYZER_RESUME: ${{ github.event.inputs.resume }}
ANALYZER_START_TAG: ${{ github.event.inputs.start_tag }}
ANALYZER_END_TAG: ${{ github.event.inputs.end_tag }}
shell: bash
run: |
set -euo pipefail
args=()
if [[ "${ANALYZER_RESUME:-false}" == "true" ]]; then
args+=(--resume)
fi
if [[ -n "${ANALYZER_START_TAG:-}" ]]; then
args+=(--start-tag "$ANALYZER_START_TAG")
fi
if [[ -n "${ANALYZER_END_TAG:-}" ]]; then
args+=(--end-tag "$ANALYZER_END_TAG")
fi
python -m adk_documentation.adk_release_analyzer.main "${args[@]}"
- name: Save session DB to cache
if: always()
uses: actions/cache/save@v4
with:
path: contributing/samples/adk_team/adk_documentation/adk_release_analyzer/sessions.db
key: analyzer-session-db-${{ github.run_id }}-${{ github.run_attempt }}
+31
View File
@@ -0,0 +1,31 @@
# 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.
name: Do Not Merge on GitHub
on:
pull_request:
branches: [main]
types: [opened, reopened, synchronize]
jobs:
block-merge:
if: github.repository == 'google/adk-python'
name: maintainers will submit via Copybara
runs-on: ubuntu-latest
steps:
- name: Explain why merging is blocked
run: |
echo "::error title=GitHub merge is disabled::Do NOT merge this pull request on GitHub. A maintainer will land the change internally, and Copybara will sync it back to this repository automatically."
exit 1
@@ -0,0 +1,194 @@
# 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.
name: Continuous Integration
on:
push:
branches: [main, v1]
paths:
- '**.py'
- '.pre-commit-config.yaml'
- 'pyproject.toml'
- 'tests/**'
pull_request:
branches: [main, v1]
paths:
- '**.py'
- '.pre-commit-config.yaml'
- 'pyproject.toml'
- 'tests/**'
permissions:
contents: read
jobs:
# 1. Code format and linting (Linter)
lint:
name: Pre-commit Linter
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v6
- name: Run pre-commit checks
uses: pre-commit/action@v3.0.1
# 2. Static type analysis (Mypy Check with Matrix)
# Compares new changes against the target base branch dynamically to support v1.
type-check:
name: Mypy Check (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.10', '3.11', '3.12', '3.13']
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Generate Baseline
env:
TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}
run: |
# Switch to target base branch to generate baseline
git checkout origin/$TARGET_BRANCH
git checkout ${{ github.sha }} -- pyproject.toml
# Install dependencies for target branch
uv venv .venv
source .venv/bin/activate
uv sync --all-extras
# Run mypy, filter for errors only, remove line numbers, and sort
# We ignore exit code (|| true) because we expect errors on baseline
uv run mypy . | grep "error:" | sed 's/:\([0-9]\+\):/::/g' | sort > baseline_errors.txt || true
echo "Found $(wc -l < baseline_errors.txt) errors on $TARGET_BRANCH."
- name: Check PR Branch
run: |
# Switch back to the PR commit
git checkout ${{ github.sha }}
# Re-sync dependencies in case the PR changed them
source .venv/bin/activate
uv sync --all-extras
# Run mypy on PR code, apply same processing
uv run mypy . | grep "error:" | sed 's/:\([0-9]\+\):/::/g' | sort > pr_errors.txt || true
echo "Found $(wc -l < pr_errors.txt) errors on PR branch."
- name: Compare and Fail on New Errors
run: |
# 'comm -13' suppresses unique lines in file1 (baseline) and common lines,
# leaving only lines unique to file2 (PR) -> The new errors.
comm -13 baseline_errors.txt pr_errors.txt > new_errors.txt
if [ -s new_errors.txt ]; then
echo "::error::The following NEW mypy errors were introduced:"
cat new_errors.txt
exit 1
else
echo "Great job! No new mypy errors introduced."
fi
# 3a. Unit testing (Unit Tests with Matrix)
unit-test:
name: Unit Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install dependencies
run: |
uv venv .venv
source .venv/bin/activate
uv sync --extra test
- name: Run unit tests with pytest
run: |
source .venv/bin/activate
pytest tests/unittests \
-n auto \
--ignore=tests/unittests/artifacts/test_artifact_service.py \
--ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py
# 3b. Dual-version A2A coverage: This job pins a2a-sdk to 0.3.x and
# runs only the A2A-related tests, so the a2a/_compat.py shim stays
# verified against both SDK majors.
# TODO: Remove this 0.3.x re-run once a2a-sdk 0.3.x support is dropped.
unit-test-a2a-v0-3:
name: A2A v0.3 Tests (Python ${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v7
with:
enable-cache: true
- name: Install dependencies
run: |
uv venv .venv
source .venv/bin/activate
uv sync --extra test
- name: Run A2A tests against a2a-sdk v0.3
run: |
source .venv/bin/activate
uv pip install --reinstall-package a2a-sdk 'a2a-sdk>=0.3.4,<0.4'
pytest tests/unittests/a2a \
tests/unittests/agents/test_remote_a2a_agent.py \
tests/unittests/integrations/agent_registry/test_agent_registry.py
+163
View File
@@ -0,0 +1,163 @@
# 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.
name: Copybara PR Handler
on:
push:
branches:
- main
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to close (for testing)'
required: true
type: string
commit_sha:
description: 'Commit SHA reference (optional, for testing)'
required: false
type: string
jobs:
close-imported-pr:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
contents: read
steps:
- name: Check for Copybara commits and close PRs
uses: actions/github-script@v8
with:
github-token: ${{ secrets.ADK_TRIAGE_AGENT }}
script: |
// Check if this is a manual test run
const isManualRun = context.eventName === 'workflow_dispatch';
let prsToClose = [];
if (isManualRun) {
// Manual testing mode
const prNumber = parseInt(context.payload.inputs.pr_number);
const commitSha = context.payload.inputs.commit_sha || context.sha.substring(0, 7);
console.log('=== MANUAL TEST MODE ===');
console.log(`Testing with PR #${prNumber}, commit ${commitSha}`);
prsToClose.push({ prNumber, commitSha });
} else {
// Normal mode: process commits from push event
const commits = context.payload.commits || [];
console.log(`Found ${commits.length} commit(s) in this push`);
// Process each commit
for (const commit of commits) {
const sha = commit.id;
const committer = commit.committer.name;
const message = commit.message;
console.log(`\n--- Processing commit ${sha.substring(0, 7)} ---`);
console.log(`Committer: ${committer}`);
// Check if this is a Copybara commit or has a pull request reference
const prRegex = /Merges?:?\s+(?:https:\/\/github\.com\/google\/adk-python\/pull\/|#)(\d+)/i;
const isCopybara = committer === 'Copybara-Service' ||
commit.author?.email === 'genai-sdk-bot@google.com' ||
message.includes('GitOrigin-RevId:') ||
message.includes('PiperOrigin-RevId:') ||
prRegex.test(message);
if (!isCopybara) {
console.log('Not a Copybara commit, skipping');
continue;
}
// Extract PR number from commit message
const prMatch = message.match(prRegex);
if (!prMatch) {
console.log('No PR number found in Copybara commit message');
continue;
}
const prNumber = parseInt(prMatch[1]);
const commitSha = sha.substring(0, 7);
prsToClose.push({ prNumber, commitSha });
}
}
// Process PRs to close
for (const { prNumber, commitSha } of prsToClose) {
console.log(`\n--- Processing PR #${prNumber} ---`);
// Get PR details to check if it's open
let pr;
try {
pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
} catch (error) {
console.log(`PR #${prNumber} not found or inaccessible:`, error.message);
continue;
}
// Only close if PR is still open
if (pr.data.state !== 'open') {
console.log(`PR #${prNumber} is already ${pr.data.state}, skipping`);
continue;
}
const author = pr.data.user.login;
try {
// Add comment with commit reference
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
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.`
});
// Add 'merged' label to the PR
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
labels: ['merged']
});
// Close the PR
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber,
state: 'closed'
});
console.log(`Successfully closed PR #${prNumber}`);
} catch (error) {
console.log(`Error closing PR #${prNumber}:`, error.message);
}
}
if (isManualRun) {
console.log('\n=== TEST COMPLETED ===');
} else {
console.log('\n--- Finished processing all commits ---');
}
@@ -0,0 +1,71 @@
# 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.
name: ADK Answering Agent for Discussions
on:
discussion:
types: [created]
discussion_comment:
types: [created]
permissions:
contents: read
jobs:
agent-answer-questions:
if: >-
github.repository == 'google/adk-python' && (
(github.event_name == 'discussion' && github.event.discussion.category.name == 'Q&A') ||
(github.event_name == 'discussion_comment' && contains(github.event.comment.body, '@adk-bot') && github.event.sender.login != 'adk-bot')
)
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Authenticate to Google Cloud
id: auth
uses: 'google-github-actions/auth@v3'
with:
credentials_json: '${{ secrets.ADK_GCP_SA_KEY }}'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install google-adk google-cloud-discoveryengine
- name: Run Answering Script
env:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
ADK_GCP_SA_KEY: ${{ secrets.ADK_GCP_SA_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION }}
VERTEXAI_DATASTORE_ID: ${{ secrets.VERTEXAI_DATASTORE_ID }}
GEMINI_API_DATASTORE_ID: ${{ secrets.GEMINI_API_DATASTORE_ID }}
GOOGLE_GENAI_USE_VERTEXAI: 1
OWNER: 'google'
REPO: 'adk-python'
INTERACTIVE: 0
PYTHONPATH: contributing/samples/adk_team
DISCUSSION_JSON: ${{ toJson(github.event.discussion) }}
run: |
python -c "import os; open('/tmp/discussion.json', 'w').write(os.environ['DISCUSSION_JSON'])"
python -m adk_answering_agent.main --discussion-file /tmp/discussion.json
+100
View File
@@ -0,0 +1,100 @@
# 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.
name: "ADK Issue Tracker Maintenance"
on:
# Daily background cleanup at 6:00 AM UTC (10 PM PST)
schedule:
- cron: '0 6 * * *'
# Allows manual triggering from the Actions console
workflow_dispatch:
inputs:
run_spam_monitor:
description: 'Run Issue Monitoring Agent (Spam Sweep)'
type: boolean
default: true
full_scan:
description: 'For Issue Monitoring: Run an Initial Full Scan of ALL open issues'
type: boolean
default: false
run_stale_auditor:
description: 'Run Stale Issue Auditor Agent'
type: boolean
default: true
permissions:
issues: write
contents: read
env:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
OWNER: ${{ github.repository_owner }}
REPO: adk-python
CONCURRENCY_LIMIT: 3
LLM_MODEL_NAME: "gemini-3.5-flash"
PYTHONPATH: contributing/samples/adk_team
jobs:
# 1. Sweep for spam, advertising, and invalid issues
sweep-spam:
if: |
github.repository == 'google/adk-python' &&
(github.event_name == 'schedule' || github.event.inputs.run_spam_monitor == 'true')
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests google-adk python-dotenv
- name: Run Issue Monitoring Agent (Spam Sweep)
env:
INITIAL_FULL_SCAN: ${{ github.event.inputs.full_scan == 'true' }}
run: python -m adk_issue_monitoring_agent.main
# 2. Audit inactive issues and nudge/close them
audit-stale:
if: |
github.repository == 'google/adk-python' &&
(github.event_name == 'schedule' || github.event.inputs.run_stale_auditor == 'true')
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests google-adk python-dateutil
- name: Run Stale Auditor Agent
run: python -m adk_stale_agent.main
+78
View File
@@ -0,0 +1,78 @@
# 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.
name: ADK Pull Request Triaging Agent
on:
# React within seconds of a PR opening/updating so the owner is assigned
# promptly. pull_request_target (not pull_request) is required so the run has
# the base-repo token needed to assign on community fork PRs; this workflow
# only reads PR metadata via the API and never checks out untrusted PR code.
pull_request_target:
types: [opened, reopened, synchronize, ready_for_review]
schedule:
# Backfill every 6 hours in case an event was missed.
- cron: '0 */6 * * *'
workflow_dispatch:
inputs:
pr_number:
description: 'The Pull Request number to triage (leave empty for batch mode)'
required: false
type: 'string'
pr_count:
description: 'Number of PRs to process in batch mode (default: 10)'
required: false
default: '10'
type: 'string'
# Never let two runs triage the same PR at once (e.g. rapid pushes); the
# scheduled backfill still runs under its own group.
concurrency:
group: pr-triage-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: false
jobs:
agent-triage-pull-request:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests google-adk
- name: Run Triaging Script
env:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GOOGLE_GENAI_USE_VERTEXAI: 0
OWNER: 'google'
REPO: 'adk-python'
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number || '' }}
PR_COUNT_TO_PROCESS: ${{ github.event.inputs.pr_count || '10' }}
INTERACTIVE: ${{ vars.PR_TRIAGE_INTERACTIVE }}
PYTHONPATH: contributing/samples/adk_team
run: python -m adk_pr_triaging_agent.main
+74
View File
@@ -0,0 +1,74 @@
# 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.
# Step 3 (optional): Cherry-picks a commit from a base branch (main or v1) into the active release candidate branch.
# Use this between step 1 and step 4 to include hotfixes in an in-progress release.
# Note: Does NOT auto-trigger release-please to preserve manual changelog edits.
name: "Release: Cherry-pick"
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch line of the release candidate (main or v1)'
required: true
default: 'main'
type: choice
options:
- main
- v1
commit_sha:
description: 'Commit SHA to cherry-pick'
required: true
jobs:
cherry-pick:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
steps:
- name: Determine Branch Configurations
id: config
run: |
BRANCH="${{ inputs.branch }}"
if [ "$BRANCH" = "v1" ]; then
echo "candidate_branch=release/v1-candidate" >> $GITHUB_OUTPUT
else
echo "candidate_branch=release/candidate" >> $GITHUB_OUTPUT
fi
- uses: actions/checkout@v6
with:
ref: ${{ steps.config.outputs.candidate_branch }}
fetch-depth: 0
- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Cherry-pick commit
run: |
CANDIDATE_BRANCH="${{ steps.config.outputs.candidate_branch }}"
echo "Cherry-picking ${INPUTS_COMMIT_SHA} to $CANDIDATE_BRANCH"
git cherry-pick ${INPUTS_COMMIT_SHA}
env:
INPUTS_COMMIT_SHA: ${{ inputs.commit_sha }}
- name: Push changes
run: |
CANDIDATE_BRANCH="${{ steps.config.outputs.candidate_branch }}"
git push origin "$CANDIDATE_BRANCH"
echo "Successfully cherry-picked commit to $CANDIDATE_BRANCH"
echo "If you want to regenerate the changelog PR, run the 'Release: Cut' workflow manually"
echo "with action='regenerate' and branch='${{ inputs.branch }}'."
+165
View File
@@ -0,0 +1,165 @@
# 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.
# Unified release manager. Supports:
# 1. Cutting a new release candidate branch from main or v1.
# 2. Regenerating/updating the changelog PR on an existing candidate branch.
name: "Release: Cut"
on:
workflow_dispatch:
inputs:
action:
description: 'Action to perform'
required: true
default: 'cut'
type: choice
options:
- cut
- regenerate
branch:
description: 'Branch to release from (main or v1)'
required: true
default: 'main'
type: choice
options:
- main
- v1
commit_sha:
description: 'Optional Commit SHA (only used for "cut" action; overrides branch latest)'
required: false
type: string
permissions:
contents: write
pull-requests: write
jobs:
cut-or-regenerate:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
steps:
- name: Determine Branch Configurations
id: config
run: |
BRANCH="${{ inputs.branch }}"
if [ "$BRANCH" = "v1" ]; then
echo "base_ref=v1" >> $GITHUB_OUTPUT
echo "candidate_branch=release/v1-candidate" >> $GITHUB_OUTPUT
echo "config_file=.github/release-please-config-v1.json" >> $GITHUB_OUTPUT
echo "manifest_file=.github/.release-please-manifest-v1.json" >> $GITHUB_OUTPUT
else
echo "base_ref=main" >> $GITHUB_OUTPUT
echo "candidate_branch=release/candidate" >> $GITHUB_OUTPUT
echo "config_file=.github/release-please-config.json" >> $GITHUB_OUTPUT
echo "manifest_file=.github/.release-please-manifest.json" >> $GITHUB_OUTPUT
fi
# Action: CUT NEW RELEASE
- name: Checkout base ref (Cut)
if: inputs.action == 'cut'
uses: actions/checkout@v6
with:
ref: ${{ inputs.commit_sha || steps.config.outputs.base_ref }}
token: ${{ secrets.RELEASE_PAT }}
- name: Check for existing candidate branch (Cut)
if: inputs.action == 'cut'
run: |
CANDIDATE_BRANCH="${{ steps.config.outputs.candidate_branch }}"
if git ls-remote --exit-code --heads origin "$CANDIDATE_BRANCH" &>/dev/null; then
echo "Error: Branch $CANDIDATE_BRANCH already exists."
echo "Please finalize or delete the existing release candidate before starting a new one."
exit 1
fi
- name: Create and push candidate branch (Cut)
if: inputs.action == 'cut'
run: |
CANDIDATE_BRANCH="${{ steps.config.outputs.candidate_branch }}"
git checkout -b "$CANDIDATE_BRANCH"
git push origin "$CANDIDATE_BRANCH"
echo "Created and pushed branch: $CANDIDATE_BRANCH"
# Action: REGENERATE EXISTING PR
- name: Checkout existing candidate branch (Regenerate)
if: inputs.action == 'regenerate'
uses: actions/checkout@v6
with:
ref: ${{ steps.config.outputs.candidate_branch }}
token: ${{ secrets.RELEASE_PAT }}
# Run Release Please
- name: Run Release Please
id: release_please
uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.RELEASE_PAT }}
config-file: ${{ steps.config.outputs.config_file }}
manifest-file: ${{ steps.config.outputs.manifest_file }}
target-branch: ${{ steps.config.outputs.candidate_branch }}
# Curate the changelog: clean up and (for large releases) fold the
# release-please output, draft a Highlights section on top, commit it back
# to the release PR branch, and sync the PR description to match. The
# script falls back to an empty Highlights template if drafting fails, so
# this step never blocks the release. Guarded on `pr` (not `prs_created`)
# so it also runs when release-please updates an existing PR (regenerate).
- name: Set up Python
if: steps.release_please.outputs.pr != ''
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install changelog curation dependencies
if: steps.release_please.outputs.pr != ''
run: pip install --upgrade google-genai
- name: Curate changelog Highlights
if: steps.release_please.outputs.pr != ''
# Curation is a nice-to-have layered on top of the release PR; never let
# it turn the release run red (e.g. a push race or a transient error).
continue-on-error: true
env:
RELEASE_PR: ${{ steps.release_please.outputs.pr }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GOOGLE_GENAI_USE_VERTEXAI: '0'
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
run: |
set -euo pipefail
PR_BRANCH=$(echo "$RELEASE_PR" | jq -r '.headBranchName')
PR_NUMBER=$(echo "$RELEASE_PR" | jq -r '.number')
echo "Curating changelog on release PR #$PR_NUMBER (branch: $PR_BRANCH)"
git fetch origin "$PR_BRANCH"
git checkout -B "$PR_BRANCH" FETCH_HEAD
python scripts/curate_changelog.py --changelog CHANGELOG.md \
--section-out /tmp/pr_body.md
# Mirror the curated notes into the PR description so reviewers read
# the same thing that ships in CHANGELOG.md. Done regardless of whether
# the file changed, since the body is regenerated by release-please.
if [ -s /tmp/pr_body.md ]; then
gh pr edit "$PR_NUMBER" --body-file /tmp/pr_body.md
fi
if git diff --quiet -- CHANGELOG.md; then
echo "No changelog file changes to commit."
exit 0
fi
USER_JSON=$(gh api user)
git config user.name "$(echo "$USER_JSON" | jq -r '.login')"
git config user.email "$(echo "$USER_JSON" | jq -r '.id')+$(echo "$USER_JSON" | jq -r '.login')@users.noreply.github.com"
git add CHANGELOG.md
git commit -m "chore: add curated highlights to changelog"
# Rebase onto any concurrent PR-branch updates so the push doesn't fail on a stale ref.
git pull --rebase origin "$PR_BRANCH"
git push origin "$PR_BRANCH"
+122
View File
@@ -0,0 +1,122 @@
# 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.
# Triggers automatically when the changelog PR is merged to a candidate branch.
# Records the last-release-sha for Release Please and renames the branch to release/v{version}.
name: "Release: Finalize"
on:
pull_request:
types: [closed]
branches:
- release/candidate
- release/v1-candidate
permissions:
contents: write
pull-requests: write
jobs:
finalize:
if: github.event.pull_request.merged == true && github.repository == 'google/adk-python'
runs-on: ubuntu-latest
steps:
- name: Check for release-please PR
id: check
env:
LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }}
run: |
if echo "$LABELS" | grep -q "autorelease: pending"; then
echo "is_release_pr=true" >> $GITHUB_OUTPUT
else
echo "Not a release-please PR, skipping"
echo "is_release_pr=false" >> $GITHUB_OUTPUT
fi
- name: Determine Branch Configurations
if: steps.check.outputs.is_release_pr == 'true'
id: config
run: |
CANDIDATE_BRANCH="${{ github.event.pull_request.base.ref }}"
if [ "$CANDIDATE_BRANCH" = "release/v1-candidate" ]; then
echo "base_branch=v1" >> $GITHUB_OUTPUT
echo "config_file=.github/release-please-config-v1.json" >> $GITHUB_OUTPUT
echo "manifest_file=.github/.release-please-manifest-v1.json" >> $GITHUB_OUTPUT
else
echo "base_branch=main" >> $GITHUB_OUTPUT
echo "config_file=.github/release-please-config.json" >> $GITHUB_OUTPUT
echo "manifest_file=.github/.release-please-manifest.json" >> $GITHUB_OUTPUT
fi
- uses: actions/checkout@v6
if: steps.check.outputs.is_release_pr == 'true'
with:
ref: ${{ github.event.pull_request.base.ref }}
token: ${{ secrets.RELEASE_PAT }}
fetch-depth: 0
- name: Extract version from manifest
if: steps.check.outputs.is_release_pr == 'true'
id: version
run: |
VERSION=$(jq -r '.["."]' "${{ steps.config.outputs.manifest_file }}")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Extracted version: $VERSION"
- name: Configure git identity from RELEASE_PAT
if: steps.check.outputs.is_release_pr == 'true'
env:
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
run: |
USER_JSON=$(gh api user)
git config user.name "$(echo "$USER_JSON" | jq -r '.login')"
git config user.email "$(echo "$USER_JSON" | jq -r '.id')+$(echo "$USER_JSON" | jq -r '.login')@users.noreply.github.com"
- name: Record last-release-sha for release-please
if: steps.check.outputs.is_release_pr == 'true'
run: |
BASE_BRANCH="${{ steps.config.outputs.base_branch }}"
CONFIG_FILE="${{ steps.config.outputs.config_file }}"
CANDIDATE_BRANCH="${{ github.event.pull_request.base.ref }}"
git fetch origin "$BASE_BRANCH"
CUT_SHA=$(git merge-base "origin/$BASE_BRANCH" HEAD)
echo "Release was cut from $BASE_BRANCH at: $CUT_SHA"
jq --arg sha "$CUT_SHA" '. + {"last-release-sha": $sha}' \
"$CONFIG_FILE" > tmp.json && mv tmp.json "$CONFIG_FILE"
git add "$CONFIG_FILE"
git commit -m "chore: update last-release-sha for next $BASE_BRANCH release"
git push origin "$CANDIDATE_BRANCH"
- name: Rename candidate to release/v{version}
if: steps.check.outputs.is_release_pr == 'true'
run: |
VERSION="v${STEPS_VERSION_OUTPUTS_VERSION}"
CANDIDATE_BRANCH="${{ github.event.pull_request.base.ref }}"
git push origin "$CANDIDATE_BRANCH:refs/heads/release/$VERSION" ":$CANDIDATE_BRANCH"
echo "Renamed $CANDIDATE_BRANCH to release/$VERSION"
env:
STEPS_VERSION_OUTPUTS_VERSION: ${{ steps.version.outputs.version }}
- name: Update PR label to tagged
if: steps.check.outputs.is_release_pr == 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr edit ${{ github.event.pull_request.number }} \
--remove-label "autorelease: pending" \
--add-label "autorelease: tagged"
echo "Updated PR label to autorelease: tagged"
+108
View File
@@ -0,0 +1,108 @@
# 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.
# Builds and publishes the package to PyPI from a release/v* branch.
# Supports both main (v2+) stable releases and v1 pre-releases (with auto PEP 440 version mapping).
# Creates a merge-back PR to sync changes back to the base branch (main or v1).
name: "Release: Publish to PyPi"
on:
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
publish:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
steps:
- name: Validate branch
run: |
if [[ ! "${GITHUB_REF_NAME}" =~ ^release/v[0-9]+\.[0-9]+\.[0-9]+ ]]; then
echo "Error: Must run from a release/v* branch (e.g., release/v0.3.0 or release/v1.35.0-alpha.1)"
exit 1
fi
- uses: actions/checkout@v6
- name: Determine Release Type and Extract Version
id: version
run: |
BRANCH_NAME="${GITHUB_REF_NAME}"
VERSION="${BRANCH_NAME#release/v}"
# Check if this version matches the one in the v1 manifest to determine if it's a v1 release
if [ -f .github/.release-please-manifest-v1.json ] && jq -e --arg v "$VERSION" '.["."] == $v' .github/.release-please-manifest-v1.json &>/dev/null; then
echo "is_v1=true" >> $GITHUB_OUTPUT
echo "base_branch=v1" >> $GITHUB_OUTPUT
SEMVER="$VERSION"
echo "semver=$SEMVER" >> $GITHUB_OUTPUT
echo "Semver version (v1): $SEMVER"
# PEP 440 Conversion (e.g., 2.0.0-alpha.1 -> 2.0.0a1)
PEP440=$(echo "$SEMVER" | sed -E 's/-alpha\./a/; s/-beta\./b/; s/-rc\./rc/')
echo "pep440=$PEP440" >> $GITHUB_OUTPUT
echo "PEP 440 version (v1): $PEP440"
else
echo "is_v1=false" >> $GITHUB_OUTPUT
echo "base_branch=main" >> $GITHUB_OUTPUT
echo "semver=$VERSION" >> $GITHUB_OUTPUT
echo "pep440=$VERSION" >> $GITHUB_OUTPUT
echo "Semver version (main): $VERSION"
fi
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Update version.py with PEP 440 version (v1 only)
if: steps.version.outputs.is_v1 == 'true'
env:
PEP440_VERSION: ${{ steps.version.outputs.pep440 }}
run: |
sed -i "s/^__version__ = .*/__version__ = \"${PEP440_VERSION}\"/" src/google/adk/version.py
echo "Updated version.py to ${PEP440_VERSION}"
grep __version__ src/google/adk/version.py
- name: Build package
run: uv build
- name: Publish to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: uv publish
- name: Create merge-back PR
env:
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
SEMVER_VERSION: ${{ steps.version.outputs.semver }}
PEP440_VERSION: ${{ steps.version.outputs.pep440 }}
BASE_BRANCH: ${{ steps.version.outputs.base_branch }}
run: |
gh pr create \
--base "$BASE_BRANCH" \
--head "${GITHUB_REF_NAME}" \
--title "chore: merge release v${PEP440_VERSION} to $BASE_BRANCH" \
--body "Syncs version bump and CHANGELOG from release v${SEMVER_VERSION} to $BASE_BRANCH."
@@ -0,0 +1,92 @@
# 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.
name: "Release: Update ADk Web"
on:
workflow_dispatch:
inputs:
adk_web_repo:
description: 'Source adk-web repository'
required: true
default: 'google/adk-web' # Default source repo
adk_web_tag:
description: 'Tag of the release to download (e.g. v1.0.0).'
required: false
default: ''
jobs:
update-frontend:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
- name: Fetch and unzip frontend assets
run: |
TARGET_DIR="src/google/adk/cli/browser"
REPO="${{ github.event.inputs.adk_web_repo }}"
TAG="${{ github.event.inputs.adk_web_tag }}"
# Clean target directory
rm -rf "$TARGET_DIR"/*
mkdir -p "$TARGET_DIR"
if [ -z "$TAG" ]; then
echo "Fetching latest release metadata for $REPO..."
RELEASE_JSON=$(curl -s "https://api.github.com/repos/$REPO/releases/latest")
else
echo "Fetching release metadata for $REPO tag $TAG..."
RELEASE_JSON=$(curl -s "https://api.github.com/repos/$REPO/releases/tags/$TAG")
fi
# Extract download URL for adk-web-browser.zip
DOWNLOAD_URL=$(echo "$RELEASE_JSON" | grep -o -E '"browser_download_url": "[^"]+"' | grep -o -E 'https://[^"]+' | grep 'adk-web-browser.zip' | head -n 1)
if [ -z "$DOWNLOAD_URL" ]; then
echo "Error: Could not find adk-web-browser.zip asset in the release."
exit 1
fi
echo "Downloading assets from: $DOWNLOAD_URL"
curl -L -o frontend.zip "$DOWNLOAD_URL"
echo "Extracting assets to $TARGET_DIR..."
unzip -o frontend.zip -d "$TARGET_DIR"
rm frontend.zip
echo "Assets extracted successfully."
- name: Extract Bot Identity
id: bot-identity
env:
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
run: |
USER_JSON=$(gh api user)
echo "name=$(echo "$USER_JSON" | jq -r '.login')" >> $GITHUB_OUTPUT
echo "email=$(echo "$USER_JSON" | jq -r '.id')+$(echo "$USER_JSON" | jq -r '.login')@users.noreply.github.com" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.RELEASE_PAT }}
commit-message: "Update compiled adk web files from ${{ github.event.inputs.adk_web_repo }}@${{ github.event.inputs.adk_web_tag || 'latest' }}"
branch: update-frontend-assets
delete-branch: true
title: "chore: update compiled adk web assets"
committer: "${{ steps.bot-identity.outputs.name }} <${{ steps.bot-identity.outputs.email }}>"
author: "${{ steps.bot-identity.outputs.name }} <${{ steps.bot-identity.outputs.email }}>"
body: |
This PR automatically updates the compiled adk web files in `src/google/adk/cli/browser/` using the assets from `${{ github.event.inputs.adk_web_repo }}@${{ github.event.inputs.adk_web_tag || 'latest' }}`.
Please review the diff before merging.
@@ -0,0 +1,69 @@
# 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.
name: Upload ADK Docs to Vertex AI Search
on:
# Runs once per day at 16:00 UTC
schedule:
- cron: '00 16 * * *'
# Manual trigger for testing and fixing
workflow_dispatch:
permissions:
contents: read
jobs:
upload-adk-docs-to-vertex-ai-search:
if: github.repository == 'google/adk-python'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Clone adk-docs repository
run: git clone https://github.com/google/adk-docs.git /tmp/adk-docs
- name: Clone adk-python repository
run: git clone https://github.com/google/adk-python.git /tmp/adk-python
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Authenticate to Google Cloud
id: auth
uses: 'google-github-actions/auth@v3'
with:
credentials_json: '${{ secrets.ADK_GCP_SA_KEY }}'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install google-adk markdown google-cloud-storage google-cloud-discoveryengine
- name: Run Answering Script
env:
GITHUB_TOKEN: ${{ secrets.ADK_TRIAGE_AGENT }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
GOOGLE_CLOUD_LOCATION: ${{ secrets.GOOGLE_CLOUD_LOCATION }}
VERTEXAI_DATASTORE_ID: ${{ secrets.VERTEXAI_DATASTORE_ID }}
GOOGLE_GENAI_USE_VERTEXAI: 1
GCS_BUCKET_NAME: ${{ secrets.GCS_BUCKET_NAME }}
ADK_DOCS_ROOT_PATH: /tmp/adk-docs
ADK_PYTHON_ROOT_PATH: /tmp/adk-python
PYTHONPATH: ${{ github.workspace }}/contributing/samples/adk_team
run: python -m adk_answering_agent.upload_docs_to_vertex_ai_search
+123
View File
@@ -0,0 +1,123 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual Environment
venv/
ENV/
env/
.env
.venv
env.bak/
venv.bak/
# IDE
.idea/
.vscode/
*.swp
*.swo
.DS_Store
# Testing
.coverage
htmlcov/
.tox/
.nox/
.pytest_cache/
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Jupyter Notebook
.ipynb_checkpoints
# Logs
*.log
logs/
log/
# Local development settings
.env.local
.env.development.local
.env.test.local
.env.production.local
uv.lock
# Google Cloud specific
.gcloudignore
.gcloudignore.local
# Documentation
docs/_build/
site/
# Misc
.DS_Store
Thumbs.db
*.bak
*.tmp
*.temp
# AI Coding Tools - Project-specific configs
# Developers should symlink or copy AGENTS.md and add their own overrides locally
.adk/
.claude/
.jetski*
.antigravity*
CLAUDE.md
.cursor/
.cursorrules
.cursorignore
.windsurfrules
.aider*
.continue/
.codeium/
.githubnext/
.roo/
.rooignore
.bolt/
.v0/
# Conformance test outputs (timestamped folders from --test mode)
**/conformance/20*-*-*_*-*-*/
+74
View File
@@ -0,0 +1,74 @@
# 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.
exclude: ^(src/google/adk/cli/browser/|src/google/adk/v1/|v1_tests/)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
hooks:
- id: check-yaml
args: [--allow-multiple-documents]
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/tox-dev/pyproject-fmt
rev: v2.24.0
hooks:
- id: pyproject-fmt
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.17
hooks:
- id: ruff
args: [--fix]
files: ^src/
- repo: https://github.com/PyCQA/isort
rev: 8.0.1
hooks:
- id: isort
- repo: https://github.com/google/pyink
rev: 25.12.0
hooks:
- id: pyink
- repo: local
hooks:
- id: addlicense
name: addlicense
entry: >
bash -c 'if command -v addlicense >/dev/null 2>&1;
then addlicense -c "Google LLC" -l apache "$@";
else echo "Warning: addlicense not installed, skipping"; fi' --
language: system
files: \.(py|sh)$
- id: check-new-py-prefix
name: Check new Python files have _ prefix
description: Enforces private-by-default policy for new Python files (see .agents/skills/adk-style/references/visibility.md).
entry: scripts/check_new_py_files.sh
language: script
files: ^src/google/adk/.*\.py$
pass_filenames: false
- id: compliance-checks
name: ADK Compliance Checks
entry: scripts/compliance_checks.py
language: script
files: \.py$
# This script documents and matches the very patterns it forbids, so
# it must not scan itself or other dev-only tooling.
exclude: ^scripts/
- repo: https://github.com/executablebooks/mdformat
rev: 0.7.22
hooks:
- id: mdformat
files: ^(README\.md|CONTRIBUTING\.md|contributing/.*\.md)$
exclude: (?i)SKILL\.md$
additional_dependencies:
- mdformat-gfm
+28
View File
@@ -0,0 +1,28 @@
## Project Overview
The Agent Development Kit (ADK) is an open-source, code-first Python toolkit for building, evaluating, and deploying sophisticated AI agents.
### Key Components
- **Agent**: Blueprint defining identity, instructions, and tools.
- **Runner**: Stateless execution engine that orchestrates agent execution.
- **Tool**: Functions/capabilities agents can call.
- **Session**: Conversation state management.
- **Memory**: Long-term recall across sessions.
- **Workflow** (ADK 2.0): Graph-based orchestration of complex, multi-step agent interactions.
- **BaseNode** (ADK 2.0): Contract for all nodes, supporting output streaming and human-in-the-loop steps.
- **Context** (ADK 2.0): Holds execution state and telemetry context mapped 1:1 to nodes.
For details on how the Runner works and the invocation lifecycle, please refer to the `adk-architecture` skill and the referenced documentation therein.
## ADK Knowledge, Architecture, and Style
Skills related to ADK development are in `.agents/skills/`.
## Project Architecture
For detailed architecture patterns, component descriptions, and core interfaces, please refer to the **`adk-architecture`** skill at `.agents/skills/adk-architecture/SKILL.md`.
## Development Setup
The project uses `uv` for package management and Python 3.10+. Please refer to the **`adk-setup`** skill at `.agents/skills/adk-setup/SKILL.md` for detailed instructions.
+2758
View File
File diff suppressed because it is too large Load Diff
+271
View File
@@ -0,0 +1,271 @@
# How to contribute
We'd love to accept your patches and contributions to this project.
- [How to contribute](#how-to-contribute)
- [Before you begin](#before-you-begin)
- [Sign our Contributor License Agreement](#sign-our-contributor-license-agreement)
- [Review our community guidelines](#review-our-community-guidelines)
- [Contribution workflow](#contribution-workflow)
- [Finding Issues to Work On](#finding-issues-to-work-on)
- [Requirement for PRs](#requirement-for-prs)
- [Large or Complex Changes](#large-or-complex-changes)
- [Testing Requirements](#testing-requirements)
- [Unit Tests](#unit-tests)
- [Manual End-to-End (E2E) Tests](#manual-end-to-end-e2e-tests)
- [Documentation](#documentation)
- [Development Setup](#development-setup)
- [Code reviews](#code-reviews)
## Before you begin
### Sign our Contributor License Agreement
Contributions to this project must be accompanied by a
[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
You (or your employer) retain the copyright to your contribution; this simply
gives us permission to use and redistribute your contributions as part of the
project.
If you or your current employer have already signed the Google CLA (even if it
was for a different project), you probably don't need to do it again.
Visit <https://cla.developers.google.com/> to see your current agreements or to
sign a new one.
### Review our community guidelines
This project follows
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).
### Code reviews
All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.
## Contribution workflow
### Finding Issues to Work On
- Browse issues labeled **`good first issue`** (newcomer-friendly) or **`help wanted`** (general contributions).
- For other issues, please kindly ask before contributing to avoid
duplication.
### Requirement for PRs
- All PRs, other than small documentation or typo fixes, should have an Issue
associated. If a relevant issue doesn't exist, please create one first or
you may instead describe the bug or feature directly within the PR
description, following the structure of our issue templates.
- Small, focused PRs. Keep changes minimal—one concern per PR.
- For bug fixes or features, please provide logs or screenshot after the fix
is applied to help reviewers better understand the fix.
- Please include a `testing plan` section in your PR to describe how you
will test. This will save time for PR review. See `Testing Requirements`
section for more details.
### Large or Complex Changes
For substantial features or architectural revisions:
- Open an Issue First: Outline your proposal, including design considerations
and impact.
- Gather Feedback: Discuss with maintainers and the community to ensure
alignment and avoid duplicate work
### Testing Requirements
To maintain code quality and prevent regressions, all code changes must include
comprehensive tests and verifiable end-to-end (E2E) evidence.
#### Unit Tests
Please add or update unit tests for your change. Please include a summary of
passed `pytest` results.
Requirements for unit tests:
- **Coverage:** Cover new features, edge cases, error conditions, and typical
use cases.
- **Location:** Add or update tests under `tests/unittests/`, following
existing naming conventions (e.g., `test_<module>_<feature>.py`).
- **Framework:** Use `pytest`. Tests should be:
- Fast and isolated.
- Written clearly with descriptive names.
- Free of external dependencies (use mocks or fixtures as needed).
- **Quality:** Aim for high readability and maintainability; include
docstrings or comments for complex scenarios.
#### Manual End-to-End (E2E) Tests
Manual E2E tests ensure integrated flows work as intended. Your tests should
cover all scenarios. Sometimes, it's also good to ensure relevant functionality
is not impacted.
Depending on your change:
- **ADK Web:**
- Use the `adk web` to verify functionality.
- Capture and attach relevant screenshots demonstrating the UI/UX changes
or outputs.
- Label screenshots clearly in your PR description.
- **Runner:**
- Provide the testing setup. For example, the agent definition, and the
runner setup.
- Execute the `runner` tool to reproduce workflows.
- Include the command used and console output showing test results.
- Highlight sections of the log that directly relate to your change.
### Documentation
For any changes that impact user-facing documentation (guides, API reference,
tutorials), please open a PR in the
[adk-docs](https://github.com/google/adk-docs) repository to update the relevant
part before or alongside your code PR.
## Development Setup
1. **Clone the repository:**
```shell
gh repo clone google/adk-python
cd adk-python
```
1. **Install uv:**
Check out
[uv installation guide](https://docs.astral.sh/uv/getting-started/installation/).
1. **Setup Development Tools:**
We use `pre-commit` for code formatting and license enforcement,
`tox` with `tox-uv` for isolated multi-version testing, and
`addlicense` for Apache 2.0 license headers.
```shell
uv tool install pre-commit
uv tool install tox --with tox-uv
```
Optionally, install Google's `addlicense` tool for license header
checks (requires Go):
```shell
go install github.com/google/addlicense@latest
```
If `addlicense` is not installed, the pre-commit hook will be
skipped and CI will catch missing headers.
Install the git hooks to automatically format and check your code
before committing:
```shell
pre-commit install
```
The pre-commit hooks run `isort`, `pyink`, `addlicense`, and
`mdformat` automatically on each commit.
1. **Create virtual environment and install dependencies:**
```shell
uv venv --python "python3.11" ".venv"
source .venv/bin/activate
uv sync --all-extras
```
1. **Run unit tests locally (Fast):**
If you just want to run tests quickly while developing, run `pytest`:
```shell
pytest ./tests/unittests
```
1. **Run multi-version unit tests (Required before PR):**
ADK guarantees compatibility across Python versions. You must run the full test suite across all supported versions using `tox`. This will execute tests in pristine, isolated environments.
```shell
tox
```
_(Note: `uv` will automatically download any Python interpreters you are missing!)_
1. **Auto-format the code:**
If you installed the git hooks in Step 3, this happens automatically on commit. To run it manually across all files:
```shell
pre-commit run --all-files
```
1. **Build the wheel file:**
```shell
uv build
```
1. **Test the locally built wheel file:** Have a simple testing folder setup as
mentioned in the
[quickstart](https://google.github.io/adk-docs/get-started/quickstart/).
Then following below steps to test your changes:
Create a clean venv and activate it:
```shell
VENV_PATH=~/venvs/adk-quickstart
```
```shell
command -v deactivate >/dev/null 2>&1 && deactivate
```
```shell
rm -rf $VENV_PATH \
&& python3 -m venv $VENV_PATH \
&& source $VENV_PATH/bin/activate
```
Install the locally built wheel file:
```shell
pip install dist/google_adk-<version>-py3-none-any.whl
```
## Contributing Resources
[Contributing folder](https://github.com/google/adk-python/tree/main/contributing)
has resources that are helpful for contributors.
## AI-Assisted Development
This repo includes built-in skills for AI coding agents
(Antigravity, Gemini CLI, and others) to help with ADK development:
- **`setup-dev-env`** — Set up the local development environment:
install dependencies, configure pre-commit hooks, and verify
the setup.
- **`adk-debug`** — Debug ADK agents: inspect sessions, trace event
flows, check LLM requests/responses, diagnose tool call issues.
Supports both `adk web` (browser UI) and `adk run` (CLI) workflows.
- **`adk-workflow`** — Build graph-based workflow agents: function
nodes, LLM agent nodes, edge patterns, routing, parallel processing
(fan-out and ParallelWorker), human-in-the-loop, state management,
and best practices. Includes reference docs and tested samples.
These skills are in `.agents/skills/` and are automatically available
when using compatible AI coding tools in this repo.
The `AGENTS.md` file provides additional project context that can
be used as LLM input.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
+122
View File
@@ -0,0 +1,122 @@
# Agent Development Kit (ADK) 2.0
[![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)
[![PyPI version](https://img.shields.io/pypi/v/google-adk.svg)](https://pypi.org/project/google-adk/)
[![Python versions](https://img.shields.io/pypi/pyversions/google-adk.svg)](https://pypi.org/project/google-adk/)
[![PyPI downloads](https://static.pepy.tech/badge/google-adk/month)](https://pepy.tech/project/google-adk)
[![Unit Tests](https://github.com/google/adk-python/actions/workflows/python-unit-tests.yml/badge.svg)](https://github.com/google/adk-python/actions/workflows/python-unit-tests.yml)
[![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://google.github.io/adk-docs/)
<h2 align="center">
<img src="https://raw.githubusercontent.com/google/adk-python/main/assets/agent-development-kit.png" width="256"/>
</h2>
<h3 align="center">
An open-source, code-first Python framework for building, evaluating, and deploying sophisticated AI agents with flexibility and control.
</h3>
<h3 align="center">
Important Links:
<a href="https://google.github.io/adk-docs/">Docs</a>,
<a href="https://github.com/google/adk-samples">Samples</a> &
<a href="https://github.com/google/adk-web">ADK Web</a>.
</h3>
______________________________________________________________________
> **⚠️ BREAKING CHANGES FROM 1.x**
>
> This release includes breaking changes to the agent API, event model, and
> session schema. **Sessions generated by ADK 2.0 are readable by ADK 1.28+
> (extra fields will be ignored), but are incompatible with older 1.x versions.**
______________________________________________________________________
## 🔥 What's New in 2.0
- **Workflow Runtime**: A graph-based execution engine for composing
deterministic execution flows for agentic apps, with support for routing,
fan-out/fan-in, loops, retry, state management, dynamic nodes,
human-in-the-loop, and nested workflows.
- **Task API**: Structured agent-to-agent delegation with multi-turn task
mode, single-turn controlled output, mixed delegation patterns,
human-in-the-loop, and task agents as workflow nodes.
## 🚀 Installation
```bash
pip install google-adk
```
**Requirements:** Python 3.10+.
To install optional integrations, you can use the following command:
```bash
pip install "google-adk[extensions]"
```
The release cadence is roughly bi-weekly.
## Quick Start
> **Beginner Note:** ADK applications are built using two main classes:
> **`Agent`** (defines an AI's instructions, tools, and behavior) and
> **`Workflow`** (orchestrates agents and tasks in a graph-based flow).
### Agent
```python
from google.adk import Agent
root_agent = Agent(
name="greeting_agent",
model="gemini-2.5-flash",
instruction="You are a helpful assistant. Greet the user warmly.",
)
```
### Workflow
```python
from google.adk import Agent, Workflow
generate_fruit_agent = Agent(
name="generate_fruit_agent",
instruction="Return the name of a random fruit. Return only the name.",
)
generate_benefit_agent = Agent(
name="generate_benefit_agent",
instruction="Tell me a health benefit about the specified fruit.",
)
root_agent = Workflow(
name="root_agent",
edges=[("START", generate_fruit_agent, generate_benefit_agent)],
)
```
### Run Locally
```bash
# Interactive CLI
adk run path/to/my_agent
# Web UI (supports multi-agent directories or pointing directly to a single agent folder)
adk web path/to/agents_dir
```
## 📚 Documentation
- **Getting Started**: https://google.github.io/adk-docs/
- **Samples**: See `contributing/workflow_samples/` and
`contributing/task_samples/` for workflow and task API examples.
## 🤝 Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
## 📄 License
This project is licensed under the Apache 2.0 License — see the
[LICENSE](LICENSE) file for details.
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`google/adk-python`
- 原始仓库:https://github.com/google/adk-python
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

+16
View File
@@ -0,0 +1,16 @@
# Contributing Resources
This folder hosts resources for ADK contributors, for example, testing samples etc.
## Samples
Samples folder host samples to test different features. The samples are usually minimal and simplistic to test one or a few scenarios.
**Note**: This is different from the [google/adk-samples](https://github.com/google/adk-samples) repo, which hosts more complex e2e samples for customers to use or modify directly.
## ADK project and architecture overview
The [adk_project_overview_and_architecture.md](adk_project_overview_and_architecture.md) describes the ADK project overview and its technical architecture from high-level.
This is helpful for contributors to understand the project and design philosophy.
It can also be fed into LLMs for vibe-coding.
@@ -0,0 +1,129 @@
# ADK Project Overview and Architecture
Google Agent Development Kit (ADK) for Python
## Core Philosophy & Architecture
- Code-First: Everything is defined in Python code for versioning, testing, and IDE support. Avoid GUI-based logic.
- Modularity & Composition: We build complex multi-agent systems by composing multiple, smaller, specialized agents.
- Deployment-Agnostic: The agent's core logic is separate from its deployment environment. The same agent.py can be run locally for testing, served via an API, or deployed to the cloud.
## Foundational Abstractions (Our Vocabulary)
- Agent: The blueprint. It defines an agent's identity, instructions, and tools. It's a declarative configuration object.
- Tool: A capability. A Python function an agent can call to interact with the world (e.g., search, API call).
- Runner: The engine. It orchestrates the "Reason-Act" loop, manages LLM calls, and executes tools.
- Session: The conversation state. It holds the history for a single, continuous dialogue.
- Memory: Long-term recall across different sessions.
- Artifact Service: Manages non-textual data like files.
## Canonical Project Structure
Adhere to this structure for compatibility with ADK tooling.
```
my_adk_project/
└── src/
└── my_app/
├── agents/
│ ├── my_agent/
│ │ ├── __init__.py # Must contain: from . import agent \
│ │ └── agent.py # Must contain: root_agent = Agent(...) \
│ └── another_agent/
│ ├── __init__.py
│ └── agent.py\
```
agent.py: Must define the agent and assign it to a variable named root_agent. This is how ADK's tools find it.
`__init__.py`: In each agent directory, it must contain `from . import agent` to make the agent discoverable.
### Nested Agent Directories (Dev Mode / `adk web`)
In the local development server (`adk web` / `dev_server`), ADK supports deeply nested agent directories (e.g., sub-packages or structured folders).
- **Recursive Discovery**: The loader recursively walks directories to discover all valid agent applications containing an `agent.py`, `root_agent.yaml`, or `__init__.py` file.
- **Dot Naming Convention**: Nested agents are represented in the system and referenced inside the Web UI using a standard dot-separated namespace notation (e.g., `agent_samples.empty_agent` or `workflow_samples.fan_out_fan_in`).
- **Isolation**: Production environments (`adk api_server`) only support flat single-level agent directories for maximum security and isolation.
## Local Development & Debugging
Interactive UI (adk web): This is our primary debugging tool. It's a decoupled system:
Backend: A FastAPI server started with adk api_server.
Frontend: An Angular app that connects to the backend.
Use the "Events" tab to inspect the full execution trace (prompts, tool calls, responses).
CLI (adk run): For quick, stateless functional checks in the terminal.
Programmatic (pytest): For writing automated unit and integration tests.
## The API Layer (FastAPI)
We expose agents as production APIs using FastAPI.
- get_fast_api_app: This is the key helper function from google.adk.cli.fast_api that creates a FastAPI app from our agent directory.
- Standard Endpoints: The generated app includes standard routes like /list-apps and /run_sse for streaming responses. The wire format is camelCase.
- Custom Endpoints: We can add our own routes (e.g., /health) to the app object returned by the helper.
```Python
from google.adk.cli.fast_api import get_fast_api_app
app = get_fast_api_app(agent_dir="./agents")
@app.get("/health")
async def health_check():
return {"status": "ok"}
```
### Default Application Resolution (`ADK_DEFAULT_APP_NAME`)
By default, the ADK API server expects an explicit application context in all requests (e.g., via the `/apps/{app_name}/...` path or in the payload body).
However, if the environment variable `ADK_DEFAULT_APP_NAME` is set, or if the server is running in **single agent mode** (when pointing directly to a directory containing an agent instead of a directory of agents), the server will automatically resolve and fall back to that agent as the default application whenever a request lacks an explicit app name. In single agent mode, the local agent takes precedence over the `ADK_DEFAULT_APP_NAME` environment variable.
- **URL Path-Rewriting (Production Endpoints)**: Requests to production endpoints that omit the `/apps/{app_name}` prefix (such as `/users/{user_id}/sessions` or `/app-info`) are automatically rewritten by an internal ASGI middleware to target the default application. (Note: `/dev` and `/builder` endpoints are excluded from rewriting).
- **Agent Execution & Streaming**: Requests to `/run`, `/run_sse`, or `/run_live` that omit the `app_name` parameter in their payload body or query string will automatically resolve to the default application.
## Deployment to Production
The adk cli provides the "adk deploy" command to deploy to Google Vertex Agent Engine, Google CloudRun, Google GKE.
## Testing & Evaluation Strategy
Testing is layered, like a pyramid.
### Layer 1: Unit Tests (Base)
What: Test individual Tool functions in isolation.
How: Use pytest in tests/test_tools.py. Verify deterministic logic.
### Layer 2: Integration Tests (Middle)
What: Test the agent's internal logic and interaction with tools.
How: Use pytest in tests/test_agent.py, often with mocked LLMs or services.
### Layer 3: Evaluation Tests (Top)
What: Assess end-to-end performance with a live LLM. This is about quality, not just pass/fail.
How: Use the ADK Evaluation Framework.
Test Cases: Create JSON files with input and a reference (expected tool calls and final response).
Metrics: tool_trajectory_avg_score (does it use tools correctly?) and response_match_score (is the final answer good?).
Run via: adk web (UI), pytest (for CI/CD), or adk eval (CLI).
+234
View File
@@ -0,0 +1,234 @@
# A2A OAuth Authentication Sample Agent
This sample demonstrates the **Agent-to-Agent (A2A)** architecture with **OAuth Authentication** workflows in the Agent Development Kit (ADK). The sample implements a multi-agent system where a remote agent can surface OAuth authentication requests to the local agent, which then guides the end user through the OAuth flow before returning the authentication credentials to the remote agent for API access.
## Overview
The A2A OAuth Authentication sample consists of:
- **Root Agent** (`root_agent`): The main orchestrator that handles user requests and delegates tasks to specialized agents
- **YouTube Search Agent** (`youtube_search_agent`): A local agent that handles YouTube video searches using LangChain tools
- **BigQuery Agent** (`bigquery_agent`): A remote A2A agent that manages BigQuery operations and requires OAuth authentication for Google Cloud access
## Architecture
```
┌─────────────────┐ ┌────────────────────┐ ┌──────────────────┐
│ End User │───▶│ Root Agent │───▶│ BigQuery Agent │
│ (OAuth Flow) │ │ (Local) │ │ (Remote A2A) │
│ │ │ │ │ (localhost:8001) │
│ OAuth UI │◀───│ │◀───│ OAuth Request │
└─────────────────┘ └────────────────────┘ └──────────────────┘
```
## Key Features
### 1. **Multi-Agent Architecture**
- Root agent coordinates between local YouTube search and remote BigQuery operations
- Demonstrates hybrid local/remote agent workflows
- Seamless task delegation based on user request types
### 2. **OAuth Authentication Workflow**
- Remote BigQuery agent surfaces OAuth authentication requests to the root agent
- Root agent guides end users through Google OAuth flow for BigQuery access
- Secure token exchange between agents for authenticated API calls
### 3. **Google Cloud Integration**
- BigQuery toolset with comprehensive dataset and table management capabilities
- OAuth-protected access to user's Google Cloud BigQuery resources
- Support for listing, creating, and managing datasets and tables
### 4. **LangChain Tool Integration**
- YouTube search functionality using LangChain community tools
- Demonstrates integration of third-party tools in agent workflows
## Setup and Usage
### Prerequisites
1. **Set up OAuth Credentials**:
```bash
export OAUTH_CLIENT_ID=your_google_oauth_client_id
export OAUTH_CLIENT_SECRET=your_google_oauth_client_secret
```
1. **Start the Remote BigQuery Agent server**:
```bash
# Start the remote a2a server that serves the BigQuery agent on port 8001
adk api_server --a2a --port 8001 contributing/samples/a2a_auth/remote_a2a
```
1. **Run the Main Agent**:
```bash
# In a separate terminal, run the adk web server
adk web contributing/samples/
```
### Example Interactions
Once both services are running, you can interact with the root agent:
**YouTube Search (No Authentication Required):**
```
User: Search for 3 Taylor Swift music videos
Agent: I'll help you search for Taylor Swift music videos on YouTube.
[Agent delegates to YouTube Search Agent]
Agent: I found 3 Taylor Swift music videos:
1. "Anti-Hero" - Official Music Video
2. "Shake It Off" - Official Music Video
3. "Blank Space" - Official Music Video
```
**BigQuery Operations (OAuth Required):**
```
User: List my BigQuery datasets
Agent: I'll help you access your BigQuery datasets. This requires authentication with your Google account.
[Agent delegates to BigQuery Agent]
Agent: To access your BigQuery data, please complete the OAuth authentication.
[OAuth flow initiated - user redirected to Google authentication]
User: [Completes OAuth flow in browser]
Agent: Authentication successful! Here are your BigQuery datasets:
- dataset_1: Customer Analytics
- dataset_2: Sales Data
- dataset_3: Marketing Metrics
```
**Dataset Management:**
```
User: Show me details for my Customer Analytics dataset
Agent: I'll get the details for your Customer Analytics dataset.
[Using existing OAuth token]
Agent: Customer Analytics Dataset Details:
- Created: 2024-01-15
- Location: US
- Tables: 5
- Description: Customer behavior and analytics data
```
## Code Structure
### Main Agent (`agent.py`)
- **`youtube_search_agent`**: Local agent with LangChain YouTube search tool
- **`bigquery_agent`**: Remote A2A agent configuration for BigQuery operations
- **`root_agent`**: Main orchestrator with task delegation logic
### Remote BigQuery Agent (`remote_a2a/bigquery_agent/`)
- **`agent.py`**: Implementation of the BigQuery agent with OAuth toolset
- **`agent.json`**: Agent card of the A2A agent
- **`BigQueryToolset`**: OAuth-enabled tools for BigQuery dataset and table management
## OAuth Authentication Workflow
The OAuth authentication process follows this pattern:
1. **Initial Request**: User requests BigQuery operation through root agent
1. **Delegation**: Root agent delegates to remote BigQuery agent
1. **Auth Check**: BigQuery agent checks for valid OAuth token
1. **Auth Request**: If no token, agent surfaces OAuth request to root agent
1. **User OAuth**: Root agent guides user through Google OAuth flow
1. **Token Exchange**: Root agent sends OAuth token to BigQuery agent
1. **API Call**: BigQuery agent uses token to make authenticated API calls
1. **Result Return**: BigQuery agent returns results through root agent to user
## Supported BigQuery Operations
The BigQuery agent supports the following operations:
### Dataset Operations:
- **List Datasets**: `bigquery_datasets_list` - Get all user's datasets
- **Get Dataset**: `bigquery_datasets_get` - Get specific dataset details
- **Create Dataset**: `bigquery_datasets_insert` - Create new dataset
### Table Operations:
- **List Tables**: `bigquery_tables_list` - Get tables in a dataset
- **Get Table**: `bigquery_tables_get` - Get specific table details
- **Create Table**: `bigquery_tables_insert` - Create new table in dataset
## Extending the Sample
You can extend this sample by:
- Adding more Google Cloud services (Cloud Storage, Compute Engine, etc.)
- Implementing token refresh and expiration handling
- Adding role-based access control for different BigQuery operations
- Creating OAuth flows for other providers (Microsoft, Facebook, etc.)
- Adding audit logging for authentication events
- Implementing multi-tenant OAuth token management
## Deployment to Other Environments
When deploying the remote BigQuery A2A agent to different environments (e.g., Cloud Run, different hosts/ports), you **must** update the `url` field in the agent card JSON file:
### Local Development
```json
{
"url": "http://localhost:8001/a2a/bigquery_agent",
...
}
```
### Cloud Run Example
```json
{
"url": "https://your-bigquery-service-abc123-uc.a.run.app/a2a/bigquery_agent",
...
}
```
### Custom Host/Port Example
```json
{
"url": "https://your-domain.com:9000/a2a/bigquery_agent",
...
}
```
**Important:** The `url` field in `remote_a2a/bigquery_agent/agent.json` must point to the actual RPC endpoint where your remote BigQuery A2A agent is deployed and accessible.
## Troubleshooting
**Connection Issues:**
- Ensure the local ADK web server is running on port 8000
- Ensure the remote A2A server is running on port 8001
- Check that no firewall is blocking localhost connections
- **Verify the `url` field in `remote_a2a/bigquery_agent/agent.json` matches the actual deployed location of your remote A2A server**
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
**OAuth Issues:**
- Verify OAuth client ID and secret are correctly set in .env file
- Ensure OAuth redirect URIs are properly configured in Google Cloud Console
- Check that the OAuth scopes include BigQuery access permissions
- Verify the user has access to the BigQuery projects/datasets
**BigQuery Access Issues:**
- Ensure the authenticated user has BigQuery permissions
- Check that the Google Cloud project has BigQuery API enabled
- Verify dataset and table names are correct and accessible
- Check for quota limits on BigQuery API calls
**Agent Communication Issues:**
- Check the logs for both the local ADK web server and remote A2A server
- Verify OAuth tokens are properly passed between agents
- Ensure agent instructions are clear about authentication requirements
- **Double-check that the RPC URL in the agent.json file is correct and accessible**
@@ -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,61 @@
# 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.llm_agent import Agent
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.tools.langchain_tool import LangchainTool
from langchain_community.tools.youtube.search import YouTubeSearchTool
# Instantiate the tool
langchain_yt_tool = YouTubeSearchTool()
# Wrap the tool in the LangchainTool class from ADK
adk_yt_tool = LangchainTool(
tool=langchain_yt_tool,
)
youtube_search_agent = Agent(
name="youtube_search_agent",
instruction="""
Ask customer to provide singer name, and the number of videos to search.
""",
description="Help customer to search for a video on Youtube.",
tools=[adk_yt_tool],
output_key="youtube_search_output",
)
bigquery_agent = RemoteA2aAgent(
name="bigquery_agent",
description="Help customer to manage notion workspace.",
agent_card=(
f"http://localhost:8001/a2a/bigquery_agent{AGENT_CARD_WELL_KNOWN_PATH}"
),
)
root_agent = Agent(
name="root_agent",
instruction="""
You are a helpful assistant that can help search youtube videos, look up BigQuery datasets and tables.
You delegate youtube search tasks to the youtube_search_agent.
You delegate BigQuery tasks to the bigquery_agent.
Always clarify the results before proceeding.
""",
global_instruction=(
"You are a helpful assistant that can help search youtube videos, look"
" up BigQuery datasets and tables."
),
sub_agents=[youtube_search_agent, bigquery_agent],
)
@@ -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,45 @@
{
"capabilities": {},
"defaultInputModes": [
"text/plain"
],
"defaultOutputModes": [
"application/json"
],
"description": "A Google BigQuery agent that helps manage users' data on Google BigQuery. Can list, get, and create datasets, as well as manage tables within datasets. Supports OAuth authentication for secure access to BigQuery resources.",
"name": "bigquery_agent",
"skills": [
{
"id": "dataset_management",
"name": "Dataset Management",
"description": "List, get details, and create BigQuery datasets",
"tags": [
"bigquery",
"datasets",
"google-cloud"
]
},
{
"id": "table_management",
"name": "Table Management",
"description": "List, get details, and create BigQuery tables within datasets",
"tags": [
"bigquery",
"tables",
"google-cloud"
]
},
{
"id": "oauth_authentication",
"name": "OAuth Authentication",
"description": "Secure authentication with Google BigQuery using OAuth",
"tags": [
"authentication",
"oauth",
"security"
]
}
],
"url": "http://localhost:8001/a2a/bigquery_agent",
"version": "1.0.0"
}
@@ -0,0 +1,77 @@
# 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 os
from dotenv import load_dotenv
from google.adk import Agent
from google.adk.tools.google_api_tool import BigQueryToolset
# Load environment variables from .env file
load_dotenv()
# Access the variable
oauth_client_id = os.getenv("OAUTH_CLIENT_ID")
oauth_client_secret = os.getenv("OAUTH_CLIENT_SECRET")
tools_to_expose = [
"bigquery_datasets_list",
"bigquery_datasets_get",
"bigquery_datasets_insert",
"bigquery_tables_list",
"bigquery_tables_get",
"bigquery_tables_insert",
]
bigquery_toolset = BigQueryToolset(
client_id=oauth_client_id,
client_secret=oauth_client_secret,
tool_filter=tools_to_expose,
)
root_agent = Agent(
name="bigquery_agent",
instruction="""
You are a helpful Google BigQuery agent that help to manage users' data on Google BigQuery.
Use the provided tools to conduct various operations on users' data in Google BigQuery.
Scenario 1:
The user wants to query their bigquery datasets
Use bigquery_datasets_list to query user's datasets
Scenario 2:
The user wants to query the details of a specific dataset
Use bigquery_datasets_get to get a dataset's details
Scenario 3:
The user wants to create a new dataset
Use bigquery_datasets_insert to create a new dataset
Scenario 4:
The user wants to query their tables in a specific dataset
Use bigquery_tables_list to list all tables in a dataset
Scenario 5:
The user wants to query the details of a specific table
Use bigquery_tables_get to get a table's details
Scenario 6:
The user wants to insert a new table into a dataset
Use bigquery_tables_insert to insert a new table into a dataset
Current user:
<User>
{userInfo?}
</User>
""",
tools=[bigquery_toolset],
)
@@ -0,0 +1,165 @@
# A2A Basic Sample Agent
This sample demonstrates the **Agent-to-Agent (A2A)** architecture in the Agent Development Kit (ADK), showcasing how multiple agents can work together to handle complex tasks. The sample implements an agent that can roll dice and check if numbers are prime.
## Overview
The A2A Basic sample consists of:
- **Root Agent** (`root_agent`): The main orchestrator that delegates tasks to specialized sub-agents
- **Roll Agent** (`roll_agent`): A local sub-agent that handles dice rolling operations
- **Prime Agent** (`prime_agent`): A remote A2A agent that checks if numbers are prime, this agent is running on a separate A2A server
## Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌────────────────────┐
│ Root Agent │───▶│ Roll Agent │ │ Remote Prime │
│ (Local) │ │ (Local) │ │ Agent │
│ │ │ │ │ (localhost:8001) │
│ │───▶│ │◀───│ │
└─────────────────┘ └──────────────────┘ └────────────────────┘
```
## Key Features
### 1. **Local Sub-Agent Integration**
- The `roll_agent` demonstrates how to create and integrate local sub-agents
- Handles dice rolling with configurable number of sides
- Uses a simple function tool (`roll_die`) for random number generation
### 2. **Remote A2A Agent Integration**
- The `prime_agent` shows how to connect to remote agent services
- Communicates with a separate service via HTTP at `http://localhost:8001/a2a/check_prime_agent`
- Demonstrates cross-service agent communication
### 3. **Agent Orchestration**
- The root agent intelligently delegates tasks based on user requests
- Can chain operations (e.g., "roll a die and check if it's prime")
- Provides clear workflow coordination between multiple agents
### 4. **Example Tool Integration**
- Includes an `ExampleTool` with sample interactions for context
- Helps the agent understand expected behavior patterns
## Setup and Usage
### Prerequisites
1. **Start the Remote Prime Agent server**:
```bash
# Start the remote a2a server that serves the check prime agent on port 8001
adk api_server --a2a --port 8001 contributing/samples/a2a_basic/remote_a2a
```
1. **Run the Main Agent**:
```bash
# In a separate terminal, run the adk web server
adk web contributing/samples/
```
### Example Interactions
Once both services are running, you can interact with the root agent:
**Simple Dice Rolling:**
```
User: Roll a 6-sided die
Bot: I rolled a 4 for you.
```
**Prime Number Checking:**
```
User: Is 7 a prime number?
Bot: Yes, 7 is a prime number.
```
**Combined Operations:**
```
User: Roll a 10-sided die and check if it's prime
Bot: I rolled an 8 for you.
Bot: 8 is not a prime number.
```
## Code Structure
### Main Agent (`agent.py`)
- **`roll_die(sides: int)`**: Function tool for rolling dice
- **`roll_agent`**: Local agent specialized in dice rolling
- **`prime_agent`**: Remote A2A agent configuration
- **`root_agent`**: Main orchestrator with delegation logic
### Remote Prime Agent (`remote_a2a/check_prime_agent/`)
- **`agent.py`**: Implementation of the prime checking service
- **`agent.json`**: Agent card of the A2A agent
- **`check_prime(nums: list[int])`**: Prime number checking algorithm
## Extending the Sample
You can extend this sample by:
- Adding more mathematical operations (factorization, square roots, etc.)
- Creating additional remote agent
- Implementing more complex delegation logic
- Adding persistent state management
- Integrating with external APIs or databases
## Deployment to Other Environments
When deploying the remote A2A agent to different environments (e.g., Cloud Run, different hosts/ports), you **must** update the `url` field in the agent card JSON file:
### Local Development
```json
{
"url": "http://localhost:8001/a2a/check_prime_agent",
...
}
```
### Cloud Run Example
```json
{
"url": "https://your-service-abc123-uc.a.run.app/a2a/check_prime_agent",
...
}
```
### Custom Host/Port Example
```json
{
"url": "https://your-domain.com:9000/a2a/check_prime_agent",
...
}
```
**Important:** The `url` field in `remote_a2a/check_prime_agent/agent.json` must point to the actual RPC endpoint where your remote A2A agent is deployed and accessible.
## Troubleshooting
**Connection Issues:**
- Ensure the local ADK web server is running on port 8000
- Ensure the remote A2A server is running on port 8001
- Check that no firewall is blocking localhost connections
- **Verify the `url` field in `remote_a2a/check_prime_agent/agent.json` matches the actual deployed location of your remote A2A server**
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
**Agent Not Responding:**
- Check the logs for both the local ADK web server on port 8000 and remote A2A server on port 8001
- Verify the agent instructions are clear and unambiguous
- **Double-check that the RPC URL in the agent.json file is correct and accessible**
+15
View File
@@ -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
+120
View File
@@ -0,0 +1,120 @@
# 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.llm_agent import Agent
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.tools.example_tool import ExampleTool
from google.genai import types
# --- Roll Die Sub-Agent ---
def roll_die(sides: int) -> int:
"""Roll a die and return the rolled result."""
return random.randint(1, sides)
roll_agent = Agent(
name="roll_agent",
description="Handles rolling dice of different sizes.",
instruction="""
You are responsible for rolling dice based on the user's request.
When asked to roll a die, you must call the roll_die tool with the number of sides as an integer.
""",
tools=[roll_die],
generate_content_config=types.GenerateContentConfig(
safety_settings=[
types.SafetySetting( # avoid false alarm about rolling dice.
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=types.HarmBlockThreshold.OFF,
),
]
),
)
example_tool = ExampleTool([
{
"input": {
"role": "user",
"parts": [{"text": "Roll a 6-sided die."}],
},
"output": [
{"role": "model", "parts": [{"text": "I rolled a 4 for you."}]}
],
},
{
"input": {
"role": "user",
"parts": [{"text": "Is 7 a prime number?"}],
},
"output": [{
"role": "model",
"parts": [{"text": "Yes, 7 is a prime number."}],
}],
},
{
"input": {
"role": "user",
"parts": [{"text": "Roll a 10-sided die and check if it's prime."}],
},
"output": [
{
"role": "model",
"parts": [{"text": "I rolled an 8 for you."}],
},
{
"role": "model",
"parts": [{"text": "8 is not a prime number."}],
},
],
},
])
prime_agent = RemoteA2aAgent(
name="prime_agent",
description="Agent that handles checking if numbers are prime.",
agent_card=(
f"http://localhost:8001/a2a/check_prime_agent{AGENT_CARD_WELL_KNOWN_PATH}"
),
)
root_agent = Agent(
name="root_agent",
instruction="""
You are a helpful assistant that can roll dice and check if numbers are prime.
You delegate rolling dice tasks to the roll_agent and prime checking tasks to the prime_agent.
Follow these steps:
1. If the user asks to roll a die, delegate to the roll_agent.
2. If the user asks to check primes, delegate to the prime_agent.
3. If the user asks to roll a die and then check if the result is prime, call roll_agent first, then pass the result to prime_agent.
Always clarify the results before proceeding.
""",
global_instruction=(
"You are DicePrimeBot, ready to roll dice and check prime numbers."
),
sub_agents=[roll_agent, prime_agent],
tools=[example_tool],
generate_content_config=types.GenerateContentConfig(
safety_settings=[
types.SafetySetting( # avoid false alarm about rolling dice.
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=types.HarmBlockThreshold.OFF,
),
]
),
)
@@ -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,26 @@
{
"capabilities": {},
"defaultInputModes": [
"text/plain"
],
"defaultOutputModes": [
"application/json"
],
"description": "An agent specialized in checking whether numbers are prime. It can efficiently determine the primality of individual numbers or lists of numbers.",
"name": "check_prime_agent",
"skills": [
{
"id": "prime_checking",
"name": "Prime Number Checking",
"description": "Check if numbers in a list are prime using efficient mathematical algorithms",
"tags": [
"mathematical",
"computation",
"prime",
"numbers"
]
}
],
"url": "http://localhost:8001/a2a/check_prime_agent",
"version": "1.0.0"
}
@@ -0,0 +1,74 @@
# 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 import Agent
from google.adk.tools.tool_context import ToolContext
from google.genai import types
async def check_prime(nums: list[int]) -> str:
"""Check if a given list of numbers are prime.
Args:
nums: The list of numbers to check.
Returns:
A str indicating which number is prime.
"""
primes = set()
for number in nums:
number = int(number)
if number <= 1:
continue
is_prime = True
for i in range(2, int(number**0.5) + 1):
if number % i == 0:
is_prime = False
break
if is_prime:
primes.add(number)
return (
'No prime numbers found.'
if not primes
else f"{', '.join(str(num) for num in primes)} are prime numbers."
)
root_agent = Agent(
name='check_prime_agent',
description='check prime agent that can check whether numbers are prime.',
instruction="""
You check whether numbers are prime.
When checking prime numbers, call the check_prime tool with a list of integers. Be sure to pass in a list of integers. You should never pass in a string.
You should not rely on the previous history on prime results.
""",
tools=[
check_prime,
],
# planner=BuiltInPlanner(
# thinking_config=types.ThinkingConfig(
# include_thoughts=True,
# ),
# ),
generate_content_config=types.GenerateContentConfig(
safety_settings=[
types.SafetySetting( # avoid false alarm about rolling dice.
category=types.HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
threshold=types.HarmBlockThreshold.OFF,
),
]
),
)
@@ -0,0 +1,195 @@
# A2A Human-in-the-Loop Sample Agent
This sample demonstrates the **Agent-to-Agent (A2A)** architecture with **Human-in-the-Loop** workflows in the Agent Development Kit (ADK). The sample implements a reimbursement processing agent that automatically handles small expenses while requiring remote agent to process for larger amounts. The remote agent will require a human approval for large amounts, thus surface this request to local agent and human interacting with local agent can approve the request.
## Overview
The A2A Human-in-the-Loop sample consists of:
- **Root Agent** (`root_agent`): The main reimbursement agent that handles expense requests and delegates approval to remote Approval Agent for large amounts
- **Approval Agent** (`approval_agent`): A remote A2A agent that handles the human approval process via long-running tools (which implements asynchronous approval workflows that can pause execution and wait for human input), this agent is running on a separate A2A server
## Architecture
```
┌─────────────────┐ ┌────────────────────┐ ┌──────────────────┐
│ Human Manager │───▶│ Root Agent │───▶│ Approval Agent │
│ (External) │ │ (Local) │ │ (Remote A2A) │
│ │ │ │ │ (localhost:8001) │
│ Approval UI │◀───│ │◀───│ │
└─────────────────┘ └────────────────────┘ └──────────────────┘
```
## Key Features
### 1. **Automated Decision Making**
- Automatically approves reimbursements under $100
- Uses business logic to determine when human intervention is required
- Provides immediate responses for simple cases
### 2. **Human-in-the-Loop Workflow**
- Seamlessly escalates high-value requests (>$100) to remote approval agent
- Remote approval agent uses long-running tools to surface approval requests back to the root agent
- Human managers interact directly with the root agent to approve/reject requests
### 3. **Long-Running Tool Integration**
- Demonstrates `LongRunningFunctionTool` for asynchronous operations
- Shows how to handle pending states and external updates
- Implements proper tool response handling for delayed approvals
### 4. **Remote A2A Agent Communication**
- The approval agent runs as a separate service that processes approval workflows
- Communicates via HTTP at `http://localhost:8001/a2a/human_in_loop`
- Surfaces approval requests back to the root agent for human interaction
## Setup and Usage
### Prerequisites
1. **Start the Remote Approval Agent server**:
```bash
# Start the remote a2a server that serves the human-in-the-loop approval agent on port 8001
adk api_server --a2a --port 8001 contributing/samples/a2a/a2a_human_in_loop/remote_a2a
```
1. **Run the Main Agent**:
```bash
# In a separate terminal, run the adk web server
adk web contributing/samples/a2a
```
### Example Interactions
Once both services are running, you can interact with the root agent through the approval workflow:
**Automatic Approval (Under $100):**
```
User: Please reimburse $50 for meals
Agent: I'll process your reimbursement request for $50 for meals. Since this amount is under $100, I can approve it automatically.
Agent: ✅ Reimbursement approved and processed: $50 for meals
```
**Human Approval Required (Over $100):**
```
User: Please reimburse $200 for conference travel
Agent: I'll process your reimbursement request for $200 for conference travel. Since this amount exceeds $100, I need to get manager approval.
Agent: 🔄 Request submitted for approval (Ticket: reimbursement-ticket-001). Please wait for manager review.
[Human manager approves the pending request from the ADK Web UI]
Agent: ✅ Great news! Your reimbursement has been approved by the manager. Processing $200 for conference travel.
```
> **Approving from the ADK Web UI:** The approval is a *long-running tool* call
> that runs on the remote approval agent. The pending call is surfaced in the
> Web UI as a function call awaiting a response. To approve (or reject), hover
> over the pending `ask_for_approval` function response in the UI and use
> **"Send another response"** to send back an updated response such as
> `{"status": "approved", "ticketId": "reimbursement-ticket-001"}`. Simply
> typing "I approve" as a chat message will **not** resume the pending request,
> because the framework needs a `FunctionResponse` that carries the same call
> `id` to resume the long-running tool.
>
> For this resume to be routed back to the remote approval agent (rather than
> restarting at the root agent), the sample is exposed as an `App` with
> `ResumabilityConfig(is_resumable=True)` in `agent.py`.
## Code Structure
### Main Agent (`agent.py`)
- **`reimburse(purpose: str, amount: float)`**: Function tool for processing reimbursements
- **`approval_agent`**: Remote A2A agent configuration for human approval workflows
- **`root_agent`**: Main reimbursement agent with automatic/manual approval logic
### Remote Approval Agent (`remote_a2a/human_in_loop/`)
- **`agent.py`**: Implementation of the approval agent with long-running tools
- **`agent.json`**: Agent card of the A2A agent
- **`ask_for_approval()`**: Long-running tool that handles approval requests
## Long-Running Tool Workflow
The human-in-the-loop process follows this pattern:
1. **Initial Call**: Root agent delegates approval request to remote approval agent for amounts >$100
1. **Pending Response**: Remote approval agent returns immediate response with `status: "pending"` and ticket ID and surface the approval request to root agent
1. **Agent Acknowledgment**: Root agent informs user about pending approval status
1. **Human Interaction**: Human manager interacts with root agent to review and approve/reject the request
1. **Updated Response**: Root agent receives updated tool response with approval decision and send it to remote agent
1. **Final Action**: Remote agent processes the approval and completes the reimbursement and send the result to root_agent
## Extending the Sample
You can extend this sample by:
- Adding more complex approval hierarchies (multiple approval levels)
- Implementing different approval rules based on expense categories
- Creating additional remote agent for budget checking or policy validation
- Adding notification systems for approval status updates
- Integrating with external approval systems or databases
- Implementing approval timeouts and escalation procedures
## Deployment to Other Environments
When deploying the remote approval A2A agent to different environments (e.g., Cloud Run, different hosts/ports), you **must** update the `url` field in the agent card JSON file:
### Local Development
```json
{
"url": "http://localhost:8001/a2a/human_in_loop",
...
}
```
### Cloud Run Example
```json
{
"url": "https://your-approval-service-abc123-uc.a.run.app/a2a/human_in_loop",
...
}
```
### Custom Host/Port Example
```json
{
"url": "https://your-domain.com:9000/a2a/human_in_loop",
...
}
```
**Important:** The `url` field in `remote_a2a/human_in_loop/agent.json` must point to the actual RPC endpoint where your remote approval A2A agent is deployed and accessible.
## Troubleshooting
**Connection Issues:**
- Ensure the local ADK web server is running on port 8000
- Ensure the remote A2A server is running on port 8001
- Check that no firewall is blocking localhost connections
- **Verify the `url` field in `remote_a2a/human_in_loop/agent.json` matches the actual deployed location of your remote A2A server**
- Verify the agent card URL passed to RemoteA2AAgent constructor matches the running A2A server
**Agent Not Responding:**
- Check the logs for both the local ADK web server on port 8000 and remote A2A server on port 8001
- Verify the agent instructions are clear and unambiguous
- Ensure long-running tool responses are properly formatted with matching IDs
- **Double-check that the RPC URL in the agent.json file is correct and accessible**
**Approval Workflow Issues:**
- Verify that updated tool responses use the same `id` and `name` as the original function call
- Check that the approval status is correctly updated in the tool response
- Ensure the human approval process is properly simulated or integrated

Some files were not shown because too many files have changed in this diff Show More