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

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
+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.