adk-graph
Graph-based workflow orchestration for Rust Agent Development Kit (ADK-Rust) agents, inspired by LangGraph.
Overview
adk-graph provides a powerful way to build complex, stateful agent workflows using a graph-based approach. It brings LangGraph-style capabilities to the Rust ADK ecosystem while maintaining full compatibility with ADK's agent system, callbacks, and streaming infrastructure.
Features
- Graph-Based Workflows: Define agent workflows as directed graphs with nodes and edges
- AgentNode: Wrap LLM agents as graph nodes with custom input/output mappers
- Cyclic Support: Native support for loops and iterative reasoning (ReAct pattern)
- Conditional Routing: Dynamic edge routing based on state
- Fan-out / fan-in: parallel branches run concurrently in a super-step. A node with more than one incoming direct edge is deferred automatically, so it runs once after its branches arrive — branches of unequal length join correctly with no configuration.
mark_deferredsets afan_in_timeoutor an n-of-mmin_predecessorsquorum - State Management: Typed state with reducers (overwrite, append, sum, custom)
- Checkpointing: Persistent state after each step (memory, SQLite)
- Durable Resume: Automatically resume from the last checkpoint after a crash — skips nodes of every checkpointed step. Execution is at-least-once: a crash mid-step re-runs that step's nodes; see Execution Guarantees
- Human-in-the-Loop: Interrupt before/after nodes, dynamic interrupts. A pause is resumable, including one raised inside a subgraph
- Subgraphs: run a compiled graph as a node with mapped channels (
SubgraphNode). A pause inside pauses the parent, and a channel mapping that names a channel neither side declares fails when the parent compiles - Routing from inside a node:
NodeOutput::with_gotonames successors and replaces the node's declared edges;AgentNode::with_goto_mapperroutes on what the agent answered;with_goto_parenthands control to a node of the parent graph - Reliability: per-node retry with capped backoff and jitter, a concurrency bound, per-node timeouts, graph-wide
NodeDefaults, andwith_node_error_handlerto recover once a retry budget is spent - Invoking a node directly:
ctx.run_node_withruns a node the graph has no edge to, sized from state, and records it so a resume does not pay for it twice - As a tool:
NodeToolexposes a graph or a single node through theTooltrait, so anLlmAgentcan call a whole graph - Bounded growth:
RetentionPolicylimits how many checkpoints a thread keeps, by count or age - Strictness, opt-in:
with_strict_channelsfails the run when a node writes a channel the schema does not declare - Streaming: Multiple stream modes (values, updates, messages, debug)
- ADK Integration: Full callback support, works with existing runners
- Functional API (feature:
functional): Write workflows as async functions with automatic checkpointing - Typed State Reducers: ReducedValue, UntrackedValue, MessagesValue containers
- State Schema Validation: Type-level validation at workflow boundaries
- Proc Macros:
#[entrypoint]and#[task]for zero-boilerplate workflow definition
Architecture
┌─────────────────────────────────────────┐
│ Agent Trait │
│ (name, description, run, sub_agents) │
└────────────────┬────────────────────────┘
│
┌───────────────────────┼───────────────────────┐
│ │ │
┌──────▼──────┐ ┌─────────▼─────────┐ ┌─────────▼─────────┐
│ LlmAgent │ │ GraphAgent │ │ RealtimeAgent │
│ (text-based)│ │ (graph workflow) │ │ (voice-based) │
└─────────────┘ └───────────────────┘ └───────────────────┘
Quick Start
Add to your Cargo.toml:
[]
= { = "2.3.0", = ["sqlite"] }
= "2.3.0"
= "2.3.0"
= "2.3.0"
Basic Graph with AgentNode
use ;
use LlmAgentBuilder;
use GeminiModel;
use json;
use Arc;
async
Conditional Routing with LLM Classification
use ;
// Create a classifier agent
let classifier = new;
let classifier_node = new
.with_input_mapper
.with_output_mapper;
// Build conditional routing graph
let graph = with_channels
.add_node
.add_node
.add_node
.add_node
.add_edge
.add_conditional_edges
.add_edge
.add_edge
.add_edge
.compile?;
Human-in-the-Loop with Risk Assessment
use ;
let checkpointer = new;
// Planner agent assesses risk
let planner_node = new
.with_output_mapper;
let graph = with_channels
.add_node
.add_node
.add_node_fn
.add_edge
.add_edge
.add_edge
.add_edge
.compile?
.with_checkpointer_arc;
// Execute - may pause for approval
let result = graph.invoke.await;
match result
ReAct Agent with Tools
use Part;
use FunctionTool;
// Create agent with tools
let reasoner = new;
let reasoner_node = new
.with_output_mapper;
// Build ReAct graph with cycle
let graph = with_channels
.add_node
.add_node_fn
.add_edge
.add_edge
.add_conditional_edges
.compile?
.with_recursion_limit;
Node Types
AgentNode
Wraps any ADK Agent (typically LlmAgent) as a graph node:
let node = new
.with_input_mapper
.with_output_mapper;
FunctionNode
Simple async functions for data processing:
.node_fn
SubgraphNode
Run a compiled graph as a node. The inner graph keeps its own channels, edges and interrupt gates, and exchanges named channels with its parent.
use SubgraphNode;
use Arc;
let outer = with_channels
.add_node
.add_edge
.add_edge
.compile?;
Channels both schemas declare under one name pass through; isolated() requires
every exchange to be named. A pause inside pauses the parent and resumes without
re-running finished work. A mapping naming a channel neither side declares fails at
compile(), as does a subgraph with an interrupt gate but no checkpointer — that
one would re-enter at its first node and pay for its work twice.
A node inside can hand control to its parent:
Ok
Routing From Inside a Node
A conditional edge fixes its targets when the graph is built. A goto does not, and may name any node in the graph:
// A plain node.
Ok
// An LLM-backed node, routing on what it answered.
new
.with_output_mapper
.with_goto_mapper
A goto replaces that node's declared edges. Naming END stops the branch, and
an unknown name fails the run with GraphError::UnknownRouteTarget.
Reliability and Limits
Each of these is off by default, so an existing graph behaves as before.
use RetentionPolicy;
use NodeDefaults;
use RetryPolicy;
let graph = graph
// Every node retries three times unless it says otherwise.
.with_node_defaults
// This one gets ten.
.with_node_retry
// At most four nodes at once, so a wide fan-out cannot trip a rate limit.
.with_max_concurrency
// Recover instead of ending the run, once the retry budget is spent.
.with_node_error_handler
// Keep a long-lived thread from growing without bound.
.with_checkpoint_retention;
| Default | Value |
|---|---|
| Super-steps per run | 100 (recursion_limit) |
| Retry when no policy is attached | one attempt |
RetryPolicy::default() |
ten attempts, about 243s of backoff in total |
| Concurrency | the whole frontier |
| Checkpoints kept | every one, until a retention policy is set |
An interrupt is never retried and never reaches an error handler: a pause is not a failure.
Invoking a Node Directly
When the number of sub-tasks comes from state rather than the graph's shape:
use RunNodeOptions;
let output = ctx
.run_node_with
.await?;
The target needs no edge. Each completed child is recorded under
<parent>/<child>@<run_id>, so a resumed run returns the recorded answer instead of
executing the child again — which matters when the child costs a model call.
A Graph as a Tool
use NodeTool;
let desk = for_graph
.with_name
.with_description;
let agent = new.model.tool.build?;
The parameter schema is derived from the graph's channels, so the tool description and the graph cannot drift apart. It reports itself long-running, so a graph that pauses travels the existing tool-confirmation path.
State Management
Channels and Reducers
let schema = builder
.channel // Overwrite (default)
.list_channel // Append to list
.channel_with_reducer // Sum values
.build;
Checkpointing
// Memory (development)
let checkpointer = new;
// SQLite (production)
let checkpointer = new.await?;
// View checkpoint history
let checkpoints = checkpointer.list.await?;
for cp in checkpoints
Durable Resume
When a checkpointer is configured, the executor automatically checks for existing checkpoints before starting. If a checkpoint exists for the thread ID, execution resumes from where it left off — skipping already-completed nodes:
use *;
use Arc;
let checkpointer = new;
let graph = builder
.description
.node_fn
.node_fn
.edge
.edge
.edge
.checkpointer_arc
.build?;
// First run — completes step_a, saves checkpoint, then crashes before step_b
// Second run — resumes from checkpoint, skips step_a, runs only step_b
let result = graph.invoke.await?;
When streaming, a StreamEvent::Resumed event is emitted to indicate execution was restored from a checkpoint.
A checkpoint records the frontier still to run and the arrivals a fan-in node is waiting on. A dynamic or tool-confirmation pause saves the successors of every node that completed in the step plus the paused node, so a sibling's downstream work still runs after the resume.
Execution Guarantees
Node execution is at-least-once. A super-step runs its nodes and applies their updates before it saves its checkpoint:
| Event | Effect on resume |
|---|---|
| Crash while a super-step is running | Every node of that step runs again, including nodes that had finished |
| Pause or completed step | No node of a checkpointed step runs again |
Make a node with external side effects idempotent, for example with an idempotency key derived from the thread id and step.
GraphAgent Turns
GraphAgent::run uses the session id as the thread id, and each call is a turn. A
paused thread resumes; a finished thread runs again from its entry nodes, starting
from the previous turn's final state with the new input merged on top. The recursion
limit applies per turn. invoke keeps the direct-graph behaviour, where a finished
thread returns its final state.
Examples
Examples are in the adk-playground repo:
# Parallel LLM agents with callbacks
# Sequential multi-agent pipeline
# LLM-based sentiment classification and routing
# ReAct pattern with tools
# Multi-agent supervisor
# Human-in-the-loop with risk assessment
# Checkpointing and time travel
Comparison with LangGraph
| Feature | LangGraph | adk-graph |
|---|---|---|
| State Management | TypedDict + Reducers | StateSchema + Reducers |
| Execution Model | Pregel super-steps | Pregel super-steps |
| Checkpointing | Memory, SQLite, Postgres | Memory, SQLite |
| Human-in-Loop | interrupt_before/after | interrupt_before/after + dynamic |
| Streaming | 5 modes | 5 modes |
| Cycles | Native support | Native support |
| Type Safety | Python typing | Rust type system |
| LLM Integration | LangChain | AgentNode + ADK agents |
Feature Flags
| Flag | Description |
|---|---|
sqlite |
Enable SQLite checkpointer |
functional |
Functional API: TaskContext, typed reducers, schema validation, proc macros |
node-cache |
Per-node result caching; redis-cache adds a Redis store |
delta-checkpoint |
Store the difference between super-steps instead of full state |
time-travel |
Step, fork, and state history |
action |
ActionNodeExecutor for adk-action node configurations |
action-http, action-rss, action-trigger |
HTTP and notification nodes, RSS feeds, cron/webhook trigger runtime |
action-db, action-email, action-code |
Placeholders only — see Action Nodes |
action-full |
Every action-* flag |
full |
sqlite and action-full |
Action Nodes
ActionNodeExecutor runs an adk-action configuration as a graph node.
| Node type | Status |
|---|---|
| Trigger, Set, Transform, Switch, Loop, Merge, Wait, File | Implemented |
Code with language: rust |
Implemented (evaluates JSON or interpolates text) |
HTTP, Notification (action-http), RSS (action-rss) |
Implemented |
| Database, Email | Not implemented in any feature configuration; rejected when the graph compiles |
Code with language: javascript or typescript |
Not implemented; rejected when the graph compiles |
Paths and URLs come from workflow state, so both are confined:
- File nodes stay inside the allowed roots — the working directory unless
with_file_rootsnames others. A..component, or a path that resolves outside every root through a symbolic link, fails the node. - HTTP nodes accept only the schemes, hosts, and redirect count an
HttpActionPolicypermits (with_http_policy). The default allowshttpsandhttpto any host and up to 5 redirects, each re-checked. Logs omit the query string.
License
Apache-2.0
Functional API
The Functional API (feature: functional) provides a higher-level programming model that lets you write agent workflows as normal async Rust functions with automatic checkpointing, typed state reducers, and interrupt/resume support.
Quick Start
[]
= { = "2.3.0", = ["functional"] }
use Arc;
use MemoryCheckpointer;
use ;
use StateSchema;
// Create a TaskContext with a checkpointer
let checkpointer = new;
let schema = builder
.channel
.list_channel
.build;
// ReducedValue — append-only accumulator
let mut results: = new;
results.push;
results.push;
assert_eq!;
// MessagesValue — chat messages with deduplication
let mut messages = new;
// Duplicate IDs are replaced (upsert semantics)
// ExecutionLog — resume-skip behavior
let mut log = new;
log.record_start;
log.record_completion;
assert!; // Skip on resume
Features
| Type | Purpose |
|---|---|
TaskContext |
Runtime context for tasks (state, checkpointing, streaming, interrupts) |
ReducedValue<T> |
Append-only accumulator persisted across checkpoints |
UntrackedValue<T> |
Transient data excluded from checkpoints |
MessagesValue |
Chat messages with ID-based deduplication |
StateSchemaValidator |
Type validation for state and task output |
ExecutionLog |
Task completion tracking for resume-skip |
TypedReducer |
Custom merge strategies (Replace, Append, Merge) |
Task Identity
#[task] records each call of a task under its own key,
{task}#{ordinal}:{arg_hash} (task_call_id): the ordinal counts the task's calls
within the run, and the hash covers the arguments that implement Serialize. A
task called in a loop records one result per call, and a resumed run replays each
call's own result. A call whose arguments differ from the recorded call at the same
position runs again.
Tasks are at-least-once: a crash after a task's body finishes but before its completion is checkpointed runs it again on resume.
Examples
Part of ADK-Rust
This crate is part of the ADK-Rust framework for building AI agents in Rust.