loopctl 0.2.0

A trait-based framework for building agent loops with pluggable LLM clients, tools, and memory
Documentation
//! Interactive REPL CLI — read user input, run a turn, repeat.
//!
//! Demonstrates a minimal read-eval-print loop using [`BareLoop`].
//! Each line of stdin becomes a new user message. Type `quit` or
//! `Ctrl-D` to exit.
//!
//! ```sh
//! cargo run --example repl-cli --features testing
//! ```

#![allow(clippy::unwrap_used)]

use std::io::{self, BufRead, Write};
use std::sync::Arc;

use loopctl::config::SessionConfig;
use loopctl::engine::BareLoop;
use loopctl::engine::RunConfig;
use loopctl::engine::core::Loop;
use loopctl::testing::MockApiClient;
use loopctl::tool::ToolRegistry;

#[tokio::main]
async fn main() {
    let stdin = io::stdin();
    let mut stdout = io::stdout();

    loop {
        // Read.
        write!(&mut stdout, "> ").unwrap();
        stdout.flush().unwrap();

        let mut input = String::new();
        if stdin.lock().read_line(&mut input).unwrap_or(0) == 0 {
            break; // EOF (Ctrl-D)
        }
        let input = input.trim();
        if input.is_empty() {
            continue;
        }
        if input == "quit" || input == "exit" {
            break;
        }

        // Eval: build a fresh loop per message with a canned response.
        let client =
            MockApiClient::new("repl-model").with_text_response(&format!("You said: {input}"));

        let mut agent = BareLoop::new(
            Arc::new(client),
            ToolRegistry::new(),
            SessionConfig::default(),
        );

        // A fresh agent per turn means a fresh CancelSignal per turn: Ctrl-C
        // interrupts the current turn only, and the next prompt gets a clean
        // signal. The listener is aborted when the turn ends so it doesn't
        // outlive the agent.
        let cancel_signal = agent.cancel_signal();
        let listener = tokio::spawn(async move {
            if tokio::signal::ctrl_c().await.is_ok() {
                cancel_signal.cancel();
            }
        });

        match agent.run(input, &RunConfig::default()).await {
            Ok(result) => {
                listener.abort();
                let output = result.output.unwrap_or_default();
                writeln!(&mut stdout, "{output}\n").unwrap();
            }
            Err(e) => {
                listener.abort();
                writeln!(&mut stdout, "Error: {e}\n").unwrap();
            }
        }
    }

    writeln!(&mut stdout, "Goodbye!").unwrap();
}