aether-sessions 0.1.0

Persisted session logs and transcript reconstruction for Aether
Documentation
use aether_core::events::{AgentEvent, ContextEvent, LlmCallOutcome, MessageEvent, ModelEvent, ToolEvent, TurnEvent};
use llm::ContentBlock;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SessionMeta {
    pub session_id: String,
    pub cwd: PathBuf,
    pub model: String,
    #[serde(default)]
    pub selected_mode: Option<String>,
    pub created_at: String,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum UserEvent {
    Message { content: Vec<ContentBlock> },
    ClearContext,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum SessionControlEvent {
    AgentSwitched { from: Option<String>, to: Option<String> },
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "data", rename_all = "camelCase")]
#[allow(clippy::large_enum_variant)]
pub enum SessionEvent {
    User(UserEvent),
    Agent(AgentEvent),
    Control(SessionControlEvent),
}

impl SessionEvent {
    pub fn content(&self) -> Option<String> {
        self.user_content().or_else(|| match self {
            Self::Agent(event) => event.content(),
            Self::User(_) | Self::Control(_) => None,
        })
    }

    pub fn user_content(&self) -> Option<String> {
        match self {
            Self::User(UserEvent::Message { content }) => {
                let text = ContentBlock::join_text(content);
                (!text.is_empty()).then_some(text)
            }
            Self::User(UserEvent::ClearContext) | Self::Agent(_) | Self::Control(_) => None,
        }
    }

    pub fn is_persisted(&self) -> bool {
        match self {
            Self::User(_) | Self::Control(_) => true,
            Self::Agent(event) => match event {
                AgentEvent::Message(
                    MessageEvent::Text { is_complete, .. } | MessageEvent::Thought { is_complete, .. },
                ) => *is_complete,
                AgentEvent::Tool(
                    ToolEvent::Call { .. }
                    | ToolEvent::Result { .. }
                    | ToolEvent::Error { .. }
                    | ToolEvent::TaskCreated { .. }
                    | ToolEvent::TaskCompleted { .. }
                    | ToolEvent::TaskFailed { .. }
                    | ToolEvent::TaskCancelled { .. },
                )
                | AgentEvent::Turn(
                    TurnEvent::RetryScheduled { .. }
                    | TurnEvent::AutoContinue { .. }
                    | TurnEvent::Ended { .. }
                    | TurnEvent::LlmCallEnded { outcome: LlmCallOutcome::Failed { .. }, .. },
                )
                | AgentEvent::Context(
                    ContextEvent::CompactionStarted { .. }
                    | ContextEvent::CompactionEnded { .. }
                    | ContextEvent::CompactionResult { .. }
                    | ContextEvent::UsageUpdated { .. }
                    | ContextEvent::Cleared,
                )
                | AgentEvent::Model(ModelEvent::Switched { .. }) => true,
                AgentEvent::Tool(
                    ToolEvent::CallUpdate { .. }
                    | ToolEvent::ExecutionStarted { .. }
                    | ToolEvent::Progress { .. }
                    | ToolEvent::TaskStatus { .. }
                    | ToolEvent::DefinitionsUpdated { .. },
                )
                | AgentEvent::Turn(
                    TurnEvent::Started { .. }
                    | TurnEvent::LlmCallStarted { .. }
                    | TurnEvent::LlmCallEnded {
                        outcome: LlmCallOutcome::Completed { .. } | LlmCallOutcome::Cancelled,
                        ..
                    },
                ) => false,
            },
        }
    }
}

pub fn last_agent_from_events(initial: Option<String>, events: &[SessionEvent]) -> Option<String> {
    events
        .iter()
        .rev()
        .find_map(|event| match event {
            SessionEvent::Control(SessionControlEvent::AgentSwitched { to, .. }) => Some(to.clone()),
            _ => None,
        })
        .unwrap_or(initial)
}