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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:25:13 +08:00
commit ec2b666284
2231 changed files with 491535 additions and 0 deletions
+26
View File
@@ -0,0 +1,26 @@
# ADK Developer Guides
This directory contains specific developer guides for the ADK Python implementation. For the official ADK documentation, visit [adk.dev](https://adk.dev/).
## Index
### Agents
* [LlmAgent Single-Turn Mode](agents/llm_agent/single_turn.md) - Guide on using LlmAgent in single-turn mode.
* [LlmAgent Task Mode](agents/llm_agent/task.md) - Guide on using LlmAgent in task mode.
* [ManagedAgent](agents/managed_agent/index.md) - Guide on using ManagedAgent with server-side tools.
### Events
* [Event and NodeInfo](events/event/index.md) - Understanding Event and NodeInfo in workflows.
* [RequestInput](events/request_input/index.md) - How to use RequestInput for human-in-the-loop interactions.
### Tools
* [to_mcp_server](tools/mcp_tool/agent_to_mcp/index.md) - Expose an ADK agent as an MCP server so any MCP host can drive it as a single tool (the MCP counterpart of to_a2a).
### Workflows
* [Workflow](workflow/workflow/index.md) - Graph-based orchestration of complex, multi-step agent interactions.
* [Workflow Graphs](workflow/graph/index.md) - Understanding nodes, edges, and graph structures in workflows.
* [Function Nodes](workflow/function_node/index.md) - Wrapping Python functions and generators as workflow nodes.
* [JoinNode](workflow/join_node/index.md) - Synchronizing parallel execution paths in workflows.
* [RetryConfig](workflow/retry_config/index.md) - Configuring retry policies for resilient workflow nodes.
* [ParallelWorker](workflow/parallel_worker/index.md) - Processing lists of items concurrently in workflows.
* [Dynamic Nodes](workflow/dynamic_nodes/index.md) - Scheduling and executing nodes dynamically at runtime.
+190
View File
@@ -0,0 +1,190 @@
# LlmAgent Single-Turn Mode
This guide explains the behavior of `LlmAgent` in `single_turn` mode, both when
executed as a workflow node and when defined as a sub-agent in a multi-agent
hierarchy. It covers default stateless execution, delegation mechanics, and how
to configure history visibility.
--------------------------------------------------------------------------------
## Introduction
In ADK, `mode="single_turn"` is designed for isolated, stateless tasks where the
agent only needs to process the immediate input without accumulating or
referencing prior conversation history.
Depending on how the agent is deployed—either as a step in a `Workflow` or as a
`sub_agent` of another LLM agent—its behavior and interaction patterns differ.
--------------------------------------------------------------------------------
## 1. Single-Turn Mode as a Workflow Node
When building a `Workflow` graph, any `LlmAgent` added to the graph defaults to
`mode="single_turn"` (unless explicitly configured otherwise).
### Behavior
- **Stateless by Default**: The node does not see previous conversation turns
in the workflow session. Its history visibility (`include_contents`)
automatically defaults to `'none'`.
- **Isolated Execution**: Each execution of the node is independent.
### Example
```python
from google.adk.agents import LlmAgent
from google.adk.workflow import Workflow, build_node
# Defaults to mode="single_turn" when run as a node
writer_agent = LlmAgent(
name="writer",
instruction="Write a short story about the input topic."
)
writer_node = build_node(writer_agent)
wf = Workflow(
name="story_generator",
edges=[
("START", writer_node),
(writer_node, "END")
]
)
```
--------------------------------------------------------------------------------
## 2. Single-Turn Mode as a Sub-Agent
You can define hierarchical agent structures by assigning agents to the
`sub_agents` list of a parent `LlmAgent`.
### Behavior
- **Exposed as a Tool**: A `single_turn` sub-agent is **not** a transfer
target. The parent agent cannot hand over control of the conversation to it.
Instead, the framework automatically exposes the sub-agent to the parent as
a **Tool** (function).
- **Functional Delegation**: The parent agent calls the sub-agent like a
function, passing arguments. The sub-agent executes, returns its output to
the parent, and the parent continues the conversation.
- **Isolated Sub-Branch**: When the parent calls the sub-agent tool, the
framework executes the sub-agent in an isolated sub-branch (derived from the
parent's branch, e.g., `parent_branch.sub_agent@run_id`).
- **Stateless by Default**: Like the workflow node, a `single_turn` sub-agent
defaults to `include_contents="none"` and only sees the inputs passed to it
in the tool call.
### Example
```python
from google.adk.agents import LlmAgent
# Define a specialized single-turn sub-agent
translator_agent = LlmAgent(
name="translator",
instruction="Translate the input text to Spanish.",
mode="single_turn" # Must be explicit if not auto-wrapped in workflow
)
# Define the parent agent and assign the sub-agent
bilingual_writer = LlmAgent(
name="bilingual_writer",
instruction="Write a poem about the topic, then use the translator tool to translate it.",
sub_agents=[translator_agent] # Exposes 'translator' as a tool to bilingual_writer
)
```
### Non-LlmAgent single-turn sub-agents
`single_turn` composition is not limited to `LlmAgent`. A `ManagedAgent`
(server-backed) can also be a single-turn sub-agent by setting
`mode='single_turn'`; ADK auto-exposes it to the parent as an inline tool, and
its internal events are preserved in the shared session. Each single-turn managed
call is stateless (isolated per call), so pass a self-contained request.
```python
from google.adk.agents import LlmAgent, ManagedAgent
specialist = ManagedAgent(
name="search_specialist",
mode="single_turn",
agent_id="...",
environment={"type": "remote"},
description="Answers questions needing fresh, grounded web facts.",
)
coordinator = LlmAgent(name="coordinator", sub_agents=[specialist])
```
--------------------------------------------------------------------------------
## How Context Isolation Works
ADK manages history visibility using **branches** and the `include_contents`
configuration:
1. **Branch Hierarchy**: When a sub-agent runs, it executes in a sub-branch
(e.g., `main.translator@1`).
- A sub-branch is allowed to read events from its parent branch (one-way
visibility).
- The parent branch cannot read events from the sub-branch (protecting the
parent from sub-agent internal reasoning chatter).
2. **History Filtering**:
- **`include_contents="none"`** (Default): The agent bypasses history
loading entirely. It only sees the immediate input (the workflow node
input or the tool call arguments).
- **`include_contents="default"`**: The agent loads conversation history.
Because of the branch hierarchy, a sub-agent with this setting can see
the parent agent's conversation history leading up to the tool call.
--------------------------------------------------------------------------------
## Configuration Options
Parameter | Type | Default | Description
:----------------- | :--------------------------------------- | :------------------------------------ | :----------
`mode` | `Literal['single_turn', 'task', 'chat']` | `'single_turn'` (when run as node) | The execution mode. `single_turn` isolates execution; `task` supports delegation; `chat` preserves full history.
`include_contents` | `Literal['default', 'none']` | `'none'` (for `single_turn` if unset) | Controls history visibility. For `single_turn` mode, it defaults to `'none'` (stateless), but can be explicitly set to `'default'` to make the agent context-aware.
--------------------------------------------------------------------------------
## Advanced Applications: Context-Aware Execution
If you want a single-turn agent (node or sub-agent) to have access to the
conversation history, you must explicitly set `include_contents="default"`.
### Context-Aware Sub-Agent Example
In this setup, the `verifier` sub-agent needs to see the history of the
conversation to verify the parent's draft against previous user constraints:
```python
verifier_agent = LlmAgent(
name="verifier",
instruction="Verify that the draft meets all constraints discussed in the chat.",
mode="single_turn",
include_contents="default" # Allows the sub-agent to see the parent's conversation history
)
editor_agent = LlmAgent(
name="editor",
instruction="Discuss the draft with the user and use verifier to check constraints.",
sub_agents=[verifier_agent]
)
```
--------------------------------------------------------------------------------
## Limitations
- **Difference from Standalone Behavior**: A standalone `LlmAgent` defaults to
`include_contents="default"`. When used in a workflow or as a sub-agent, it
defaults to `include_contents="none"`.
- **No Direct Transfer**: You cannot use `transfer_to_agent` to target a
`single_turn` agent. They must be invoked via tool calls.
## Related samples
- [Single-Turn Sub-Agent Sample](../../../../contributing/samples/multi_agent/single_turn_sub_agent/README.md) - A complete sample demonstrating how to define a single-turn sub-agent and use it as a tool.
+138
View File
@@ -0,0 +1,138 @@
# LlmAgent Task Mode
This guide explains the behavior of `LlmAgent` in `task` mode. It covers how
task agents are used for delegated, goal-oriented execution, how they signal
completion using the `finish_task` tool, and how they enforce structured inputs
and outputs.
--------------------------------------------------------------------------------
## Introduction
In ADK, `mode="task"` is designed for agents that are assigned a specific,
self-contained task. Unlike `chat` mode (which supports ongoing back-and-forth
conversation and peer transfers) or `single_turn` mode (which is stateless and
immediate), a `task` agent:
1. **Runs until completion**: It executes a thought loop, calling tools as
needed, until it decides the task is finished.
2. **Converses with the User**: It can interact with the user to ask questions
or seek clarification. The framework manages pausing and resuming the task
agent across turns.
3. **Signals completion**: It must explicitly call the built-in `finish_task`
tool to end its execution.
4. **Returns structured output**: It validates its final output against a
defined `output_schema` before returning it to the caller.
When used as a sub-agent, a task agent is exposed to its parent as a tool.
Calling this tool suspends the parent agent and runs the task agent to
completion.
--------------------------------------------------------------------------------
## 1. Task Mode as a Sub-Agent
The primary use case for task agents is delegation in a multi-agent hierarchy.
### Behavior
- **Exposed as a Tool**: Similar to `single_turn` agents, a `task` agent is
exposed to its parent as a tool, not a transfer target.
- **Deferred Response**: When the parent calls the task agent's tool, the
parent's execution is suspended. The framework runs the task agent in a
sub-branch.
- **Execution Loop**: The task agent runs its own loop, using its own tools,
until it calls `finish_task`.
- **Structured Return**: The output passed to `finish_task` is validated and
returned to the parent agent as the tool result.
### Example
Here is how to define a task agent with structured inputs and outputs and
delegate to it.
```python
from google.adk.agents import LlmAgent
from pydantic import BaseModel, Field
# 1. Define schemas for Input and Output
class ResearchInput(BaseModel):
topic: str = Field(description="The topic to research.")
depth: str = Field(default="brief", description="Depth of research: brief or detailed.")
class ResearchOutput(BaseModel):
summary: str = Field(description="A summary of the findings.")
sources: list[str] = Field(description="List of sources used.")
# 2. Define the Task Agent
researcher_agent = LlmAgent(
name="researcher",
instruction="Research the given topic and provide a structured summary.",
mode="task",
input_schema=ResearchInput,
output_schema=ResearchOutput,
# Add tools needed for the task
tools=[...]
)
# 3. Define the Parent Agent
writer_agent = LlmAgent(
name="writer",
instruction="Write a blog post. Use the researcher agent to get info on the topic.",
sub_agents=[researcher_agent] # Exposes 'researcher' agent to writer
)
```
### User Interaction & Resumption
A task agent is not limited to one-shot execution. If the task is unclear or
requires user input, the agent can converse with the user:
1. **Asking a question**: The task agent outputs text directed to the user
*instead* of calling `finish_task`.
2. **Pausing**: The framework detects that the agent has returned control
without finishing the task, pauses execution, and delivers the message to
the user.
3. **Resuming**: When the user replies, the framework automatically routes the
reply back to the task agent, resuming its execution loop.
4. **Completing**: The agent continues this interaction until it eventually
calls `finish_task` with the final result.
--------------------------------------------------------------------------------
## 2. The `finish_task` Tool
Every agent configured with `mode="task"` automatically receives the
`finish_task` tool.
### How it works
- **System Instruction**: The framework appends instructions to the agent's
prompt, telling it to use `finish_task` only when the task is fully
complete.
- **Validation**: When the agent calls `finish_task(output=...)`, the
framework validates the `output` against the agent's `output_schema`.
- **Retry on Failure**: If validation fails, the framework returns the
validation error to the agent, allowing it to correct its output and try
again.
- **Default Schema**: If no `output_schema` is specified, the agent defaults
to returning a simple string (`result`).
--------------------------------------------------------------------------------
## Task Mode in Workflows
Task mode is fully supported in workflows. You can use task-mode agents as static nodes in a workflow graph. The workflow runner will automatically manage the task lifecycle, including pausing for human input and resuming with the correct context.
## Limitations
- **No Direct Transfer**: You cannot transition to a task agent using
`transfer_to_agent`. They must be invoked as tools.
- **Must Call `finish_task`**: If a task agent fails to call `finish_task`
(e.g., due to a bug or limit reach), the task will not complete
successfully.
## Related samples
- [Task Sub-Agent Sample](../../../../contributing/samples/multi_agent/task_sub_agent/README.md) - A complete sample demonstrating how to define a task-mode sub-agent with custom input/output schemas and delegate tasks to it.
+156
View File
@@ -0,0 +1,156 @@
# ManagedAgent
## Introduction
`ManagedAgent` allows you to leverage managed agents backed by the Managed
Agents API (`interactions.create`) via either the
[Gemini Enterprise Agents Platform (GEAP, formerly Vertex)](https://docs.cloud.google.com/gemini-enterprise-agent-platform/build/managed-agents)
or the [Gemini API](https://ai.google.dev/gemini-api/docs/agents) from within
your ADK flows. It is particularly useful when you want to utilize Google's
powerful first-party, out-of-the-box agents (like the Antigravity agent) that
have specialized server-side execution environments built-in without requiring
client-side function declarations.
This solves the developer problem of needing a robust, server-hosted environment
for agents that require specialized built-in capabilities, rather than managing
sandbox environments and Python code execution locally. `ManagedAgent` can be
used as a standalone agent, integrated directly into a workflow, or encapsulated
as a tool via `AgentTool` so that a coordinating `LlmAgent` can delegate
specialized tasks to it.
## Prerequisites
The `ManagedAgent` supports two distinct backends: the Gemini API backend and
the Gemini Enterprise Agents Platform (GEAP) backend. Depending on which backend
you intend to use, you must satisfy the corresponding prerequisites for
authentication and obtaining an Agent ID.
### Option 1: Gemini API Backend
* **Authentication**: You must obtain a Gemini API key. Set this as the
`GEMINI_API_KEY` environment variable.
* **Agent ID**: You need an `agent_id` to connect to. You can either:
* Create a new agent by following the
[Gemini API Agents documentation](https://ai.google.dev/gemini-api/docs/agents).
* Use an out-of-the-box agent ID, such as `antigravity-preview-05-2026`,
which is commonly used in our examples.
### Option 2: Gemini Enterprise Agents Platform (GEAP) Backend
* **Authentication**: GEAP (formerly Vertex) requires Google Cloud
credentials. Follow the
[GEAP setup instructions](https://docs.cloud.google.com/gemini-enterprise-agent-platform/build/managed-agents/create-manage#before-you-begin)
to authenticate your local environment (e.g., using `gcloud auth
application-default login`).
* **Agent ID**: Similar to the Gemini API, you need an `agent_id`. You can
either:
* Create a new agent via the
[GEAP Managed Agents guide](https://docs.cloud.google.com/gemini-enterprise-agent-platform/build/managed-agents).
* Use an out-of-the-box agent ID if available to your project.
## Get started
Here is a minimal implementation of `ManagedAgent` demonstrating its use.
```python
import os
from google.adk.agents import ManagedAgent
from google.adk.tools import google_search
from google.genai import types
# Ensure you have the MANAGED_AGENT_ID and the proper environment config
_AGENT_ID = os.environ.get('MANAGED_AGENT_ID', 'antigravity-preview-05-2026')
managed_search_agent = ManagedAgent(
name='managed_search_agent',
description='Answers questions that need fresh, grounded information from the web.',
agent_id=_AGENT_ID,
environment={'type': 'remote'},
tools=[google_search],
)
# A managed code execution agent using raw types.Tool
managed_code_execution_agent = ManagedAgent(
name='managed_code_execution_agent',
description='Solves computational questions by running code server-side.',
agent_id=_AGENT_ID,
environment={'type': 'remote'},
tools=[types.Tool(code_execution=types.ToolCodeExecution())],
)
```
To see an orchestrator pattern using this code, you could wrap them using
`AgentTool`:
```python
from google.adk.agents import LlmAgent
from google.adk.tools.agent_tool import AgentTool
# The local coordinator delegates tasks to the server-backed agents
root_agent = LlmAgent(
name='managed_tool_coordinator',
description='Calls managed specialists as tools and composes the answer.',
tools=[
AgentTool(agent=managed_search_agent),
AgentTool(agent=managed_code_execution_agent),
],
)
```
## How it works
The `ManagedAgent` implements the `BaseAgent` contract but bypasses standard
`generate_content` calls, instead sending interactions via
`_create_interactions` with `background=True`. It natively streams partial
events and terminal events in real-time back to the ADK `Runner` or parent flow.
When using the GEAP backend, it enforces a connection to the `global` location
since the Managed Agents API is solely available globally. Because it runs
remotely, tools are translated into standard `ToolParam` formats for
interactions; any raw `google.genai.types.Tool` configs are passed through to
the backend, enabling server-side code execution or remote google search
seamlessly.
### State: local session vs. remote
`ManagedAgent` keeps almost no state locally. The ADK session only persists two
values on the events it emits: the `previous_interaction_id` and the sandbox
`environment_id`. On each new turn the agent recovers both by scanning prior
session events, then reuses them so the conversation and its sandbox continue.
Everything else lives server-side. The Managed Agents API owns the sandbox
environment and the full interaction history, and that remote interaction — not
the local session — is the source of truth for continuing a conversation.
Response text appears in both places (the local ADK events and the remote
interaction history), but ADK stores only the ids it needs to recover and reuse
the remote state; it never re-sends prior turns.
## Advanced applications
### Tool encapsulation for orchestration
* **Problem solved**: Sometimes a single LLM request needs to compose results
from multiple independent, robust specialists without losing control of the
execution turn.
* **Implementation**: Encapsulate each `ManagedAgent` instance within its own
separate `AgentTool` and provide them as a list of tools to an `LlmAgent`
coordinator. The coordinator will invoke the managed agents (which run their
sandboxed logic server-side), collect the results, and then compose the
final synthesized response natively.
## Limitations
* **Location pinned (GEAP only)**: For the GEAP backend, the Managed Agents
API is currently only served from the `global` location. Enterprise clients
using regional endpoints will raise an error.
* **Server-side tools only**: Client-executed tools (Python functions,
callables) and MCP tools are not supported. Providing these will raise a
`NotImplementedError`.
* **Streaming only**: The agent only supports streaming interactions.
Background-polling execution or strictly non-streaming connections are not
yet fully supported (it natively uses `stream=True` and yields events).
## Related samples
* [Managed Agent Basic](../../../../contributing/samples/managed_agent/basic)
* [Managed Agent Code Execution](../../../../contributing/samples/managed_agent/code_execution)
+112
View File
@@ -0,0 +1,112 @@
# Event and NodeInfo
The `event.py` file defines the `Event` and `NodeInfo` classes, which are the fundamental data structures used in the Agent Development Kit (ADK) to represent interactions, actions, and metadata within a workflow.
## Introduction
In ADK, conversations and workflow executions are modeled as a sequence of events. The `Event` class represents a single unit of this sequence, capturing:
- **Content:** Messages exchanged between users and agents (text, function calls, function responses).
- **Actions:** Side-effects or instructions, such as state updates, routing decisions, agent transfers, and UI rendering requests.
- **Metadata:** Information about who generated the event, when, and from which part of the workflow.
`NodeInfo` specifically carries metadata about the workflow node that generated the event, enabling tracking of execution paths and run IDs.
Key classes depending on `Event` include `Session` (which stores the event history) and `Workflow` / `NodeRunner` (which use events for execution flow and state management).
## Get started
Here is how to create and use `Event` objects.
### Basic Message Event
You can create a simple event with a text message:
```python
from google.adk.events.event import Event
# Create a user message event
user_event = Event(author="user", message="Hello, agent!")
# The 'message' argument is a convenience alias for 'content'
print(user_event.message.parts[0].text) # Output: Hello, agent!
```
### Event with State Delta
Events can carry state updates that should be applied to the session state:
```python
from google.adk.events.event import Event
# Create an agent event that updates the state
state_event = Event(
author="my_agent",
message="I've updated the user preference.",
state={"user_theme": "dark"}
)
print(state_event.actions.state_delta) # Output: {'user_theme': 'dark'}
```
### Event with Node Metadata
When events are generated within a workflow, they usually include node information.
> [!NOTE]
> `NodeInfo` is automatically populated by the ADK framework. While you can access these fields, you should not manually construct or modify `node_info` in your application logic.
```python
from google.adk.events.event import Event, NodeInfo
node_event = Event(
author="agent_node",
node_path="parent_workflow/child_node@run-123",
output="some_result"
)
print(node_event.node_info.path) # Output: parent_workflow/child_node@run-123
print(node_event.node_info.name) # Output: child_node
print(node_event.node_info.run_id) # Output: run-123
```
## How it works
`Event` inherits from `LlmResponse`, which allows it to directly wrap responses from Gemini models, including content, grounding metadata, and token usage.
### Convenience Kwargs Routing
The `Event` constructor accepts several convenience arguments that are automatically routed to nested Pydantic models:
- `message`: Automatically converted to `types.Content` and set to the `content` field.
- `state`: Mapped to `actions.state_delta`.
- `route`: Mapped to `actions.route`.
- `node_path`: Mapped to `node_info.path`.
This routing is handled by the `@model_validator(mode='before')` method `_accept_convenience_kwargs`.
### Serialization
Both `Event` and `NodeInfo` are Pydantic models configured to use camelCase aliases for serialization. When sending events over the wire or saving them, use `model_dump(by_alias=True)` to ensure compatibility with ADK APIs.
### Lifecycle
Every event is assigned a unique UUID `id` and a `timestamp` upon initialization if they are not explicitly provided.
## Advanced applications
### Workflow Routing
Workflows use `Event` to communicate routing decisions. By setting `route` (which maps to `actions.route`), a node can signal to the workflow engine which edge to follow next.
```python
routing_event = Event(author="router_node", route="success_path")
```
### Context Isolation
The `isolation_scope` field is used by the Task API to isolate conversations of delegated agents. Events with a specific `isolation_scope` (e.g., `"task:fc-987"`) will only be visible to agents running within that same scope, preventing them from seeing the main conversation history.
## Limitations
- **NodeInfo Assignment:** The `node_info` field (and the `node_path` constructor argument) is managed and assigned by the ADK framework during workflow execution. Developers should not manually set or modify `node_info` in production code.
- **Internal Fields:** The `isolation_scope` field is an internal implementation detail. External developers should not rely on it or modify it directly.
- **Mutual Exclusion:** You cannot specify both `message` and `content` in the `Event` constructor; doing so will raise a `ValueError`.
+53
View File
@@ -0,0 +1,53 @@
# RequestInput
The `RequestInput` class represents a structured request for input from the user, typically used to trigger an interrupt in a workflow (Human-in-the-loop).
## Introduction
In ADK, workflows can be configured to pause and wait for user intervention. The `RequestInput` event is the data structure that represents this interrupt request. It is typically yielded by a workflow node and translated into an `Event` with a special function call (`adk_request_input`) that the client application handles.
Key classes depending on `RequestInput` include `Workflow` (which pauses execution when encountering this event) and various HITL helper utilities (like `create_request_input_event` and `create_request_input_response`) that wrap it. It solves the developer problem of pausing a workflow and gathering structured feedback from a user before resuming.
## Get started
To request input from a user within a workflow, you yield a `RequestInput` object from a node function.
Here is a basic example of a node that requests user details:
```python
from typing import Any, AsyncGenerator
from google.adk import Context
from google.adk.events.request_input import RequestInput
from pydantic import BaseModel
class UserDetails(BaseModel):
name: str
age: int
async def request_input_node(
ctx: Context,
node_input: Any,
) -> AsyncGenerator[Any, None]:
"""A simple node that requests input from the user."""
# Yield RequestInput to pause and request user details.
# The response must conform to UserDetails schema.
yield RequestInput(
interrupt_id="get-user-details-1",
message="Please provide user details.",
response_schema=UserDetails,
)
```
## How it works
When a node yields a `RequestInput` object, the following process occurs:
1. **Workflow Pause**: The workflow engine detects the `RequestInput` event and pauses the execution of the workflow.
2. **Event Translation**: The `RequestInput` is wrapped into an `Event` containing a mock function call named `adk_request_input`. The fields `message`, `payload`, and `response_schema` are passed as arguments to this function call.
3. **Client Interaction**: The client application receiving this event displays the message to the user (optionally validating the input against the provided `response_schema`).
4. **Resuming execution**: To resume the workflow, the client sends back a `FunctionResponse` matching the `interrupt_id` (used as the function call `id`) and named `adk_request_input`. The response payload is placed inside the `response` dictionary.
5. **Resume**: The workflow engine delivers this response back to the node, allowing it to continue execution.
## Limitations
- **Client-Side Validation**: When using `response_schema`, the client application is responsible for validating that the user's input conforms to the schema before sending it back to resume the workflow. ADK handles parsing on resume, but client-side validation is recommended for a better user experience.
@@ -0,0 +1,139 @@
# to_mcp_server
Exposes an ADK agent as an MCP server so any MCP host (Claude Code, OpenAI
Codex, an IDE, or any MCP client) can drive it as a single tool. It is the MCP
counterpart of `to_a2a`.
## Introduction
`to_mcp_server` turns a whole ADK agent into a standard
[Model Context Protocol](https://modelcontextprotocol.io/) server. The agent —
its model loop and all of its tools — is registered as a *single* MCP tool named
after the agent. A host that speaks MCP sends a request string and receives the
agent's final response; it never imports ADK and does not see the agent's
individual tools.
This solves the problem of making an ADK agent consumable by harnesses that are
not ADK. Where `to_a2a` publishes an agent over A2A, `to_mcp_server` publishes it
over MCP, so coding agents and IDEs that already speak MCP can delegate a task to
an ADK agent as if it were any other tool. It builds on `Runner` to execute the
agent and returns a `FastMCP` server, leaving the choice of transport (stdio for
local hosts, streamable-http for networked ones) to the caller.
## Get started
Define an agent and expose it. Running the file starts the MCP server on stdio;
an MCP host can also launch it as a subprocess.
```python
import random
from google.adk.agents import LlmAgent
from google.adk.tools.mcp_tool import to_mcp_server
def roll_die(sides: int) -> int:
"""Roll a die with the given number of sides and return the result."""
return random.randint(1, sides)
dice_agent = LlmAgent(
name="dice_agent",
description="Rolls dice with any number of sides and reports the outcome.",
instruction="Use the roll_die tool to roll the dice the user asks for.",
tools=[roll_die],
)
# The whole agent becomes one MCP tool named "dice_agent".
server = to_mcp_server(dice_agent)
if __name__ == "__main__":
server.run(transport="stdio")
```
A host configured to launch this file sees one tool, `dice_agent`, and calls it
with a `request` string; the ADK agent runs its own model and `roll_die` loop and
returns the answer.
## How it works
`to_mcp_server` creates a `FastMCP` server and registers one tool whose handler
runs the agent through a `Runner`. If no `runner` is supplied, one is built with
in-memory session, artifact, memory, and credential services.
On each tool call the handler:
1. Resolves an ADK session (see below), then wraps the incoming `request` string
as a user `Content`.
2. Drives `Runner.run_async` and iterates the event stream.
3. Forwards intermediate (non-final) text events to the host as MCP **progress
notifications**, so the host can show the agent working in real time.
4. Maps the parts of the final response to MCP content blocks and returns them:
text becomes `TextContent`, inline image data becomes `ImageContent`, audio
becomes `AudioContent`, and any other inline data becomes an
`EmbeddedResource`. This is why a multimodal agent's output is preserved
rather than flattened to text.
### Session continuity
`to_mcp_server` keeps one ADK session per MCP connection, so successive tool
calls on the same connection form a single multi-turn conversation. The mapping
from connection to session is held in a `weakref.WeakKeyDictionary`, so a
session's entry is dropped when its connection is garbage-collected. Over stdio
there is one connection per process, so all calls share one conversation; over
streamable-http each client connection gets its own session.
`to_mcp_server` depends on `Runner`, the agent (`BaseAgent`/`LlmAgent`),
`google.genai.types`, and `mcp.server.fastmcp.FastMCP`; it returns a `FastMCP`
that the caller runs on a transport of their choice.
## Configuration options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `agent` | `BaseAgent` | *required* | The agent to serve. Its model loop and all of its tools are exposed together as one MCP tool. |
| `name` | `str \| None` | `None` | The MCP server and tool name. Defaults to the agent's name (or `"adk_agent"`). Set it when you want the tool to appear under a name other than the agent's. |
| `instructions` | `str \| None` | `None` | Optional server instructions an MCP host may surface to its model to describe how to use the tool. |
| `runner` | `Runner \| None` | `None` | A pre-built `Runner`. If omitted, one is created with in-memory services. Supply your own to use persistent or custom session, artifact, memory, or credential services — this is the recommended path for a long-lived networked server. |
## Advanced applications
### Serving over the network
* **Problem solved**: a host on another machine needs to reach the agent.
* **Implementation**: run the same server with the networked transport:
`server.run(transport="streamable-http")`. Nothing about the agent changes;
only the transport differs.
### Bringing your own services
* **Problem solved**: the default in-memory services do not persist across
process restarts and are not suited to multi-client production serving.
* **Implementation**: build a `Runner` with your chosen services and pass it in:
`to_mcp_server(agent, runner=my_runner)`. The tool then uses those services
for every call.
### Multimodal responses
* **Problem solved**: the agent produces images or audio, not just text.
* **Implementation**: no extra work — non-text parts of the final response are
returned as `ImageContent`, `AudioContent`, or `EmbeddedResource`, so the
host receives them alongside any text.
## Limitations
* **Text input only**: the tool accepts a single `request` string. Passing
media *into* the agent is not supported through the tool call, because MCP
tool arguments are JSON that the host's model fills in and hosts do not place
media in tool arguments. For media input, use MCP resources or elicitation
instead.
* **Default services are in-memory**: for a long-lived streamable-http server,
sessions accumulate with no eviction; inject a `runner` with a persistent or
cleaning session service. Tool calls on a single connection are expected to
be sequential, since they share one session.
* **Experimental**: `to_mcp_server` is `@experimental` and lives behind the
`mcp` extra; its behavior may change in future releases.
## Related samples
* [MCP: serve an ADK agent](../../../../../contributing/samples/mcp/mcp_serve_agent)
+199
View File
@@ -0,0 +1,199 @@
# Dynamic Node Scheduling
Dynamic node scheduling allows you to execute workflow nodes dynamically at runtime using `ctx.run_node()`. This enables imperative workflow construction using standard Python control flow instead of static graph edges.
## Introduction
While static graph definitions (`Workflow(edges=[...])`) are suitable for many structured tasks, some scenarios require more flexibility. For example, you might need to:
- Loop a set of nodes until a condition is met (e.g., generator-evaluator loops).
- Run a variable number of tasks in parallel based on runtime input (dynamic fan-out).
- Conditionally execute nodes based on complex logic that is difficult to express in static edges.
`ctx.run_node()` allows a parent node to execute a child node (which can be a function, an Agent, or another Workflow) and await its result.
## Get started
The following example demonstrates how to dynamically execute a child agent from a parent node.
```python
from google.adk import Agent, Context, Event, Workflow
from google.adk.workflow import node
# Define a child agent
generate_headline = Agent(
name="generate_headline",
instruction="Write a catchy headline about the topic in the user message.",
)
# Define the parent orchestrator node (MUST have rerun_on_resume=True)
@node(rerun_on_resume=True)
async def orchestrate(ctx: Context, node_input: str) -> str:
# Dynamically execute the child agent and await its output
headline = await ctx.run_node(generate_headline, node_input=node_input)
yield Event(output=headline)
# Build the workflow
root_agent = Workflow(
name="root_agent",
edges=[("START", orchestrate)],
)
```
## How it works
When `await ctx.run_node(node_like, ...)` is called:
1. **Orchestrator Registration**: The workflow's `DynamicNodeScheduler` registers the child node execution.
2. **State Tracking**: The execution state and events of the child node are tracked under the parent node's path (e.g., `parent_node@1/child_node@1`).
3. **Resumption Support**: If the child node interrupts (e.g., waiting for user input), the parent node is also paused. When the workflow resumes, the parent node is re-run from the beginning (`rerun_on_resume=True`), but previous successful `ctx.run_node()` calls are replayed from history (cached outputs are returned) to avoid re-executing completed steps.
### Input Mapping
The `node_input` passed to `ctx.run_node(node, node_input=value)` is delivered differently depending on the type of the child node:
- **Python Functions / FunctionNodes**: The `value` is passed directly to the function parameter named `node_input`. Other parameters are bound from the session state (default mode).
- **Agents (Single-Turn Mode)**: The `value` is converted to a user-role message (`types.Content`) and appended to the session events history. The agent receives it as the incoming user message.
- **Agents (Task Mode)**: The `value` is set as `user_content` in the `InvocationContext`, serving as the fallback first user turn for the task agent if it wasn't triggered by a tool call.
## Requirements & Rules
### 1. `rerun_on_resume=True` is Mandatory for Parents
Any node that calls `ctx.run_node()` **must** be configured with `rerun_on_resume=True`.
If the parent node does not have this setting, calling `ctx.run_node()` will raise a `ValueError` at runtime.
### 2. Function Parameter Mapping (`node_input` vs. Dict Binding)
By default, functions wrapped as nodes look up their arguments in the session state (state binding). However, the `node_input` argument passed to `ctx.run_node(..., node_input=value)` is passed directly to the node.
How you receive this input depends on how you define your function:
#### Pass-through `node_input` (Default)
To receive the raw `value` directly, the function's parameter must be named exactly `node_input`.
```python
# Correct: receives the raw value passed to node_input
def my_worker(node_input: str):
return f"Done: {node_input}"
# Incorrect: will fail because it tries to look up 'data' in session state
def my_worker(data: str):
return f"Done: {data}"
```
#### Binding Dictionary Keys to Parameters (`parameter_binding='node_input'`)
If you pass a dictionary to `node_input` (e.g., `node_input={'foo': 'bar'}`) and want to bind its keys to individual function parameters (e.g., `def my_worker(foo: str)`), you must configure the node with `parameter_binding='node_input'`.
You can configure this using the `@node` decorator with `parameter_binding='node_input'`:
```python
from google.adk.workflow import node
# Decorate with parameter_binding='node_input'
@node(parameter_binding='node_input')
def my_worker(foo: str):
return f"Done: {foo}"
# Call via ctx.run_node
result = await ctx.run_node(my_worker, node_input={'foo': 'bar'}) # foo gets 'bar'
```
### 3. Nested Dynamic Nodes
If a dynamically scheduled node *itself* calls `ctx.run_node()`, it becomes a parent and must also have `rerun_on_resume=True`.
You should decorate the nested function with `@node(rerun_on_resume=True)` to ensure it has this property when executed:
```python
from google.adk.workflow import node
@node(rerun_on_resume=True)
async def inner_parent(ctx: Context):
# Calls another dynamic node internally
result = await ctx.run_node(some_child)
yield Event(output=result)
# In the outer parent:
await ctx.run_node(inner_parent)
```
### 4. Generator Returns
In nodes that use `yield` (generators), you cannot use `return value` to produce the final output of the node due to Python syntax constraints. You must yield `Event(output=value)` instead.
## Method Signature
```python
async def run_node(
self,
node: NodeLike,
node_input: Any = None,
*,
use_as_output: bool = False,
run_id: str | None = None,
use_sub_branch: bool = False,
override_branch: str | None = None,
) -> Any:
```
### Parameters
| Parameter | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `node` | `NodeLike` | *Required* | The node to execute (Function, Agent, or Workflow). |
| `node_input` | `Any` | `None` | Input data to pass to the dynamic node. |
| `use_as_output` | `bool` | `False` | If `True`, the child node's output is used as the calling parent node's output. The parent's own output event is suppressed. Can only be set once per parent execution. |
| `run_id` | `str \| None` | `None` | Optional custom run ID. If provided, **must contain non-numeric characters** (e.g., `"run_a"`) to prevent collision with auto-generated IDs. |
| `use_sub_branch` | `bool` | `False` | If `True`, executes the node in a sub-branch (appending `node_name@run_id` to the branch path). Essential for parallel runs to isolate events. |
| `override_branch` | `str \| None` | `None` | Explicitly overrides the branch name for the execution context. |
## Advanced Applications
### Dynamic Fan-Out (Parallel Execution)
You can perform dynamic fan-out by scheduling multiple tasks in parallel using `asyncio.gather`. When doing this, you **must** set `use_sub_branch=True` to isolate the events of each parallel execution.
```python
import asyncio
from google.adk import Context, Event, Agent
from google.adk.workflow import node
worker = Agent(name="worker", instruction="Process {node_input}")
@node(rerun_on_resume=True)
async def parallel_orchestrator(ctx: Context, node_input: list[str]):
tasks = []
for topic in node_input:
tasks.append(
ctx.run_node(
worker,
node_input=topic,
use_sub_branch=True, # Critical for parallel isolation
)
)
# Await all tasks concurrently
results = await asyncio.gather(*tasks)
yield Event(output=results)
```
## Best Practices
- **Avoid Unsupervised Tasks**: Always `await` `ctx.run_node()` directly (or via `asyncio.gather`). Do **not** wrap it in `asyncio.create_task()` without awaiting it, as errors will be swallowed, and tasks won't be cancelled if the workflow is interrupted.
- **Manage Side Effects and Resumption**: Because a parent node with `rerun_on_resume=True` is executed from the beginning on resumption, any code with side effects (e.g., database writes, API calls) in the parent node will run again.
- *Best Practice*: Keep the parent orchestrator node's logic as light as possible, containing mostly control flow and `ctx.run_node` calls.
- *Best Practice*: Move any logic with side effects into dedicated child nodes and execute them via `ctx.run_node`. Since completed child nodes are cached and replayed, their side effects will *not* be executed again on resumption.
## Limitations
- **Replay Overhead**: Because the parent node is re-run from the beginning on resume, long-running parent node logic (outside of `ctx.run_node` calls) will be re-executed. Keep the orchestrator node logic light and delegate heavy lifting to child nodes.
## Related samples
- [Dynamic Nodes Sample](../../../../contributing/samples/workflows/dynamic_nodes/)
- [Dynamic Fan-Out / Fan-In Sample](../../../../contributing/samples/workflows/dynamic_fan_out_fan_in/)
+165
View File
@@ -0,0 +1,165 @@
# Function Nodes
In ADK, any standard Python function, coroutine, or generator can be used as a workflow node. The framework automatically wraps these callables under the hood, allowing you to build complex graphs with minimal boilerplate.
## Introduction
Function nodes are the most common and lightweight way to implement logic in ADK workflows. Instead of subclassing `BaseNode` for every step, you can write standard Python functions.
Developer problems solved:
- **Zero Boilerplate**: Write standard Python code without framework-specific class definitions.
- **Implicit Wrapping**: Pass functions directly to workflow edges; the framework handles integration automatically.
- **Declarative Signatures**: Access workflow state, input from predecessor nodes, or the execution context simply by declaring them in the function parameters.
## Get started
The following example demonstrates how to define standard Python functions and use them directly in a workflow chain.
```python
from google.adk import START, Workflow
# 1. Simple sequential steps
# The output of step_one is automatically passed as input to step_two
def step_one(node_input: str) -> str:
return f"{node_input} -> step_one"
def step_two(node_input: str) -> str:
return f"{node_input} -> step_two"
# 2. Step that accesses workflow state
# user_name is automatically resolved from ctx.state["user_name"]
def step_three(node_input: str, user_name: str) -> str:
return f"Hello {user_name}! {node_input}"
# Use the functions directly in the workflow edges
workflow = Workflow(
name="my_workflow",
edges=[
(START, step_one, step_two, step_three),
],
)
```
## How it works
When a workflow executes a function node, it performs several operations automatically:
### Parameter Resolution
The framework inspects the function signature to determine how to populate its arguments:
* **`ctx`** (or any parameter type-hinted as `Context`): Injects the workflow `Context` object.
* **`node_input`**: Injects the output value from the predecessor node.
* **Any other parameter**: Resolved by looking up the parameter name in `ctx.state` (or `node_input` if parameter binding is customized).
### Type Coercion
Input values are automatically validated and coerced to match the function's type hints using Pydantic:
* **Pydantic Models**: If a parameter is type-hinted as a Pydantic `BaseModel` (e.g., `node_input: MyModel`) and the input is a dictionary, it is auto-converted to the model instance.
* **Content to String**: If a parameter expects a `str` but receives a `types.Content` object (e.g. the raw user message from `START`), it automatically extracts and concatenates the text parts.
### Event Normalization
Return and yield values are normalized to `Event` objects:
* Returning or yielding `None` does not emit an output event, but execution continues downstream with `None` passed as the input to successor nodes.
* Raw values (strings, dicts, etc.) are wrapped in `Event(output=value)`.
* Pydantic models are serialized to dictionaries.
* State changes made via `ctx.state` during execution are automatically captured and attached to the event to be persisted.
## Configuration & Explicit Wrapping
While implicit wrapping works for most cases, you can wrap functions explicitly using the `FunctionNode` class or the `@node` decorator when you need to configure execution behavior.
Use explicit configuration when you need to define:
* `rerun_on_resume`: Control if the node should rerun when the workflow resumes (default is `False` for function nodes, meaning they complete with the resuming input).
* `retry_config`: Enable retries on failures.
* `timeout`: Set a maximum execution time.
* `auth_config`: Gate execution with user authentication.
### Using `@node` Decorator
```python
from google.adk.workflow import node
@node(rerun_on_resume=True)
def process_payment(node_input: dict) -> str:
# This node will rerun if the workflow is resumed after a pause
...
```
### Using `FunctionNode` Class
```python
from google.adk.workflow import FunctionNode, RetryConfig
def my_func(node_input: str) -> str:
...
# Wrap explicitly to configure retries
custom_node = FunctionNode(
my_func,
name="payment_step",
retry_config=RetryConfig(max_attempts=3),
)
```
## Advanced applications
### Emitting Message Events for Web UI
Only the `Event.message` (user-facing content) is rendered in the Web UI, while `Event.output` is internal and passed downstream. For terminal nodes or nodes producing user-visible intermediate results, yield both a message event and an output event:
```python
from google.adk.events.event import Event
async def summarize(ctx: Context, node_input: str):
result = f"Summary: {node_input}"
# Rendered in UI (message accepts a raw string and auto-wraps it)
yield Event(message=result)
# Passed to downstream nodes
yield Event(output=result)
```
### State Integration
You can update the shared workflow state in two ways: by mutating `ctx.state` directly, or by yielding/returning an `Event(state=...)`.
#### 1. Mutating `ctx.state` directly (Imperative)
This is the most common way when your function already accesses the context. Mutations are tracked and automatically persisted by the framework when the node finishes execution.
```python
def update_via_context(ctx: Context, node_input: str) -> str:
# State is updated immediately in memory
ctx.state["counter"] = ctx.state.get("counter", 0) + 1
return node_input
```
#### 2. Yielding/Returning `Event(state=...)` (Declarative)
This is useful if you want to declare state changes as events, or if your function does not need the `ctx` object otherwise.
```python
from google.adk.events.event import Event
def update_via_event(node_input: str):
# Returns the state change without needing 'ctx' in the signature
return Event(
output=node_input,
state={"last_processed": node_input}
)
```
#### Key Differences
| Feature | Mutating `ctx.state` | Yielding `Event(state=...)` |
| :--- | :--- | :--- |
| **Visibility** | Changes are visible **immediately** to subsequent lines in the same function. | Changes are only visible **after** the event is yielded and processed by the framework. |
| **Signature** | Requires `ctx: Context` in the function parameters. | Can be used in any function (no `ctx` required). |
| **Style** | Imperative state modification. | Declarative event-driven state update. |
## Limitations
- **Union Type Hints**: If `node_input` is hinted with a `Union` type (e.g. `str | dict`), the framework skips automatic type validation to avoid false positives. You must perform manual `isinstance` checks in the function body if you need to validate the input type.
## Related samples
The following samples demonstrate function node usage:
- [Node Output](../../../../contributing/samples/workflows/node_output/agent.py) - Auto type conversion to Pydantic models.
- [Route](../../../../contributing/samples/workflows/route/agent.py) - Yielding events with routes.
- [State](../../../../contributing/samples/workflows/state/agent.py) - Interacting with workflow state.
- [Auth API Key](../../../../contributing/samples/workflows/auth_api_key/agent.py) - Using authentication.
- [Request Input Advanced](../../../../contributing/samples/workflows/request_input_advanced/agent.py) - Human-in-the-loop with schemas.
+133
View File
@@ -0,0 +1,133 @@
# Workflow Graphs
In ADK 2.0, workflows are represented as directed graphs where execution flows from node to node along defined edges. This guide explains the core concepts of **nodes**, **edges**, and **graphs**, how to define them, and the validation rules enforced by the framework.
## Introduction
A workflow graph defines the execution plan for your multi-step agent interactions. It specifies:
- What tasks to run (Nodes).
- The order of execution (Edges).
- How data flows and how branches fork or merge.
The graph structure is compiled and validated when you instantiate the `Workflow` class.
## Core Concepts
### Nodes (`NodeLike`)
A node represents a single unit of execution in the workflow. In ADK, you can use several types of objects as nodes (collectively referred to as `NodeLike`):
1. **Python Functions**: Sync or async functions (and generators) decorated with `@node`. They are automatically wrapped in a `FunctionNode`.
2. **Agents**: `LlmAgent` instances (typically in `single_turn` mode). They are automatically wrapped in an internal `_LlmAgentWrapper`.
3. **Tools**: `BaseTool` instances. They are wrapped in a `ToolNode`.
4. **Workflows**: A `Workflow` is itself a `BaseNode` and can be nested as a child node in another workflow.
5. **`START`**: A special sentinel node that marks the entry point of the workflow. Every graph must have exactly one edge starting from `START`.
### Edges (`Edge`)
An edge defines a transition from a source node (`from_node`) to a destination node (`to_node`).
#### Unconditional Edges
By default, edges are unconditional. When the source node completes, execution immediately transitions to the destination node.
#### Conditional Edges (Routing)
An edge can be associated with one or more **routes** (a string, integer, or boolean). The edge is only followed if the source node explicitly emits a matching route.
To emit a route, the source node must yield an `Event(route="my_route")` (or return/yield an object that maps to that route).
#### Default Route
You can define a fallback edge using `DEFAULT_ROUTE` (imported as `from google.adk.workflow import DEFAULT_ROUTE` or using `"__DEFAULT__"`). This edge is followed if the source node emits a route, but no specific conditional edge matches it.
---
## Defining the Graph (Syntax)
You define the graph structure by passing a list of `edges` to the `Workflow` constructor. ADK supports two syntax styles:
### 1. Chain Tuples (Recommended)
Chain tuples provide a concise way to define sequential, parallel, and conditional transitions using Python tuples.
* **Sequential Chain**:
```python
edges=[
(START, step_a, step_b, step_c),
]
```
This defines: `START -> step_a -> step_b -> step_c`.
* **Parallel Fan-Out**: Use a tuple of nodes to split execution into parallel branches.
```python
edges=[
(START, step_a, (step_b, step_c)),
]
```
This defines: `START -> step_a`, and then `step_a -> step_b` AND `step_a -> step_c` in parallel.
* **Conditional Routing**: Use a dictionary (Routing Map) to define conditional branches.
```python
from google.adk.workflow import DEFAULT_ROUTE
edges=[
(START, step_a, {
"success": step_b,
"failure": step_c,
DEFAULT_ROUTE: fallback_step,
}),
]
```
If `step_a` yields `Event(route="success")`, it goes to `step_b`. If it yields `"failure"`, it goes to `step_c`. Any other route goes to `fallback_step`.
### 2. Explicit Edge Objects
For complex graphs or when you prefer explicit declarations, you can use `Edge` objects:
```python
from google.adk.workflow import Edge, START
edges=[
Edge(from_node=START, to_node=step_a),
Edge(from_node=step_a, to_node=step_b, route="success"),
Edge(from_node=step_a, to_node=step_c, route="failure"),
]
```
---
## Graph Validation
When a `Workflow` is initialized, it builds an internal `Graph` representation and runs `validate_graph()` to catch structural errors early. The following rules are strictly enforced:
### 1. Unique Node Names
All distinct node objects in the graph must have unique names.
* *Error*: If you have two different function nodes named `process_data`, validation will fail.
* *Solution*: Ensure unique names, or reuse the exact same object instance if you want to route back to the same node.
### 2. Single START Entry Point
The graph must contain the `START` node, and `START` must not have any incoming edges.
* *Error*: A graph without `START` or with an edge pointing back to `START` will fail validation.
### 3. Connectivity (Reachability)
All nodes in the graph must be reachable from the `START` node.
* *Error*: If you define a node but do not connect it to the rest of the graph, validation will fail.
### 4. No Duplicate Edges
You cannot define duplicate edges between the same two nodes.
* *Error*: `Edge(from_node=A, to_node=B)` and `Edge(from_node=A, to_node=B)` in the same list will fail.
### 5. Default Route Constraints
- A node can have at most one outgoing `DEFAULT_ROUTE` edge.
- `DEFAULT_ROUTE` cannot be combined with other routes in a list (e.g., `route=["success", DEFAULT_ROUTE]` is invalid).
### 6. No Unconditional Cycles
The graph must not contain cycles consisting entirely of unconditional edges (edges with no route).
* *Allowed*: Conditional loops are allowed (e.g., `A -> B -> A` where `B -> A` is conditional on a route).
* *Forbidden*: Unconditional loops (`A -> B -> A` with no routes) are rejected to prevent infinite execution loops.
### 7. Static Schema Matching
If a node defines an `output_schema` and its successor defines an `input_schema`, they must match exactly.
* *Error*: Schema mismatch on transition edges will fail validation.
### 8. Chat Agent Wiring
`LlmAgent` instances configured with `mode='chat'` are only allowed to follow the `START` node.
* *Reason*: Chat-mode agents manage their own conversational history and cannot consume direct inputs from preceding nodes in a workflow chain. For sequential steps, use `mode='single_turn'`.
+107
View File
@@ -0,0 +1,107 @@
# JoinNode
`JoinNode` is a built-in workflow node used to synchronize parallel execution paths (fan-out/fan-in) by waiting for all its predecessor nodes to complete.
## Introduction
In complex workflows, you may want to run multiple tasks in parallel to improve efficiency or perform independent operations, and then aggregate their results before proceeding. `JoinNode` solves this by acting as a synchronization barrier. It waits for all incoming edges to complete and aggregates their outputs into a single dictionary, which is then passed to the downstream node.
Key features:
- **Synchronization**: Automatically pauses execution of downstream paths until all parallel predecessor branches have completed.
- **Aggregation**: Combines outputs from multiple nodes into a single structured dictionary.
- **Branch Resolution**: Computes a common branch prefix for the output event, merging parallel branches.
## Get started
The following example demonstrates a simple fan-out/fan-in workflow where three tasks run in parallel, and their results are aggregated by a `JoinNode`.
```python
from typing import Any
from google.adk import Event, Workflow
from google.adk.workflow import JoinNode
# Define parallel tasks
def make_uppercase(node_input: str) -> str:
return node_input.upper()
def count_characters(node_input: str) -> int:
return len(node_input)
def reverse_string(node_input: str) -> str:
return node_input[::-1]
# Define the JoinNode
join_node = JoinNode(name="join_for_results")
# Define the aggregation node
async def aggregate(node_input: dict[str, Any]):
yield Event(
message=(
f"Uppercase: {node_input['make_uppercase']}\n"
f"Character Count: {node_input['count_characters']}\n"
f"Reversed: {node_input['reverse_string']}\n"
),
)
# Build the workflow
root_agent = Workflow(
name="root_agent",
edges=[(
"START",
(make_uppercase, count_characters, reverse_string),
join_node,
aggregate,
)],
)
```
## How it works
`JoinNode` inherits from `BaseNode` and overrides key behaviors to support synchronization:
1. **Waiting for Predecessors**: It sets `_requires_all_predecessors` to `True`. The workflow orchestrator checks this property and ensures that `JoinNode` is only executed after all nodes pointing to it have completed.
2. **Input Aggregation**: When executed, the orchestrator provides `JoinNode` with a dictionary containing the outputs of all its predecessors. The keys of this dictionary are the names of the predecessor nodes, and the values are their respective outputs.
3. **Pass-through Execution**: The `JoinNode`'s `_run_impl` simply yields this aggregated dictionary as its output event.
4. **Branch Merging**: In parallel execution, nodes might run in different branch contexts (e.g., `NodeA@1`, `NodeB@1`). `JoinNode` computes the common branch prefix of all incoming triggers. If they ran in parallel branches of the same iteration, they are merged back into the parent branch context.
## Configuration options
`JoinNode` does not introduce new configuration options beyond what is inherited from `BaseNode`. However, it overrides the behavior of `input_schema`:
| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `input_schema` | `SchemaType` | `None` | Schema to validate **individual** trigger inputs (outputs of predecessor nodes), not the aggregated dictionary. |
### Input Schema Validation
When `input_schema` is set on a `JoinNode`, it validates each predecessor's output individually as it arrives (or during aggregation).
- If a predecessor's output is a dictionary, it is validated against the `input_schema`.
- If a predecessor's output is `None`, validation is skipped for that input.
- If validation fails for any input, the workflow execution fails.
Example using `input_schema`:
```python
from pydantic import BaseModel
from google.adk.workflow import JoinNode
class ProcessedData(BaseModel):
value: int
status: str
# This JoinNode will ensure that every predecessor node outputs data
# that conforms to the ProcessedData schema.
validation_join = JoinNode(
name="validation_join",
input_schema=ProcessedData
)
```
## Limitations
- **Dictionary Output**: The output of `JoinNode` is always a dictionary with predecessor node names as keys. If you need a different format, you must use a downstream node to transform it.
- **Conditional Routing**: If a `JoinNode` has a predecessor that is part of a conditional routing path, and that path is not taken, the `JoinNode` will never trigger, and the workflow may hang or stall. All static predecessors defined in the graph for a `JoinNode` must execute and complete.
## Related samples
- [Fan-Out / Fan-In Sample](../../../../contributing/samples/workflows/fan_out_fan_in/)
@@ -0,0 +1,70 @@
# ParallelWorker
`ParallelWorker` is a workflow node wrapper that executes a wrapped node in parallel for each item in an input list.
## Introduction
When processing a list of items (e.g., a list of documents to analyze, queries to run, or topics to explain), running them sequentially can be slow, especially if the processing node performs I/O-bound operations like calling an LLM or an external API. `ParallelWorker` solves this by executing the wrapped node concurrently for all items in the input list, significantly reducing total execution time.
Key features:
- **Concurrency**: Runs multiple instances of the wrapped node in parallel (optionally throttled via `max_parallel_workers`).
- **Aggregation**: Gathers all outputs and returns them as a single list, maintaining the original order of the inputs.
- **Error Propagation**: If any parallel task fails, all other pending tasks are cancelled, and the error is raised immediately.
## Get started
There are two ways to enable parallel worker mode:
### 1. Using the `@node` decorator (Recommended for functions)
You can wrap a Python function in a parallel worker by setting `parallel_worker=True` in the `@node` decorator.
```python
from google.adk.workflow import node
@node(parallel_worker=True)
async def process_item(item: str) -> str:
# This function will receive a single item from the list
# and runs in parallel for each item.
return f"Processed: {item}"
```
### 2. Using `Agent` configuration (Recommended for Agents)
You can configure an `Agent` to run in parallel worker mode when used as a workflow node.
```python
from google.adk import Agent
analyzer_agent = Agent(
name="analyzer",
instruction="Analyze the following text: {node_input}",
parallel_worker=True
)
```
## How it works
1. **Input Handling**: The parallel worker expects a `list` as input. If it receives a single non-list item, it automatically wraps it in a single-element list.
2. **Task Spawning**: It iterates through the input list and spawns an asynchronous task for each item using `ctx.run_node(..., use_sub_branch=True)`. This creates a sub-branch for each task (e.g., `parent_node@1/worker_node@1`, `parent_node@1/worker_node@2`).
3. **Result Ordering**: Although tasks run in parallel and may complete out of order, the worker keeps track of the original index of each item and places the results in the correct order in the final output list.
4. **Failure Handling**: If any of the parallel tasks raises an exception:
- The worker immediately catches it.
- It cancels all other currently running/pending tasks for this worker.
- It waits for the cancellation of those tasks to complete.
- It re-raises the original exception, failing the node.
## Advanced applications
### Human-in-the-Loop (HITL) in Parallel
If the wrapped node triggers an interrupt (e.g., `RequestInput`), the parallel worker handles it. However, because tasks run concurrently, multiple tasks might trigger interrupts. The workflow engine will handle these interrupts, but the user experience may vary depending on how the runner manages multiple active interrupts.
## Limitations
- **List Input**: The worker always expects a list and returns a list. If your upstream node doesn't produce a list, it will be treated as a list of one item.
- **Fail-Fast**: The failure of a single item fails the entire worker and cancels all other items. There is currently no "continue on error" option to collect partial results.
## Related samples
- [Parallel Worker Sample](../../../../contributing/samples/workflows/parallel_worker/)
+103
View File
@@ -0,0 +1,103 @@
# RetryConfig
`RetryConfig` is a configuration class used to define retry policies for workflow nodes, enabling them to automatically handle transient failures.
## Introduction
In distributed systems and AI workflows, transient failures (such as network glitches, rate limits, or temporary service outages) are common. `RetryConfig` allows developers to define a resilient policy for individual nodes. When a node execution fails with a configured exception, the ADK will automatically retrying the node execution according to the specified delay and backoff strategy, before propagating the failure.
Key benefits:
- **Resilience**: Automatically recovers from transient errors.
- **Configurable Backoff**: Supports exponential backoff to avoid overwhelming downstream services.
- **Jitter**: Introduces randomness to retry delays to prevent thundering herd problems.
- **Targeted Retries**: Can be configured to only retry on specific exception types.
## Get started
To enable retries for a node, define a `RetryConfig` and pass it to the node definition.
```python
from google.adk.workflow import RetryConfig, node
# Define a retry configuration
unstable_service_retry = RetryConfig(
max_attempts=3, # Try up to 3 times (1 original + 2 retries)
initial_delay=1.0, # Wait 1 second before the first retry
backoff_factor=2.0, # Double the wait time for subsequent retries (1s, 2s)
exceptions=[ConnectionError, "TimeoutError"] # Only retry on these exceptions
)
# Apply the retry configuration to a node
@node(retry_config=unstable_service_retry)
async def call_unstable_api(node_input: str):
# This operation might raise ConnectionError
return await external_api_client.fetch(node_input)
```
## How it works
When a node configured with `RetryConfig` raises an exception during execution:
1. **Exception Matching**: The `NodeRunner` catches the exception and checks if it matches any of the types specified in `RetryConfig.exceptions`. If `exceptions` is `None` (the default), it matches all exceptions.
1. **Attempt Count Check**: It checks if the current attempt count is less than `max_attempts`.
1. **Delay Calculation**: If a retry is warranted, it calculates the delay. The delay is capped at `max_delay`.
```math
$$\text{delay} = \text{initial\_delay} \times (\text{backoff\_factor}^{\text{attempt} - 1})$$
```
4. **Jitter Application**: If `jitter` is enabled (greater than 0.0), a random offset is added to the delay. The final delay is guaranteed to be non-negative.
```math
$$\text{delay} = \text{delay} + \text{random}(-jitter \times \text{delay}, jitter \times \text{delay})$$
```
5. **Execution Pause and Retry**: The runner sleeps for the calculated delay and then re-executes the node's logic.
## Configuration options
`RetryConfig` is a Pydantic model with the following fields:
| Field | Type | Default | Description |
| :--------------- | :----------------------------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------ |
| `max_attempts` | `int \| None` | `5` (if omitted) | Maximum number of attempts, including the original request. If `0` or `1`, retries are disabled. |
| `initial_delay` | `float \| None` | `1.0` | Initial delay before the first retry, in seconds. |
| `max_delay` | `float \| None` | `60.0` | Maximum delay between retries, in seconds. |
| `backoff_factor` | `float \| None` | `2.0` | Multiplier by which the delay increases after each attempt. |
| `jitter` | `float \| None` | `1.0` | Randomness factor for the delay. Set to `0.0` to disable jitter (deterministic delays). |
| `exceptions` | `list[str \| type[BaseException]] \| None` | `None` | Exceptions to retry on. Can be exception classes or their string names. `None` means retry on all exceptions. |
## Advanced applications
### Exception Normalization
You can specify exceptions in `exceptions` using either the class type itself or its string name. This is useful if you want to avoid importing the exception class at the node definition site, or if the exception is dynamically defined.
```python
retry_config = RetryConfig(
exceptions=[ValueError, "CustomNetworkError"]
)
```
### Deterministic Delays for Testing
For testing purposes, you might want to disable jitter and set low delays to speed up test execution and ensure deterministic behavior.
```python
test_retry_config = RetryConfig(
max_attempts=3,
initial_delay=0.1,
backoff_factor=1.0,
jitter=0.0
)
```
## Limitations
- **State Persistence**: The retry attempt counter is maintained in memory during the execution loop. If the workflow is interrupted (e.g., waiting for a human-in-the-loop input downstream, or if the application restarts) and subsequently resumed, the retry attempt count for the node is **not** persisted. When resumed, if the node needs to run again, the attempt count starts back at 1.
- **Local Retries**: Retries happen locally within the node execution. If a node fails all its retries, the node enters the `FAILED` state, and the workflow execution fails (or follows error handling paths if configured).
## Related samples
- [Resilient Nodes with RetryConfig](../../../../contributing/samples/workflows/retry/)
+131
View File
@@ -0,0 +1,131 @@
# Workflow
`Workflow` is a graph-based orchestration node in ADK 2.0 that executes a Directed Acyclic Graph (DAG) of nodes. It manages the execution flow, including parallel branch execution, dynamic node scheduling, and resuming execution from session events.
## Introduction
The `Workflow` class allows developers to define complex, multi-step agent interactions as a graph of nodes. It acts as the orchestration engine, coordinating the execution of individual nodes (which can be agents, tools, or custom functions) based on defined edges and routing logic.
Key classes that depend on `Workflow`:
- **`Runner`**: The execution engine that runs the workflow.
- **`App`**: The container that registers the workflow as an agent.
Developer problems solved by `Workflow`:
- **Complex Orchestration**: Managing multi-agent systems with conditional transitions and parallel execution.
- **State Preservation and Resumption**: Workflows can pause for human input or external events and resume later, reconstructing their state from session history.
- **Dynamic Execution**: Support for spawning nodes dynamically at runtime based on data.
## Get started
The following example demonstrates a simple sequential workflow with two steps defined using Python functions.
```python
from google.adk import START, Workflow
# Define simple Python functions to act as workflow nodes
def step_one(node_input: str) -> str:
return f"{node_input} -> step_one"
def step_two(node_input: str) -> str:
return f"{node_input} -> step_two"
# Define the workflow graph using edges
workflow = Workflow(
name="simple_workflow",
edges=[
(START, step_one, step_two),
],
)
```
## How it works
- **Compilation**: During initialization, the `Workflow` compiles the provided `edges` into an internal `Graph` representation and validates it (e.g., checking for duplicate node names or unconditional cycles).
- **Execution Loop**: The core orchestration happens in `_run_impl()`. It maintains a loop that:
1. Schedules "ready" nodes (nodes whose predecessors have completed).
2. Runs each scheduled node in a separate `NodeRunner` as an asyncio task.
3. Waits for tasks to complete.
4. Handles completion by caching outputs and buffering triggers for downstream nodes.
- **Rehydration (Resume)**: When a workflow is resumed (e.g., after a human-in-the-loop interrupt), it scans the session history for previous execution events. It reconstructs the state of completed nodes to avoid re-running them and deterministically replays the execution flow up to the interrupt point.
- **Dynamic Scheduling**: Workflows support dynamic node execution via `ctx.run_node()`. The workflow registers a `DynamicNodeScheduler` on the context, allowing nodes to spawn and await other nodes dynamically, bypassing the static graph edges.
## Workflow Output
A workflow is itself a node, and when it finishes, it can produce an output. The output of a workflow is determined by the outputs of its **terminal nodes** (nodes with no outgoing edges):
1. **Single Terminal Output**: The workflow's output is the output of the terminal node that executed and completed with a non-`None` result.
2. **Multiple Terminal Nodes**: If your graph has multiple terminal nodes (e.g., parallel branches):
* If only **one** of them executes and produces an output (e.g., in a conditional branching scenario where only one path is taken), that output becomes the workflow's output.
* If **multiple** terminal nodes execute and produce outputs (e.g., parallel branches executing concurrently), the workflow will fail with a `ValueError` upon completion.
3. **Aggregating Outputs**: If you have parallel branches and want to return their combined results, you **must** use a `JoinNode` to synchronize the branches and aggregate their outputs into a single output before the workflow ends. The `JoinNode` then acts as the single terminal node of the workflow.
## Configuration options
The `Workflow` class introduces specific configuration options to define the graph structure and control execution concurrency:
### `edges`
* **Type**: `list[EdgeItem]`
* **Default**: `[]`
* **Description**: Defines the structure of the workflow graph by specifying connections between nodes. The graph must have a single entry point starting from the `START` sentinel node.
* **Usage Patterns**:
* **Sequential Chain**: Define a list of tuples representing sequential steps.
```python
edges=[(START, step_a, step_b)]
```
* **Parallel Fan-Out**: Route from one node to multiple downstream nodes in parallel.
```python
edges=[
(START, step_a, (step_b, step_c)),
]
```
* **Conditional Routing**: Route to different nodes based on a routing key returned by the predecessor.
```python
edges=[
(START, step_a, {"route_x": step_b, "route_y": step_c}),
]
```
In this case, `step_a` must yield an `Event` with the `route` field set to either `"route_x"` or `"route_y"`.
### `max_concurrency`
* **Type**: `int | None`
* **Default**: `None` (unlimited)
* **Description**: Limits the number of static graph nodes that can execute in parallel. This is useful for managing resource usage or rate limits when running workflows with large parallel branches.
* **Usage Patterns**:
* **Throttling parallel tasks**: If you have a fan-out of 100 tasks but want to run at most 5 at a time:
```python
workflow = Workflow(
name="throttled_workflow",
edges=[(START, list_of_parallel_nodes)],
max_concurrency=5,
)
```
* *Note*: This limit only applies to nodes scheduled via graph edges. Dynamic nodes spawned via `ctx.run_node()` are excluded from this limit to prevent deadlocks (as they are awaited inline by their parent node).
## Advanced applications
### Nested Workflows
A `Workflow` is itself a `BaseNode`, meaning it can be used as a node inside another workflow. This allows for modular and hierarchical workflow design.
### Joining Parallel Branches
You can use `JoinNode` to synchronize parallel execution paths. A `JoinNode` waits for all its predecessors to complete before it executes and aggregates their outputs.
### Dynamic Node Execution
For scenarios where the execution path cannot be predefined, nodes can use `ctx.run_node(node_instance, input)` to execute nodes dynamically.
## Limitations
- **Unconditional Cycles**: The workflow graph validator rejects graphs with unconditional cycles to prevent infinite loops. Cycles must be conditional (i.e., controlled by routing logic).
## Related samples
The following samples demonstrate various workflow features:
- [Sequence Workflow](../../../../contributing/samples/workflows/sequence/agent.py) - Basic sequential execution.
- [Conditional Routing](../../../../contributing/samples/workflows/route/agent.py) - Branching based on node outputs.
- [Looping Workflow](../../../../contributing/samples/workflows/loop/agent.py) - Iterative execution.
- [Nested Workflows](../../../../contributing/samples/workflows/nested_workflow/agent.py) - Workflows containing other workflows.
- [Parallel Execution (Fan-Out/Fan-In)](../../../../contributing/samples/workflows/fan_out_fan_in/agent.py) - Running tasks in parallel and joining them.
- [Dynamic Nodes](../../../../contributing/samples/workflows/dynamic_nodes/agent.py) - Executing nodes dynamically via context.
- [Node Retries](../../../../contributing/samples/workflows/retry/agent.py) - Configuring error handling and retries.