Core concepts
Graphs
Graphs connect systems into agent behavior with sequential, conditional, parallel, and looping control flow.
A graph connects systems into agent behavior — defining what runs, in what order, under what conditions.
use polaris_ai::graph::Graph;
async fn parse_input() -> String { todo!() }async fn call_llm() -> String { todo!() }async fn format_response() -> String { todo!() }
let mut graph = Graph::new();graph .add_system(parse_input) .add_system(call_llm) .add_system(format_response);Graph::new() creates an empty graph. The add_system method appends a system node and chains it sequentially to the previous one. The first system you add becomes the graph’s entry point. The result is a linear pipeline: parse_input runs, then call_llm, then format_response.
parse_input ──▶ call_llm ──▶ format_responseConditional branching
Section titled “Conditional branching”add_conditional_branch adds binary if/else routing based on a predicate over the previous system’s output. The predicate receives &T and returns bool. If true, the first branch runs; otherwise, the second branch runs.
use polaris_ai::graph::Graph;use polaris_ai::models::llm::LlmResponse;
async fn act() -> LlmResponse { todo!() }async fn execute_tools() { }async fn respond() { }
let mut graph = Graph::new();graph.add_system(act);
graph.add_conditional_branch::<LlmResponse, _, _, _>( "has_tool_calls", |response| response.has_tool_calls(), |tool_branch| { tool_branch.add_system(execute_tools); }, |done_branch| { done_branch.add_system(respond); },);The type parameter LlmResponse tells the predicate what output type to read from the context. The two closures each receive a fresh &mut Graph for building their respective subgraphs.
┌─ true ──▶ execute_toolsact ──▶ has_tool_calls? └─ false ─▶ respondadd_loop repeats a subgraph until a termination predicate returns true. The predicate evaluates an output type produced inside the loop body.
use polaris_ai::graph::Graph;
struct ReactState { is_complete: bool,}
async fn init_loop() -> ReactState { todo!() }async fn reason() -> ReactState { todo!() }async fn act() -> ReactState { todo!() }
let mut graph = Graph::new();graph.add_system(init_loop);
graph.add_loop::<ReactState, _, _>( "react_loop", |state| state.is_complete, |g| { g.add_system(reason) .add_system(act); },);The loop body executes repeatedly. After each iteration, the framework evaluates the termination predicate against the most recent ReactState output. When is_complete is true, execution continues past the loop.
┌─────────────────────┐ ▼ │init_loop ──▶ reason ──▶ act ──────┘ │ (exit when is_complete == true) ▼ (continue)For a simple bounded loop without a predicate, use add_loop_n with a maximum iteration count:
graph.add_loop_n("retry", 3, |g| { g.add_system(attempt); });Switch
Section titled “Switch”add_switch provides multi-way routing based on a discriminator function. The discriminator returns a &'static str case key, and execution follows the matching branch.
use polaris_ai::graph::Graph;
struct RouterOutput { action: &'static str }
async fn route() -> RouterOutput { todo!() }async fn use_tool() { }async fn clarify() { }async fn respond() { }
let mut graph = Graph::new();graph.add_system(route);
graph.add_switch::<RouterOutput, _, _, _>( "route_action", |output| output.action, vec![ ("tool", Box::new(|g: &mut Graph| { g.add_system(use_tool); }) as Box<dyn FnOnce(&mut Graph)>), ("clarify", Box::new(|g: &mut Graph| { g.add_system(clarify); }) as Box<dyn FnOnce(&mut Graph)>), ], Some(Box::new(|g: &mut Graph| { g.add_system(respond); })),);The last argument is an optional default branch, used when no case key matches. Switch is useful when the previous system produces more than two possible outcomes.
Parallel
Section titled “Parallel”add_parallel runs multiple branches concurrently. All branches must complete before execution continues.
use polaris_ai::graph::Graph;
async fn fetch_context() { }async fn fetch_tools() { }async fn merge_results() { }
let mut graph = Graph::new();graph.add_parallel("gather", vec![ |g: &mut Graph| { g.add_system(fetch_context); }, |g: &mut Graph| { g.add_system(fetch_tools); },]);graph.add_system(merge_results);Each closure builds an independent subgraph. The branches run simultaneously, and execution resumes from the parallel node’s outgoing edge once all branches finish.
Composing with pipe
Section titled “Composing with pipe”pipe passes &mut Graph to a function, letting you compose reusable graph fragments inline without constructing a separate Graph object.
use polaris_ai::graph::Graph;
async fn log_metrics() { }async fn check_health() { }async fn fetch_data() { }async fn respond() { }
fn monitoring(g: &mut Graph) { g.add_system(log_metrics) .add_system(check_health);}
let mut graph = Graph::new();graph .add_system(fetch_data) .pipe(monitoring) .add_system(respond);The monitoring function adds its systems directly to the current graph. This is different from the branch closures in add_loop or add_conditional_branch, which build into isolated subgraphs.
Scope and dynamic composition
Section titled “Scope and dynamic composition”Use add_scope to embed a subgraph with an explicit context policy. This is the boundary for reusing a sub-agent while controlling which resources it receives. Outputs merge back to the caller; the context policy does not filter those outputs.
use polaris_ai::graph::{Graph, node::ContextPolicy};
async fn summarize() -> String { "A summary".into() }let mut research = Graph::new();research.add_system(summarize);let mut graph = Graph::new();graph.add_scope("research", research, ContextPolicy::new().share_rest());add_dynamic selects among named candidate graphs at runtime. A DynamicSlot declares the required graph signature and context policy; candidates must satisfy that contract. add_dynamic_registry instead reads candidates from a session-local SubgraphRegistry, allowing the candidate set to change between executions. These primitives support replaceable agent components; they do not implement an automatic improvement or training loop.
See the public graph reference for candidate registration and policy examples.
Running a graph
Section titled “Running a graph”You execute a graph with GraphExecutor. The executor traverses the graph from its entry point, running each system with a SystemContext that holds resources, outputs, and parent scopes.
use polaris_ai::graph::{Graph, GraphExecutor};use polaris_ai::system::param::SystemContext;
async fn reason() -> i32 { 1 }async fn act() -> i32 { 2 }
async fn run() -> Result<(), Box<dyn std::error::Error>> { let mut graph = Graph::new(); graph.add_system(reason).add_system(act);
let mut ctx = SystemContext::new(); let executor = GraphExecutor::new(); let result = executor.execute(&graph, &mut ctx, None, None).await?; Ok(())}The execute method returns an ExecutionResult with the number of nodes executed and the total duration. In practice, you rarely call GraphExecutor directly – the Sessions plugin and the Agent trait handle execution for you.
Copyright © 2026, Radiant AI Limited.