loopctl
A trait-based framework for building agent loops with pluggable LLM clients, tools, and memory.
Overview
loopctl provides the core infrastructure for building LLM-based agent loops: a streaming
API client abstraction, tool registry, loop detection and convergence, fallback model chains,
cancellation, and a default loop engine (BareLoop). You bring your own LLM provider client
and tool implementations; the framework handles the rest.
Modules
| Module | Description |
|---|---|
api |
ApiClient trait for LLM provider communication (streaming + non-streaming) |
api::error |
API error types with retry classification |
cancel |
Cooperative cancellation via CancelSignal (AtomicBool + tokio::Notify) |
capabilities |
Capability traits (Observable, Detectable, Compactable, etc.) |
compact |
Context compaction: ContextCompactor trait, TruncatingCompactor, TokenSplitter |
config |
Session configuration (LoopConfig) |
detection |
Loop detection, convergence detection, DetectionManager |
engine |
BareLoop<C> — the default agent loop engine (stream → accumulate → dispatch tools → repeat) |
error |
Central LoopError enum for all framework operations |
fallback |
Circuit-breaker pattern for automatic API model fallback (FallbackManager) |
memory |
LoopMemory trait and entry types; memory::builtin provides InMemoryStore |
message |
Conversation types: Message, MessagePart, ToolContent, roles |
middleware |
Tool dispatch pipeline: timeouts, permissions, output limits, unknown-tool handling |
observer |
LoopObserver trait and ObserverHost for lifecycle event observation |
reflection |
Failure reflection and recovery strategies (Reflector, RecoveryStrategy) |
managers |
LoopManagers — the default infrastructure bundle |
stream |
Streaming event types, accumulator, stop reasons, usage tracking |
tool |
Tool trait, ToolRegistry, ToolSchema, ToolOutput, FnTool adapter |
hooks |
Bidirectional lifecycle control (allow/block/ask before tool use). Requires hooks feature. |
testing |
Mock API client, mock tools, and test fixture factories. Requires testing feature. |
Quick Start
Implement a Tool
use ;
use ;
use Pin;
use Future;
;
Run an Agent Loop
use BareLoop;
use Loop;
use RunConfig;
use ToolRegistry;
use SessionConfig;
use Arc;
// 1. Bring your own API client (implements ApiClient trait)
# ;
# use ApiClient;
#
let client = new;
// 2. Register tools
let mut registry = new;
// registry.register(EchoTool);
// 3. Configure
let config = default;
// 4. Run
let mut agent = new;
// let result = agent.run("Use the echo tool to say hello", &RunConfig::default()).await?;
// println!("Completed in {} turns", result.turn_count());
Use the Testing Module
[]
= { = "0.3", = ["testing"] }
use ;
use BareLoop;
use Loop;
use ToolRegistry;
use Arc;
let mut client = new;
client = client.with_text_response;
let mut registry = new;
registry.register;
let agent = new;
// let result = agent.run("test input").await?;
Feature Flags
| Feature | Default | Depends on | Description |
|---|---|---|---|
derive |
No | — | Re-exports loopctl-derive: #[derive(Tool)] generates the Tool impl (name, description, schema, dispatch) from a Deserialize input struct |
azure |
No | providers, openai |
Azure OpenAI via the v1 API — an OpenAiClient profile (AZURE_OPENAI_API_KEY, resource-named endpoint) |
moonshot |
No | providers, openai |
Moonshot AI (Kimi) — an OpenAiClient profile (MOONSHOT_API_KEY, api.moonshot.ai) |
bedrock |
No | providers, streaming, anthropic, hmac, sha2, hex |
AWS Bedrock (BedrockClient) — SigV4 auth, Anthropic-native + Converse paths, binary event-stream decoding |
hooks |
No | — | Bidirectional lifecycle hooks (allow/block/ask before tool use, compaction) |
testing |
No | — | Mock clients, tools, and test fixtures |
tool_health |
No | — | Per-tool health monitoring, circuit breakers, and self-healing routing |
tool_shield |
No | tool_health |
ToolSafetyShield risk evaluation (UnixShield reference patterns) + opt-in SafetyShieldMiddleware enforcement of Block decisions |
streaming |
No | async-stream |
Streaming engine path: StreamHandler (retry, timeout, fallback), per-delta observer callbacks (on_text_delta, on_thinking_delta), text_streamer. Without it the engine drives each turn via ApiClient::create_message. |
providers |
No | reqwest, httpdate, bytes |
Base HTTP provider support; enables the provider module |
openai |
No | providers, streaming |
OpenAI-compatible API client (provider::openai) |
anthropic |
No | providers, streaming |
Anthropic Claude API client (provider::anthropic) |
ollama |
No | providers, openai |
Ollama local model client (OpenAI-compatible) |
deepseek |
No | providers, openai |
DeepSeek API client (OpenAI-compatible) |
grok |
No | providers, openai |
Grok (xAI) API client (OpenAI-compatible) |
xai |
No | grok |
Alias for grok (xAI API client) |
gemini |
No | providers, streaming |
Google Gemini API client (provider::gemini) |
zai |
No | providers, anthropic |
Z.AI API client (Anthropic-compatible) |
grammar |
No | providers |
Tool-call grammar providers for grammar-aware samplers (vLLM guided_json); enables the Grammar mode of ToolConstraint |
schema_validation |
No | — | JSON Schema validation of Correction::modified_input in LlmReflector (pulls jsonschema); when off, validation is skipped |
redaction |
No | regex |
RedactingMiddleware — scrub secrets (bearer headers, AWS keys, PEM blocks, PATs, high-entropy tokens) from tool output as [REDACTED:<kind>] |
mcp |
No | rmcp, reqwest, async-stream, jsonschema |
MCP client + server adapters — adapt foreign MCP servers' tools as Tool impls (McpToolProvider), or serve a ToolRegistry over MCP/stdio to any MCP client (McpServerAdapter; served schemas are validated with jsonschema, external refs refused) |
Streaming vs non-streaming
The engine selects a turn mode at runtime via
TurnMode
(NonStreaming or Streaming), set with
set_turn_mode.
The two modes are independent of whether the streaming feature is compiled
in, though the feature gates what Streaming can do:
-
TurnMode::NonStreamingdrives each turn via [ApiClient::create_message] — a single request/response with no per-delta callbacks. The full assistant text surfaces throughon_response. Always available, even underdefault = []. -
TurnMode::Streamingroutes turns throughStreamHandlerwith retry, timeout, rate-limit detection, andon_text_delta/on_thinking_deltacallbacks for real-time token display. Requires thestreamingfeature (implied by every HTTP provider).
The constructor default is Streaming when the streaming feature is enabled
and NonStreaming otherwise — but a constructed loop can switch to either mode
at runtime regardless of the default.
Architecture
At the center is BareLoop, the default agent loop. Each turn it requests a
response from an ApiClient (your LLM provider) — via the streaming path
(StreamHandler) under TurnMode::Streaming, or via create_message under
TurnMode::NonStreaming — then dispatches any requested tool calls through a
ToolRegistry. Results are fed back into the conversation and the cycle
repeats until the model ends its turn or a configured limit is reached.
Two cross-cutting concerns run alongside the main loop:
- Detection & Fallback — convergence detection, loop detection, and automatic model/API fallback when requests fail.
- ToolRegistry — holds your registered tools and routes tool calls to them.
Development
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.