rho-coding-agent 1.17.1

A lightweight agent harness inspired by Pi
Documentation
use rho_sdk::{Error, RunEvent};

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum InteractiveState {
    #[default]
    Idle,
    Run(RunState),
    Transition(TransitionState),
    Completed,
    Failed,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RunState {
    Running(RunPhase),
    WaitingForHostInput,
    Cancelling(RunPhase),
    Compacting,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum TransitionState {
    SwitchingProvider,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RunPhase {
    Model,
    Tool,
    Steering,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ActiveRunCommand {
    Quit,
    SwitchSession,
    ReplaceProvider,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ActiveRunDisposition {
    CancelAndWait,
    RejectUntilFinished,
    DeferUntilFinished,
}

pub(crate) const fn active_run_disposition(command: ActiveRunCommand) -> ActiveRunDisposition {
    match command {
        ActiveRunCommand::Quit => ActiveRunDisposition::CancelAndWait,
        ActiveRunCommand::SwitchSession => ActiveRunDisposition::RejectUntilFinished,
        ActiveRunCommand::ReplaceProvider => ActiveRunDisposition::DeferUntilFinished,
    }
}

pub(crate) fn begin_provider_switch(current: InteractiveState) -> Result<InteractiveState, Error> {
    if current == InteractiveState::Idle {
        Ok(InteractiveState::Transition(
            TransitionState::SwitchingProvider,
        ))
    } else {
        Err(Error::SessionBusy)
    }
}

pub(crate) fn state_after_event(current: InteractiveState, event: &RunEvent) -> InteractiveState {
    match event {
        RunEvent::Started { .. } | RunEvent::StepStarted { .. } => {
            running_unless_cancelling(current, RunPhase::Model)
        }
        RunEvent::ToolStarted { .. } => running_unless_cancelling(current, RunPhase::Tool),
        RunEvent::ToolFinished { .. } => running_unless_cancelling(current, RunPhase::Model),
        RunEvent::HostInputRequested { .. } | RunEvent::ToolHostInputRequested { .. } => {
            if is_cancelling(current) {
                current
            } else {
                InteractiveState::Run(RunState::WaitingForHostInput)
            }
        }
        RunEvent::CompactionStarted { .. } => {
            if is_cancelling(current) {
                current
            } else {
                InteractiveState::Run(RunState::Compacting)
            }
        }
        RunEvent::CompactionCompleted { .. } => running_unless_cancelling(current, RunPhase::Model),
        RunEvent::Completed { .. } | RunEvent::Cancelled { .. } => InteractiveState::Completed,
        RunEvent::Failed { .. } => InteractiveState::Failed,
        _ => current,
    }
}

fn running_unless_cancelling(current: InteractiveState, phase: RunPhase) -> InteractiveState {
    if is_cancelling(current) {
        current
    } else {
        InteractiveState::Run(RunState::Running(phase))
    }
}

fn is_cancelling(state: InteractiveState) -> bool {
    matches!(state, InteractiveState::Run(RunState::Cancelling(_)))
}

#[cfg(test)]
#[path = "interactive_state_tests.rs"]
mod tests;