chore: import upstream snapshot with attribution
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / Pre-commit Linter (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.10) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.11) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.12) (push) Has been cancelled
Continuous Integration / Mypy Check (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.12) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / Unit Tests (Python 3.14) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.10) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.11) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.12) (push) Has been cancelled
Copybara PR Handler / close-imported-pr (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.13) (push) Has been cancelled
Continuous Integration / A2A v0.3 Tests (Python 3.14) (push) Has been cancelled
This commit is contained in:
@@ -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`.
|
||||
@@ -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()`
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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)],
|
||||
)
|
||||
```
|
||||
@@ -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` |
|
||||
@@ -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.
|
||||
```
|
||||
@@ -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
|
||||
|
||||
```
|
||||
@@ -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.
|
||||
|
||||
```
|
||||
@@ -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 2–3 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()
|
||||
Reference in New Issue
Block a user