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
//! Messages:框架对外的**实时消息通道**。
//!
//! 它是推给**前端实时渲染**的内容流,承载流式增量(`Partial*`)与成型消息。
//! 观测/监控则走标准 `tracing` span/event,由宿主程序决定是否导出到 OpenTelemetry。
//!
//! 故意做成一个**具体 struct**([`MessageChannel`])而非 trait —— 内部就是一个 mpsc,
//! 使用方拿 `receiver` 消费即可,不需要实现任何 sink。未配置时用 [`MessageChannel::disconnected`],
//! `send` 静默丢弃。

use crate::shared::ContentBlock;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::sync::mpsc;

/// 一条对前端的消息:会话 id + 发生时间 + 具体内容。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentMessage {
    pub session_id: String,
    pub at: u64,
    #[serde(flatten)]
    pub kind: MessageKind,
}

impl AgentMessage {
    pub fn new(session_id: &str, kind: MessageKind) -> Self {
        let at = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_millis() as u64)
            .unwrap_or(0);
        Self {
            session_id: session_id.to_string(),
            at,
            kind,
        }
    }
}

/// 消息内容。`Partial*` 是流式增量,与之配对的成型变体在内容完整时发出。
///
/// 序列化为 `{ "type": "tool_call", ... }`,方便前端按 tag 分发。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum MessageKind {
    /// 用户提交的一条消息。
    UserMessage { text: String },
    /// 一次推理开始(provider 即将被调用)。
    ReasoningStarted,
    /// assistant 文本的流式增量。(预留:需 provider 支持 token 流式后才发出。)
    PartialAssistantMessage { delta: String },
    /// 一条成型的 assistant 消息(含 thinking / text / tool_use 块)。
    AssistantMessage { blocks: Vec<ContentBlock> },
    /// 工具调用参数的流式拼装增量。(预留:同上。)
    PartialToolCall {
        call_id: String,
        name: String,
        delta: String,
    },
    /// 模型决定调用某工具(参数已成型)。
    ToolCall {
        call_id: String,
        name: String,
        arguments: Value,
    },
    /// 工具执行期间的实时原始输出(如 bash 边跑边吐)。**纯前端展示用**:
    /// 不进 transcript、模型也看不到——它不是 `ToolResponse` 的一部分。
    /// 模型看到的真正结果始终是下面那条完整的 `ToolResponse`(后台工具任务则是 `{id, running}` 信封)。
    /// 不带 call_id —— 归属"当前正在执行的工具"(非只读工具独占串行,任一时刻至多一个)。
    ToolOutput { chunk: String },
    /// 工具执行结束的**完整**结果(模型看到的、也是会落进 transcript 的那条消息)。
    /// 后台工具任务超时让步时,这里就是 `{id, status:"running", …}` 那个异步信封。
    ToolResponse {
        call_id: String,
        output: Vec<ContentBlock>,
        is_error: bool,
    },
}

/// 实时消息通道。`Clone` 即克隆底层 sender,可多生产者并发推送。
#[derive(Clone)]
pub struct MessageChannel {
    tx: mpsc::UnboundedSender<AgentMessage>,
}

impl MessageChannel {
    /// 创建通道,返回 `(channel, receiver)`。所有 `channel` 副本 drop 后,`receiver` 收到 `None`。
    pub fn new() -> (Self, mpsc::UnboundedReceiver<AgentMessage>) {
        let (tx, rx) = mpsc::unbounded_channel();
        (Self { tx }, rx)
    }

    /// 一个没有消费者的通道:`send` 全部静默丢弃。未配置 messages 时的默认值。
    pub fn disconnected() -> Self {
        let (tx, _rx) = mpsc::unbounded_channel();
        Self { tx }
    }

    /// 投递一条消息。fire-and-forget:消费者已断开则静默丢弃,不阻断主流程。
    pub fn send(&self, session_id: &str, kind: MessageKind) {
        let _ = self.tx.send(AgentMessage::new(session_id, kind));
    }
}

impl Default for MessageChannel {
    fn default() -> Self {
        Self::disconnected()
    }
}