Expand description
§neuron-loop
The agentic while-loop for the neuron ecosystem. Composes a Provider, a
ToolRegistry, and a ContextStrategy into a loop that sends messages to an
LLM, executes tool calls, manages context compaction, and repeats until the
model produces a final response or a turn limit is reached.
§Installation
cargo add neuron-loop§Key Types
AgentLoop<P, C>– the core loop, generic overProviderandContextStrategyAgentLoopBuilder<P, C>– builder for constructing anAgentLoopwith optional configurationLoopConfig– system prompt, max turns, parallel tool execution flag, optionalUsageLimitsAgentResult– final output: response text, all messages, cumulative token usage, turn countTurnResult– per-turn result for step-by-step iteration:ToolsExecuted { calls, results }— tool calls were made and executedFinalResponse(AgentResult)— model produced a final text responseCompactionOccurred { old_tokens, new_tokens }— context was compactedMaxTurnsReached— turn limit hitError(LoopError)— something failed
§Usage
Build an AgentLoop using the builder pattern. Only provider and context
are required; tools, config, hooks, and durability are optional with sensible
defaults.
use neuron_loop::{AgentLoop, LoopConfig};
use neuron_tool::ToolRegistry;
use neuron_context::SlidingWindowStrategy;
use neuron_types::ToolContext;
// Set up components
let provider = MyProvider::new("claude-sonnet-4-20250514");
let context = SlidingWindowStrategy::new(20, 100_000);
let mut tools = ToolRegistry::new();
tools.register(MyTool);
// Build and run
let mut agent = AgentLoop::builder(provider, context)
.tools(tools)
.system_prompt("You are a helpful assistant.")
.max_turns(10)
.parallel_tool_execution(true)
.build();
let tool_ctx = ToolContext { /* ... */ };
let result = agent.run_text("What is 2 + 2?", &tool_ctx).await?;
println!("Response: {}", result.response);
println!("Turns: {}", result.turns);
println!("Tokens used: {} in, {} out",
result.usage.input_tokens,
result.usage.output_tokens);For step-by-step iteration (streaming UIs, custom control flow, or injecting messages between turns):
use neuron_loop::{AgentLoop, TurnResult};
use neuron_types::{Message, Role, ContentBlock, ToolContext};
let mut agent = AgentLoop::builder(provider, context).tools(tools).build();
let user_msg = Message {
role: Role::User,
content: vec![ContentBlock::Text("What is 2 + 2?".into())],
};
let tool_ctx = ToolContext::default();
let mut steps = agent.run_step(user_msg, &tool_ctx);
while let Some(turn) = steps.next().await {
match turn {
TurnResult::ToolsExecuted { calls, .. } => {
println!("executed {} tool calls", calls.len());
}
TurnResult::FinalResponse(result) => {
println!("final: {}", result.response);
break;
}
TurnResult::CompactionOccurred { old_tokens, new_tokens } => {
println!("compacted {old_tokens} -> {new_tokens} tokens");
}
TurnResult::MaxTurnsReached => break,
TurnResult::Error(e) => { eprintln!("error: {e}"); break; }
}
}Add observability hooks to log, meter, or control loop execution:
let agent = AgentLoop::builder(provider, context)
.hook(my_logging_hook)
.durability(my_temporal_context)
.build();§Cancellation
The loop respects ToolContext.cancellation_token (a tokio_util::sync::CancellationToken).
Cancellation is checked at the top of each iteration and before tool execution.
When cancelled, the loop returns LoopError::Cancelled.
use tokio_util::sync::CancellationToken;
let token = CancellationToken::new();
let tool_ctx = ToolContext {
cancellation_token: token.clone(),
// ...
};
// Cancel from another task
tokio::spawn(async move {
tokio::time::sleep(Duration::from_secs(5)).await;
token.cancel();
});
let result = agent.run_text("Long task", &tool_ctx).await;
// Returns Err(LoopError::Cancelled) if cancelled§Parallel Tool Execution
When LoopConfig.parallel_tool_execution is true and the model returns
multiple tool calls in a single response, all calls execute concurrently.
When false (the default), tool calls execute sequentially in order.
let mut agent = AgentLoop::builder(provider, context)
.parallel_tool_execution(true)
.build();§Usage Limits
Set LoopConfig.usage_limits to enforce token budget constraints across the
entire loop. When cumulative usage exceeds any configured limit, the loop
returns LoopError::UsageLimitExceeded.
use neuron_loop::{AgentLoop, LoopConfig};
use neuron_types::UsageLimits;
let limits = UsageLimits::default()
.with_input_tokens_limit(50_000)
.with_output_tokens_limit(10_000)
.with_total_tokens_limit(60_000);
let mut agent = AgentLoop::builder(provider, context)
.usage_limits(limits)
.build();
// The loop will return Err(LoopError::UsageLimitExceeded(_))
// if any limit is exceeded during execution.Limits are checked after each LLM response and after tool execution. This is inspired by Pydantic AI’s usage limit pattern, ported to Rust with compile-time type safety.
§Architecture
AgentLoop depends on neuron-types (traits) and
neuron-tool (ToolRegistry).
RPITIT traits (Provider, ObservabilityHook, DurableContext) are
type-erased internally via BoxedHook and BoxedDurable wrappers for
dyn-compatibility — you never construct these directly; they’re created by
.hook() and .durability() on the builder. The ContextStrategy is used as
a generic parameter.
§Part of neuron
This crate is part of neuron, a composable building-blocks library for AI agents in Rust.
§License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.