agentkit-loop 0.7.0

Runtime-agnostic loop driver, interrupts, and model session traits for agentkit.
Documentation

agentkit-loop

Runtime-agnostic agent loop orchestration for sessions, turns, tools, and interrupts.

This crate provides:

  • Model adapter traits -- ModelAdapter, ModelSession, and ModelTurn abstract away the model provider so you can swap between OpenRouter, Anthropic, or a local LLM without changing loop logic.
  • Agent builder and LoopDriver -- configure tools, permissions, observers, and compaction, then drive the loop step-by-step.
  • Interrupt handling -- the loop pauses and yields LoopStep::Interrupt on blocking events (tool approval) and cooperative yields (AwaitingInput at end-of-turn, AfterToolResult between tool rounds). The host either resolves the interrupt or just calls next() again depending on whether LoopInterrupt::is_blocking() is true.
  • Observer hooks -- attach LoopObserver implementations to receive streaming AgentEvents (deltas, tool calls, usage, warnings, lifecycle events).
  • Transcript compaction -- optionally compact the transcript when it grows too large, via the agentkit-compaction integration.

Use it as the central coordinator between model providers, tool execution, and application UI or control flow.

Quick start

use agentkit_core::{Item, ItemKind};
use agentkit_loop::{
    Agent, LoopInterrupt, LoopStep, PromptCacheRequest, PromptCacheRetention, SessionConfig,
};
use agentkit_provider_openrouter::{OpenRouterAdapter, OpenRouterConfig};

# #[tokio::main]
# async fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Create a model adapter
let adapter = OpenRouterAdapter::new(
    OpenRouterConfig::new("sk-or-v1-...", "openrouter/auto"),
)?;

// 2. Build an agent. Preload the system prompt and first user turn so the
//    very first `next()` call dispatches the model directly.
let agent = Agent::builder()
    .model(adapter)
    .transcript(vec![Item::text(ItemKind::System, "You are a helpful assistant.")])
    .input(vec![Item::text(ItemKind::User, "Hello, agent!")])
    .build()?;

// 3. Start a session to get a LoopDriver
let mut driver = agent
    .start(
        SessionConfig::new("demo").with_cache(
            PromptCacheRequest::automatic().with_retention(PromptCacheRetention::Short),
        ),
    )
    .await?;

// 4. Drive the loop. Subsequent user turns are supplied via the
//    `InputRequest::submit` handle yielded by `LoopInterrupt::AwaitingInput`.
loop {
    match driver.next().await? {
        LoopStep::Finished(result) => {
            println!("Turn finished ({:?}): {:?}", result.finish_reason, result.items);
            break;
        }
        LoopStep::Interrupt(LoopInterrupt::AwaitingInput(_)) => {
            // No more input to feed in this example; stop here.
            break;
        }
        LoopStep::Interrupt(interrupt) => {
            // See "Handling interrupts" below for how to resolve each variant.
            println!("Loop paused: {interrupt:?}");
            break;
        }
    }
}
# Ok(())
# }

Adding tools and observers

AgentBuilder::add_tool_source accepts any ToolSource. A ToolRegistry implements ToolSource directly, so you can hand it in by value; call the method again to federate additional sources (MCP catalogs, plugin loaders, etc.).

use agentkit_loop::{Agent, AgentEvent, LoopObserver};
use agentkit_tools_core::ToolRegistry;

struct PrintObserver;

impl LoopObserver for PrintObserver {
    fn handle_event(&self, event: AgentEvent) {
        println!("[event] {event:?}");
    }
}

# fn example<M: agentkit_loop::ModelAdapter>(adapter: M, registry: ToolRegistry) -> Result<(), agentkit_loop::LoopError> {
let agent = Agent::builder()
    .model(adapter)
    .add_tool_source(registry)
    .observer(PrintObserver)
    .build()?;
# Ok(())
# }

Handling interrupts

When a tool call requires approval the loop yields a blocking interrupt; AwaitingInput and AfterToolResult are cooperative (use LoopInterrupt::is_blocking to tell them apart). Resolve any pending approval and call next() again to resume:

use agentkit_core::{Item, ItemKind};
use agentkit_loop::{LoopInterrupt, LoopStep};

# async fn handle<S: agentkit_loop::ModelSession>(
#     driver: &mut agentkit_loop::LoopDriver<S>,
# ) -> Result<(), agentkit_loop::LoopError> {
loop {
    match driver.next().await? {
        LoopStep::Finished(result) => {
            println!("Done: {:?}", result.finish_reason);
            break;
        }
        LoopStep::Interrupt(LoopInterrupt::ApprovalRequest(pending)) => {
            println!("Approve {}? (auto-approving)", pending.summary);
            pending.approve(driver)?;
        }
        LoopStep::Interrupt(LoopInterrupt::AwaitingInput(request)) => {
            // Hand the next user turn to the driver, or break to stop.
            request.submit(driver, vec![Item::text(ItemKind::User, "continue")])?;
        }
        // Cooperative yield between tool rounds. Interactive hosts may use
        // `info.submit(driver, items)` to interject a user message before
        // the next model call; non-interactive callers just loop.
        LoopStep::Interrupt(LoopInterrupt::AfterToolResult(_info)) => continue,
    }
}
# Ok(())
# }