tiny-agent 0.1.0

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

use super::{Agent, AgentRunTime, FailureKind, ToolRunState, storage_failure};
use crate::{
    error::AgentError,
    shared::{ContentBlock, ToolCall},
    state::AgentState,
    trajectory::TrajectoryEventKind,
};
use futures_util::future::join_all;
use tokio_util::sync::CancellationToken;

impl AgentRunTime {
    /// 执行一批工具调用。
    ///
    /// **不变量**:assistant 消息里的每个 `tool_use` 都必须被一条 `tool_result` 回应 ——
    /// 成功、失败、被护栏拒绝、被取消,都照样落一条结果(失败/取消写 `is_error: true`)。
    /// 任何退出路径都不允许留下"孤儿 tool_use",否则下一轮把 transcript 投影成
    /// OpenAI 格式时会因 tool_calls 缺少对应 tool 消息而被 API 直接 400。
    pub(crate) async fn execute_pending_tools(
        &self,
        ctx: &AgentState,
        mut tool_run: ToolRunState,
        cancellation_token: CancellationToken,
    ) -> Result<Agent, AgentError> {
        if tool_run.cursor >= tool_run.calls.len() {
            return Ok(Agent::Ready(ctx.clone()));
        }

        let sandbox = match self.open_sandbox(ctx, cancellation_token.clone()).await {
            Ok(Some(sandbox)) => sandbox,
            Ok(None) => {
                if let Err(e) = self
                    .answer_unanswered(
                        &ctx.session_id,
                        &tool_run.calls[tool_run.cursor..],
                        "cancelled by user",
                    )
                    .await
                {
                    return Ok(storage_failure(ctx.clone(), e));
                }
                return self.interrupt(ctx).await;
            }
            Err(message) => {
                if let Err(e) = self
                    .answer_unanswered(
                        &ctx.session_id,
                        &tool_run.calls[tool_run.cursor..],
                        "tool not executed: sandbox unavailable",
                    )
                    .await
                {
                    return Ok(storage_failure(ctx.clone(), e));
                }
                return Ok(self.fail_with(ctx.clone(), FailureKind::SandboxOpen, message));
            }
        };

        // 从 cursor 往后,按"连续只读"切段:一段只读工具并发执行,非只读工具单独成段串行。
        while tool_run.cursor < tool_run.calls.len() {
            let start = tool_run.cursor;
            let end = self.segment_end(&tool_run.calls, start);

            let outcomes =
                join_all(tool_run.calls[start..end].iter().map(|call| {
                    self.run_one(ctx, sandbox.clone(), call, cancellation_token.clone())
                }))
                .await;

            for (offset, outcome) in outcomes.into_iter().enumerate() {
                let index = start + offset;
                let tool_call = &tool_run.calls[index];
                match outcome {
                    run_one::SingleOutcome::MiddlewareErr {
                        message,
                        tool_executed,
                    } => {
                        if tool_executed {
                            if let Err(e) = self
                                .append_tool_result(
                                    &ctx.session_id,
                                    ContentBlock::ToolResult {
                                        tool_use_id: tool_call.call_id.clone(),
                                        content: vec![ContentBlock::Text {
                                            text: "tool executed but result unavailable: after_tool middleware error"
                                                .to_string(),
                                        }],
                                        is_error: true,
                                    },
                                )
                                .await
                            {
                                return Ok(storage_failure(ctx.clone(), e));
                            }
                            if let Err(e) = self
                                .answer_unanswered(
                                    &ctx.session_id,
                                    &tool_run.calls[index + 1..],
                                    "skipped due to middleware error",
                                )
                                .await
                            {
                                return Ok(storage_failure(ctx.clone(), e));
                            }
                        } else if let Err(e) = self
                            .answer_unanswered(
                                &ctx.session_id,
                                &tool_run.calls[index..],
                                "tool not executed: before_tool middleware error",
                            )
                            .await
                        {
                            return Ok(storage_failure(ctx.clone(), e));
                        }
                        self.persist_sandbox(&ctx.session_id, &sandbox).await;
                        return Ok(self.fail_with(ctx.clone(), FailureKind::Middleware, message));
                    }
                    run_one::SingleOutcome::ToolErr(message) => {
                        if let Err(e) = self
                            .append_tool_result(
                                &ctx.session_id,
                                ContentBlock::ToolResult {
                                    tool_use_id: tool_call.call_id.clone(),
                                    content: vec![ContentBlock::Text {
                                        text: message.clone(),
                                    }],
                                    is_error: true,
                                },
                            )
                            .await
                        {
                            return Ok(storage_failure(ctx.clone(), e));
                        }
                        if let Err(e) = self
                            .answer_unanswered(
                                &ctx.session_id,
                                &tool_run.calls[index + 1..],
                                "skipped due to previous failure",
                            )
                            .await
                        {
                            return Ok(storage_failure(ctx.clone(), e));
                        }
                        self.persist_sandbox(&ctx.session_id, &sandbox).await;
                        return Ok(self.fail_tool(ctx.clone(), tool_call.clone(), message));
                    }
                    run_one::SingleOutcome::Cancelled => {
                        if let Err(e) = self
                            .answer_unanswered(
                                &ctx.session_id,
                                &tool_run.calls[index..],
                                "cancelled by user",
                            )
                            .await
                        {
                            return Ok(storage_failure(ctx.clone(), e));
                        }
                        self.persist_sandbox(&ctx.session_id, &sandbox).await;
                        return self.interrupt(ctx).await;
                    }
                    run_one::SingleOutcome::Done(result) => {
                        if let Err(e) = self.append_tool_result(&ctx.session_id, result).await {
                            return Ok(storage_failure(ctx.clone(), e));
                        }
                        tool_run.cursor = index + 1;
                    }
                    run_one::SingleOutcome::NeedsUserInteraction {
                        result,
                        interaction,
                    } => {
                        if let Err(e) = self.append_tool_result(&ctx.session_id, result).await {
                            return Ok(storage_failure(ctx.clone(), e));
                        }
                        tool_run.cursor = index + 1;
                        if let Err(e) = self
                            .answer_unanswered(
                                &ctx.session_id,
                                &tool_run.calls[tool_run.cursor..],
                                "skipped pending user input",
                            )
                            .await
                        {
                            return Ok(storage_failure(ctx.clone(), e));
                        }
                        self.persist_sandbox(&ctx.session_id, &sandbox).await;
                        let agent = Agent::WaitingForUser(ctx.clone(), interaction.clone());
                        if let Err(e) = self
                            .checkpoint_storage
                            .save_checkpoint(&ctx.session_id, &agent)
                            .await
                        {
                            return Ok(storage_failure(ctx.clone(), e));
                        }
                        self.emit(
                            &ctx.session_id,
                            TrajectoryEventKind::WaitingForUser { interaction },
                        )
                        .await;
                        return Ok(agent);
                    }
                }
            }
        }

        tokio::select! {
            _ = cancellation_token.cancelled() => {
                self.persist_sandbox(&ctx.session_id, &sandbox).await;
                self.interrupt(ctx).await
            }
            save_result = sandbox.save() => {
                match save_result {
                    Ok(()) => {
                        let mut next_ctx = ctx.clone();
                        next_ctx.consecutive_fail_count = 0;
                        Ok(Agent::Ready(next_ctx))
                    }
                    Err(e) => {
                        Ok(self.fail_with(ctx.clone(), FailureKind::SandboxSave, e.to_string()))
                    }
                }
            }
        }
    }

    /// 求出从 `start` 起的并发段右边界(开区间)。
    fn segment_end(&self, calls: &[ToolCall], start: usize) -> usize {
        if !self.tools.is_read_only(&calls[start].tool_name) {
            return start + 1;
        }
        let mut end = start;
        while end < calls.len() && self.tools.is_read_only(&calls[end].tool_name) {
            end += 1;
        }
        end
    }
}