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 already-completed nodes
- 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.1.0", = ["sqlite"] }
= "2.1.0"
= "2.1.0"
= "2.1.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.
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 |
full |
Enable all features |
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.1.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) |
Examples
Part of ADK-Rust
This crate is part of the ADK-Rust framework for building AI agents in Rust.