rho-coding-agent 1.4.1

A lightweight agent harness inspired by Pi
Documentation
use crate::tool::truncate;

use super::agent::SubagentSnapshot;

const DETAIL_BYTES: usize = 160;
const RESULT_EXCERPT_BYTES: usize = 16 * 1024;

#[derive(Clone, Copy)]
pub(super) enum SnapshotFormat {
    Completion,
    Status,
}

pub(super) fn format_background_start(id: &str, preset: &str) -> String {
    format!("subagent {id} ({preset}) started in background\nattach: rho attach {id}")
}

pub(super) fn format_running(id: &str) -> String {
    format!("subagent {id} running\nattach: rho attach {id}")
}

pub(super) fn format_snapshot(snapshot: &SubagentSnapshot, format: SnapshotFormat) -> String {
    let mut lines = vec![format!(
        "subagent {} ({}): {}",
        snapshot.id,
        snapshot.preset,
        snapshot.status.state.as_str()
    )];
    let metrics = format!(
        "turns: {} · tokens: {} in / {} out",
        snapshot.status.turns, snapshot.status.input_tokens, snapshot.status.output_tokens
    );
    match format {
        SnapshotFormat::Completion => lines.push(metrics),
        SnapshotFormat::Status => {
            lines.push(format!(
                "elapsed: {} · {metrics}",
                format_elapsed(snapshot.elapsed.as_secs())
            ));
            if let Some(activity) = &snapshot.status.last_activity {
                lines.push(format!("activity: {activity}"));
            }
            if let Some(text) = &snapshot.status.last_text {
                lines.push(format!("latest: {text}"));
            }
        }
    }
    if let Some(error) = &snapshot.status.error {
        lines.push(format!("error: {error}"));
    }
    if let Some(error) = &snapshot.status.attachment_error {
        lines.push(format!("attachment error: {error}"));
    }
    if matches!(format, SnapshotFormat::Status) {
        lines.push(format!("attach: rho attach {}", snapshot.id));
    }
    if snapshot.done {
        if let Some(result) = snapshot
            .status
            .result
            .as_deref()
            .filter(|result| !result.is_empty())
        {
            lines.push(String::new());
            lines.push(truncate(result.to_string(), RESULT_EXCERPT_BYTES));
        }
    }
    lines.join("\n")
}

pub(super) fn format_list_entry(snapshot: &SubagentSnapshot) -> String {
    let detail = snapshot
        .status
        .last_activity
        .as_deref()
        .or(snapshot.status.last_text.as_deref())
        .unwrap_or(if snapshot.done { "finished" } else { "working" });
    let detail = detail.lines().next().unwrap_or_default().trim();
    format!(
        "{}  {}  {}  {}  {}",
        snapshot.id,
        snapshot.preset,
        snapshot.status.state.as_str(),
        format_elapsed(snapshot.elapsed.as_secs()),
        truncate(detail.to_string(), DETAIL_BYTES)
    )
}

fn format_elapsed(seconds: u64) -> String {
    if seconds < 60 {
        return format!("{seconds}s");
    }
    let minutes = seconds / 60;
    let seconds = seconds % 60;
    if minutes < 60 {
        return format!("{minutes}m {seconds:02}s");
    }
    let hours = minutes / 60;
    let minutes = minutes % 60;
    format!("{hours}h {minutes:02}m")
}

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