regy 0.1.4

Private-by-default desktop agent for the Regy web interface
use std::{path::Path, sync::Arc, time::Duration};

use crate::{
    domain::{
        errors::{AgentError, AgentResult, ErrorCode},
        pi_rpc::{PiCommandInfo, PiRpcCommand, PiRpcResponse},
    },
    infrastructure::pi_rpc_probe::PiRpcProbe,
    operational::pi_rpc_manager::PiRpcManager,
};

#[derive(Clone)]
pub(crate) struct PiSkillRuntime {
    probe: PiRpcProbe,
    manager: Arc<PiRpcManager>,
}

impl PiSkillRuntime {
    pub(crate) fn new(
        command: Vec<String>,
        request_timeout: Duration,
        manager: Arc<PiRpcManager>,
    ) -> Self {
        Self {
            probe: PiRpcProbe::new(command, request_timeout),
            manager,
        }
    }

    pub(crate) async fn get_commands(&self, workspace: &Path) -> AgentResult<Vec<PiCommandInfo>> {
        match self.manager.get_commands(workspace).await {
            Ok(Some(commands)) => Ok(commands),
            Ok(None) => self.fresh_commands(workspace).await,
            Err(error) => Err(skill_runtime_error("active Pi session", error)),
        }
    }

    pub(crate) async fn fresh_commands(&self, workspace: &Path) -> AgentResult<Vec<PiCommandInfo>> {
        if !self.probe.is_configured() {
            return Err(AgentError::new(
                ErrorCode::SkillFilesystemFailed,
                "Pi RPC command is not configured for skill discovery",
            ));
        }
        let workspace = workspace
            .canonicalize()
            .map_err(|_| AgentError::new(ErrorCode::SkillScopeDenied, "skill workspace denied"))?;
        let response = self
            .probe
            .request(
                &workspace,
                PiRpcCommand::GetCommands {
                    id: "__regy:skills:probe".into(),
                },
            )
            .await
            .map_err(|error| skill_runtime_error("ephemeral Pi probe", error))?;
        match response {
            PiRpcResponse::GetCommands { data, .. } => Ok(data.commands),
            PiRpcResponse::Failure { error, .. } => Err(AgentError::new(
                ErrorCode::SkillFilesystemFailed,
                format!("Pi get_commands failed: {error}"),
            )),
            response => Err(AgentError::new(
                ErrorCode::SkillInvalid,
                format!(
                    "Pi returned {} while waiting for get_commands",
                    response.command()
                ),
            )),
        }
    }
}

fn skill_runtime_error(context: &str, error: AgentError) -> AgentError {
    let code = if error.message().contains("invalid Pi RPC JSON record") {
        ErrorCode::SkillInvalid
    } else {
        ErrorCode::SkillFilesystemFailed
    };
    AgentError::new(
        code,
        format!("{context} skill discovery failed: {}", error.message()),
    )
}