malvin 0.2.5

Non-interactive research and coding agent
#[path = "agent_phase_signal.rs"]
mod agent_phase_signal;

use std::sync::Mutex;

use crate::tool_summary::{ParsedToolUpdate, ToolSummaryTracker};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum AgentPhase {
    Orienting = 0,
    Researching,
    Reasoning,
    Implementing,
    Executing,
    Verifying,
    Debugging,
    Waiting,
    Reporting,
}

const PHASE_LABELS: [&str; 9] = [
    "Orienting",
    "Researching",
    "Reasoning",
    "Implementing",
    "Executing",
    "Verifying",
    "Debugging",
    "Waiting",
    "Reporting",
];

impl AgentPhase {
    #[must_use]
    pub const fn label(self) -> &'static str {
        PHASE_LABELS[self as usize]
    }
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ToolKind {
    Read,
    Search,
    Edit,
    Execute,
}

#[allow(clippy::struct_excessive_bools)]
pub(super) struct PhaseState {
    pub(super) verifying_depth: u32,
    pub(super) reporting: bool,
    pub(super) orienting: bool,
    pub(super) debugging: bool,
    pub(super) running_shells: u32,
    pub(super) reasoning: bool,
    pub(super) active_tool: Option<(ToolKind, u8)>,
}

impl PhaseState {
    const fn fresh() -> Self {
        Self {
            verifying_depth: 0,
            reporting: false,
            orienting: true,
            debugging: false,
            running_shells: 0,
            reasoning: false,
            active_tool: None,
        }
    }

    fn resolve(&self) -> AgentPhase {
        phase_if(self.reporting, AgentPhase::Reporting)
            .or_else(|| phase_if(self.verifying_depth > 0, AgentPhase::Verifying))
            .or_else(|| phase_if(self.running_shells > 0, AgentPhase::Waiting))
            .or_else(|| phase_if(self.debugging, AgentPhase::Debugging))
            .or_else(|| self.active_tool.map(|(k, _)| active_tool_phase(k)))
            .or_else(|| phase_if(self.reasoning, AgentPhase::Reasoning))
            .or_else(|| phase_if(self.orienting, AgentPhase::Orienting))
            .unwrap_or(AgentPhase::Reasoning)
    }
}

fn phase_if(cond: bool, phase: AgentPhase) -> Option<AgentPhase> {
    cond.then_some(phase)
}

const fn active_tool_phase(kind: ToolKind) -> AgentPhase {
    match kind {
        ToolKind::Read | ToolKind::Search => AgentPhase::Researching,
        ToolKind::Edit => AgentPhase::Implementing,
        ToolKind::Execute => AgentPhase::Executing,
    }
}

static STATE: Mutex<PhaseState> = Mutex::new(PhaseState::fresh());

#[cfg(test)]
pub(crate) static AGENT_PHASE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

pub(crate) fn with_state<R>(f: impl FnOnce(&mut PhaseState) -> R) -> R {
    let mut guard = STATE
        .lock()
        .unwrap_or_else(std::sync::PoisonError::into_inner);
    f(&mut guard)
}

pub fn reset_for_run() {
    with_state(|s| *s = PhaseState::fresh());
}

pub fn note_orienting() {
    with_state(|s| {
        s.orienting = true;
        s.reasoning = false;
    });
}

pub fn enter_verifying() {
    with_state(|s| {
        s.verifying_depth = s.verifying_depth.saturating_add(1);
        s.orienting = false;
    });
}

pub fn leave_verifying() {
    with_state(|s| s.verifying_depth = s.verifying_depth.saturating_sub(1));
}

pub fn set_reporting(active: bool) {
    with_state(|s| s.reporting = active);
}

pub(crate) fn observe_tool_update(parsed: &ParsedToolUpdate, tracker: &ToolSummaryTracker) {
    with_state(|s| agent_phase_signal::observe_tool_update_state(s, parsed, tracker));
}

#[must_use]
pub fn heartbeat_label() -> &'static str {
    with_state(|s| s.resolve().label())
}

#[cfg(test)]
pub(crate) fn current_phase_for_test() -> AgentPhase {
    with_state(|s| s.resolve())
}

#[cfg(test)]
pub(crate) fn reset_phase_state_for_test() {
    with_state(|s| *s = PhaseState::fresh());
}

#[cfg(test)]
pub(crate) mod kiss_cov {
    pub(crate) use super::ToolKind;
    use super::{active_tool_phase, phase_if};

    #[must_use]
    pub(crate) fn witness_tool_kinds() -> [ToolKind; 4] {
        [
            ToolKind::Read,
            ToolKind::Search,
            ToolKind::Edit,
            ToolKind::Execute,
        ]
    }

    #[must_use]
    pub(crate) fn witness_phase_if(cond: bool) -> Option<super::AgentPhase> {
        phase_if(cond, super::AgentPhase::Waiting)
    }

    #[must_use]
    pub(crate) fn witness_active_tool_phase(kind: ToolKind) -> super::AgentPhase {
        active_tool_phase(kind)
    }
}