tiny-agent 0.3.0

一个小而完整的 Rust LLM Agent 运行时:可中断、可恢复、可观测、可插拔的 agent loop / A small but complete LLM agent runtime in Rust — an interruptible, resumable, observable, pluggable agent loop.
Documentation
mod host;

pub use async_trait::async_trait;
pub use host::HostSandbox;
use std::{fmt, sync::Arc};

/// 命令输出的来源流。
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutStream {
    Stdout,
    Stderr,
}

/// 实时输出回调:命令执行期间每产出一块就被调用一次(同步,对齐 bashkit 的 `OutputCallback`)。
/// 块不保证按行对齐;把所有块拼起来等于最终的 stdout/stderr。
pub type OutputCallback = Arc<dyn Fn(OutStream, &str) + Send + Sync>;

/// 一次命令执行的结果。
#[derive(Debug, Clone)]
pub struct ExecOutput {
    pub stdout: String,
    pub stderr: String,
    pub exit_code: Option<i32>,
}

impl ExecOutput {
    /// 退出码为 0 视为成功。
    pub fn is_success(&self) -> bool {
        self.exit_code == Some(0)
    }
}

/// sandbox 操作可能产生的错误。
#[derive(Debug)]
pub enum SandboxError {
    /// 底层 IO / 进程错误。
    Io(String),
    /// 该 sandbox 不支持此能力(例如不支持快照)。
    Unsupported,
    /// 其他执行错误。
    Other(String),
}

impl fmt::Display for SandboxError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SandboxError::Io(e) => write!(f, "sandbox io error: {e}"),
            SandboxError::Unsupported => {
                write!(f, "operation not supported by this sandbox")
            }
            SandboxError::Other(e) => write!(f, "sandbox error: {e}"),
        }
    }
}

impl std::error::Error for SandboxError {}

impl From<std::io::Error> for SandboxError {
    fn from(e: std::io::Error) -> Self {
        SandboxError::Io(e.to_string())
    }
}

/// agent 工具的执行环境抽象。
///
/// 所有会触达"外部世界"(进程、文件系统)的工具都应通过 [`Sandbox::execute`]
/// 走这一层,这样替换 sandbox 实现(真机 / bashkit / 容器)时工具代码无需改动。
///
/// agent 的执行环境抽象。
///
/// 一个 `Sandbox` 实例绑定一个会话:[`execute`](Sandbox::execute) 负责执行,
/// [`save`](Sandbox::save) 持久化自身状态,[`fork`](Sandbox::fork) 把自身复制给另一个会话。
///
/// **框架完全不规定状态怎么序列化**:可能是一坨字节、一个容器 image id、一个远程
/// 快照引用……都藏在实现内部,trait 不暴露任何序列化格式。不需要持久化的后端
/// (如 [`HostSandbox`])用默认实现即可。
///
/// [`Sandbox::open`] 是按 session 打开 / 恢复 / 初始化执行环境的入口。
/// 对无会话状态的实现(如 [`HostSandbox`]),可直接返回自己;对需要快照 / 挂载 /
/// bootstrap 的实现,则在这里按 `session_id` 恢复出真正的 session-bound sandbox。
#[async_trait]
pub trait Sandbox: Send + Sync {
    /// 打开或恢复某个 session 对应的 sandbox。实现应保持幂等:同一 session 多次 open
    /// 应恢复到同一份状态,并且 bootstrap 逻辑不应重复破坏环境。
    async fn open(self: Arc<Self>, session_id: &str) -> Result<Arc<dyn Sandbox>, SandboxError>;

    /// 执行一条命令并返回结果。
    ///
    /// 文件读写等操作由各工具自行拼成命令(如 `cat`、`printf`),本层只负责执行。
    ///
    /// `on_output` 可选:传入则在执行期间每产出一块输出就回调一次(用于实时刷屏 / 增量收集);
    /// 不关心实时输出的调用方传 `None`。无论是否流式,完整结果都在末尾通过返回值给出。
    async fn execute(
        &self,
        command: &str,
        on_output: Option<OutputCallback>,
    ) -> Result<ExecOutput, SandboxError>;

    /// 持久化自身当前状态。实例自己知道 session_id 和存到哪里,故无参。
    /// 默认不做事(真机文件系统本来就持久)。
    async fn save(&self) -> Result<(), SandboxError> {
        Ok(())
    }

    /// 把自身当前状态复制给另一个会话 `to_session`。默认不支持。
    async fn fork(&self, _to_session: &str) -> Result<(), SandboxError> {
        Err(SandboxError::Unsupported)
    }
}