regy 0.1.0

Private-by-default desktop agent for the Regy web interface
use std::path::PathBuf;

use serde::{Deserialize, Serialize};
use serde_json::Value;

use crate::domain::pi_rpc::{PiSessionStats, ThinkingLevel};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ChatStatus {
    Starting,
    Idle,
    Streaming,
    Compacting,
    Retrying,
    Settled,
    Exited,
    Error,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum ChatRole {
    User,
    Assistant,
    Tool,
    System,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(
    tag = "type",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub(crate) enum ChatContent {
    Text {
        text: String,
    },
    Thinking {
        text: String,
    },
    ToolCall {
        id: String,
        name: String,
        arguments: Value,
    },
    ToolResult {
        tool_call_id: String,
        content: Value,
        is_error: bool,
    },
    Unknown {
        value: Value,
    },
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChatMessage {
    pub(crate) id: String,
    pub(crate) role: ChatRole,
    pub(crate) content: Vec<ChatContent>,
    pub(crate) timestamp: Option<i64>,
    pub(crate) provider: Option<String>,
    pub(crate) model: Option<String>,
    pub(crate) usage: Option<Value>,
    pub(crate) stop_reason: Option<ChatStopReason>,
    pub(crate) error_message: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) enum ChatStopReason {
    Stop,
    Length,
    ToolUse,
    Error,
    Aborted,
    Unknown(String),
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct ChatModel {
    pub(crate) provider: String,
    pub(crate) id: String,
    pub(crate) value: Value,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChatSessionState {
    pub(crate) session_id: String,
    pub(crate) pi_session_id: String,
    pub(crate) session_file: Option<String>,
    pub(crate) workspace: PathBuf,
    pub(crate) model: Option<ChatModel>,
    pub(crate) thinking_level: ThinkingLevel,
    pub(crate) status: ChatStatus,
    pub(crate) is_streaming: bool,
    pub(crate) is_compacting: bool,
    pub(crate) name: Option<String>,
    pub(crate) message_count: u64,
    pub(crate) pending_message_count: u64,
    pub(crate) stats: Option<Box<PiSessionStats>>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChatExtensionUiRecord {
    pub(crate) id: String,
    pub(crate) method: String,
    pub(crate) payload: Value,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChatExtensionKeyedRecord {
    pub(crate) key: String,
    pub(crate) id: String,
    pub(crate) method: String,
    pub(crate) payload: Value,
}

#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChatExtensionState {
    pub(crate) pending_requests: Vec<ChatExtensionUiRecord>,
    pub(crate) notifications: Vec<ChatExtensionUiRecord>,
    pub(crate) statuses: Vec<ChatExtensionKeyedRecord>,
    pub(crate) widgets: Vec<ChatExtensionKeyedRecord>,
    pub(crate) editor_text: Option<String>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChatTranscriptPage {
    pub(crate) messages: Vec<ChatMessage>,
    pub(crate) tools: Vec<ChatToolExecution>,
    pub(crate) older_cursor: Option<String>,
    pub(crate) newer_cursor: Option<String>,
    pub(crate) page_bytes: usize,
    pub(crate) event_cursor: u64,
    pub(crate) generation: String,
}

// Task 3 enforces this while measuring serialized page envelopes.
#[allow(dead_code)]
pub(crate) const MAX_TRANSCRIPT_PAGE_BYTES: usize = 4 * 1024 * 1024;

/// The authenticated UI protocol event duplicates metadata around its inner message. Keep this
/// fixed headroom so a transcript envelope that is valid on its own also fits the actual Iroh/UI
/// payload after wrapping. `pageBytes` remains the real inner `AgentMessage` byte count.
pub(crate) const TRANSCRIPT_DELIVERY_HEADROOM_BYTES: usize = 1024;
pub(crate) const MAX_TRANSCRIPT_DELIVERY_BYTES: usize =
    MAX_TRANSCRIPT_PAGE_BYTES - TRANSCRIPT_DELIVERY_HEADROOM_BYTES;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub(crate) enum TranscriptDirection {
    Older,
    Newer,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChatSnapshot {
    pub(crate) state: ChatSessionState,
    pub(crate) transcript: ChatTranscriptPage,
    pub(crate) extension: ChatExtensionState,
    pub(crate) event_sequence: u64,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct PromptAccepted {
    pub(crate) request_id: String,
    pub(crate) user_message_id: String,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct ChatSessionStarted {
    pub(crate) state: ChatSessionState,
    pub(crate) pid: Option<u32>,
    pub(crate) prompt: PromptAccepted,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ChatDeltaKind {
    Text,
    Thinking,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum ChatToolStatus {
    Running,
    Succeeded,
    Failed,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ChatToolExecution {
    pub(crate) id: String,
    pub(crate) name: String,
    pub(crate) arguments: Value,
    pub(crate) output: Option<Value>,
    pub(crate) is_error: bool,
    pub(crate) status: ChatToolStatus,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(
    tag = "type",
    rename_all = "camelCase",
    rename_all_fields = "camelCase"
)]
pub(crate) enum ChatEvent {
    SessionStarted {
        session_id: String,
        started: ChatSessionStarted,
    },
    StatusChanged {
        session_id: String,
        status: ChatStatus,
    },
    PromptAccepted {
        session_id: String,
        request_id: String,
        user_message_id: String,
    },
    AgentEnded {
        session_id: String,
        will_retry: bool,
    },
    MessageStarted {
        session_id: String,
        message: ChatMessage,
    },
    ContentDelta {
        session_id: String,
        message_id: String,
        content_index: usize,
        kind: ChatDeltaKind,
        delta: String,
    },
    ToolCallDelta {
        session_id: String,
        message_id: String,
        content_index: usize,
        delta: String,
    },
    ToolCallCompleted {
        session_id: String,
        message_id: String,
        content_index: usize,
        tool_call: ChatContent,
    },
    MessageEnded {
        session_id: String,
        message: ChatMessage,
    },
    ToolExecutionChanged {
        session_id: String,
        execution: ChatToolExecution,
        output_is_accumulated: bool,
    },
    QueueUpdated {
        session_id: String,
        steering: Vec<String>,
        follow_up: Vec<String>,
    },
    CompactionChanged {
        session_id: String,
        started: bool,
        reason: String,
        result: Option<Value>,
        aborted: bool,
        will_retry: bool,
        error: Option<String>,
    },
    RetryChanged {
        session_id: String,
        started: bool,
        attempt: u64,
        max_attempts: Option<u64>,
        delay_ms: Option<u64>,
        success: Option<bool>,
        error: Option<String>,
    },
    ThinkingLevelChanged {
        session_id: String,
        level: ThinkingLevel,
    },
    SessionInfoChanged {
        session_id: String,
        name: Option<String>,
    },
    UsageUpdated {
        session_id: String,
        message_id: String,
        usage: Value,
    },
    StatsUpdated {
        session_id: String,
        stats: PiSessionStats,
    },
    ExtensionError {
        session_id: String,
        extension_path: String,
        event: String,
        error: String,
    },
    ExtensionUiRequest {
        session_id: String,
        id: String,
        method: String,
        payload: Value,
    },
    Error {
        session_id: String,
        code: String,
        message: String,
        recoverable: bool,
    },
    Exited {
        session_id: String,
        message: String,
    },
    ResyncRequired {
        session_id: String,
        reason: String,
    },
}

/// Task 6 reconnect rule: after applying a snapshot, discard only queued
/// records whose `event_sequence` is `Some(sequence)` and `sequence` is at or
/// before the snapshot cursor. Records with `None` are manager control events
/// and must always be handled.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SequencedChatEvent {
    pub(crate) event_sequence: Option<u64>,
    pub(crate) event: ChatEvent,
}

impl ChatEvent {
    pub(crate) fn session_id(&self) -> &str {
        match self {
            Self::SessionStarted { session_id, .. }
            | Self::StatusChanged { session_id, .. }
            | Self::PromptAccepted { session_id, .. }
            | Self::AgentEnded { session_id, .. }
            | Self::MessageStarted { session_id, .. }
            | Self::ContentDelta { session_id, .. }
            | Self::ToolCallDelta { session_id, .. }
            | Self::ToolCallCompleted { session_id, .. }
            | Self::MessageEnded { session_id, .. }
            | Self::ToolExecutionChanged { session_id, .. }
            | Self::QueueUpdated { session_id, .. }
            | Self::CompactionChanged { session_id, .. }
            | Self::RetryChanged { session_id, .. }
            | Self::ThinkingLevelChanged { session_id, .. }
            | Self::SessionInfoChanged { session_id, .. }
            | Self::UsageUpdated { session_id, .. }
            | Self::StatsUpdated { session_id, .. }
            | Self::ExtensionError { session_id, .. }
            | Self::ExtensionUiRequest { session_id, .. }
            | Self::Error { session_id, .. }
            | Self::Exited { session_id, .. }
            | Self::ResyncRequired { session_id, .. } => session_id,
        }
    }
}