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
//! Trajectory:一次 agent 运行过程中产生的**只读事件流**。
//!
//! 它回答的是"刚刚发生了什么",区别于 `Transcript`(喂给模型的消息历史)和
//! `Checkpoint`(可恢复的状态快照)。典型消费者:
//! - `examples/websocket`:run 在一个 task 里跑,事件经 channel 实时推给客户端;
//! - 日志 / 回放 / 评测。
//!
//! 运行时在关键节点同步 `emit`,因此 sink 收到事件的顺序天然等于发生顺序;
//! `at`(毫秒时间戳)只是附带的排序/展示元数据。

use crate::shared::{ContentBlock, UserInteraction};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};

/// 单条轨迹事件:会话 id + 发生时间 + 具体事件。
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TrajectoryEvent {
    pub session_id: String,
    pub at: u64,
    #[serde(flatten)]
    pub kind: TrajectoryEventKind,
}

impl TrajectoryEvent {
    pub fn new(session_id: &str, kind: TrajectoryEventKind) -> 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,
        }
    }
}

/// 事件种类。序列化为 `{ "type": "user_message", ... }`,方便前端按 tag 分发。
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TrajectoryEventKind {
    /// 用户向会话提交了一条消息(`submit` 入口)。轨迹的对话起点。
    UserMessage { text: String },
    /// 一次推理开始(provider 即将被调用)。
    /// `request` 为 composer + middleware 之后,由 provider 投影出的请求快照。
    ReasoningStarted { request: Value },
    /// 一条 assistant 消息已成型并落库(含 thinking / text / tool_use 块)。
    AssistantMessage { blocks: Vec<ContentBlock> },
    /// 某个工具开始执行。
    ToolStarted {
        call_id: String,
        name: String,
        arguments: Value,
    },
    /// 某个工具执行结束。
    ToolCompleted {
        call_id: String,
        name: String,
        output: Vec<ContentBlock>,
        is_error: bool,
    },
    /// 运行被取消并已存档 checkpoint。
    Interrupted,
    /// 工具已返回合法结果,但该结果要求用户交互后再继续会话。
    WaitingForUser { interaction: UserInteraction },
    /// 进入失败(可能仍会重试,见 max_consecutive_fail_count)。
    Failed { kind: String, message: String },
    /// 推理未再产生工具调用,运行成功结束。
    Succeeded { final_resp: Option<String> },
}

impl TrajectoryEventKind {
    pub fn kind_type(&self) -> &'static str {
        match self {
            Self::UserMessage { .. } => "user_message",
            Self::ReasoningStarted { .. } => "reasoning_started",
            Self::AssistantMessage { .. } => "assistant_message",
            Self::ToolStarted { .. } => "tool_started",
            Self::ToolCompleted { .. } => "tool_completed",
            Self::Interrupted => "interrupted",
            Self::WaitingForUser { .. } => "waiting_for_user",
            Self::Failed { .. } => "failed",
            Self::Succeeded { .. } => "succeeded",
        }
    }
}

/// 轨迹事件的下游。实现者负责把事件送到日志 / channel / websocket 等。
///
/// `emit` 不返回错误:可观测性不应阻断主流程,投递失败(如 channel 关闭)由实现内部吞掉。
#[async_trait]
pub trait TrajectorySink: Send + Sync {
    async fn emit(&self, event: TrajectoryEvent);
}

/// 默认 sink:丢弃所有事件。未配置 trajectory 时使用。
pub struct NoopSink;

#[async_trait]
impl TrajectorySink for NoopSink {
    async fn emit(&self, _event: TrajectoryEvent) {}
}

/// 基于无界 mpsc 的 sink,为 websocket 场景准备:
/// 一个 task 跑 `AgentRunTime::run`,另一个 task 从 `receiver` 拉事件推给客户端。
pub struct ChannelSink {
    tx: tokio::sync::mpsc::UnboundedSender<TrajectoryEvent>,
}

impl ChannelSink {
    /// 返回 sink 与配对的 receiver。run 结束、sink 被 drop 后,receiver 收到 `None`。
    pub fn new() -> (
        Arc<Self>,
        tokio::sync::mpsc::UnboundedReceiver<TrajectoryEvent>,
    ) {
        let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
        (Arc::new(Self { tx }), rx)
    }
}

#[async_trait]
impl TrajectorySink for ChannelSink {
    async fn emit(&self, event: TrajectoryEvent) {
        // 客户端已断开 → receiver 被 drop,这里静默丢弃。
        let _ = self.tx.send(event);
    }
}