roder-tools 0.1.0

Agentic software development tools and SDKs for Roder.
Documentation
pub(super) fn trim_output_buffer_to_max_bytes(
    output: &mut String,
    cursor: &mut usize,
    max_bytes: usize,
) {
    if output.len() <= max_bytes {
        return;
    }

    let keep_from = output.len() - max_bytes;
    let remove_to = next_char_boundary(output, keep_from);
    output.replace_range(..remove_to, "");
    *cursor = cursor.saturating_sub(remove_to).min(output.len());
}

pub(super) fn truncate_output(
    output: &str,
    max_output_tokens: Option<usize>,
    default_max_output_tokens: usize,
) -> String {
    let max_bytes = max_output_tokens
        .unwrap_or(default_max_output_tokens)
        .saturating_mul(4);
    if output.len() <= max_bytes {
        return output.trim_end().to_string();
    }

    let keep_from = output.len() - max_bytes;
    let suffix_start = next_char_boundary(output, keep_from);
    let suffix = output[suffix_start..].trim_end();
    format!("[{} bytes omitted]\n{suffix}", suffix_start)
}

pub(super) fn format_exec_output(
    exit_code: Option<i32>,
    status: &str,
    duration_ms: u64,
    session_id: Option<u64>,
    output: &str,
) -> String {
    let exit = exit_code
        .map(|code| code.to_string())
        .unwrap_or_else(|| "still running".to_string());
    let mut text = format!(
        "Exit code: {exit}\nStatus: {status}\nWall time: {:.3} seconds",
        duration_ms as f64 / 1000.0
    );
    if status == "running"
        && let Some(session_id) = session_id
    {
        text.push_str(&format!("\nSession id: {session_id}"));
    }
    text.push_str(&format!("\nOutput:\n{output}"));
    text
}

fn next_char_boundary(text: &str, start: usize) -> usize {
    if start >= text.len() {
        return text.len();
    }

    let mut index = start;
    while index < text.len() && !text.is_char_boundary(index) {
        index += 1;
    }
    index
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn truncates_output_on_utf8_boundary() {
        let output = format!("{}{}", "a".repeat(20), "é".repeat(20));

        let truncated = truncate_output(&output, Some(5), 6000);

        assert!(truncated.starts_with("["));
        assert!(truncated.contains("bytes omitted]\n"));
        assert!(truncated.ends_with('é'));
    }

    #[test]
    fn trims_output_buffer_on_utf8_boundary_and_updates_cursor() {
        let mut output = format!("{}{}", "é".repeat(8), "tail");
        let mut cursor = output.len();

        trim_output_buffer_to_max_bytes(&mut output, &mut cursor, 5);

        assert_eq!(output, "tail");
        assert_eq!(cursor, output.len());
    }

    #[test]
    fn trimming_buffer_resets_cursor_when_unread_prefix_is_removed() {
        let mut output = format!("{}{}", "é".repeat(8), "tail");
        let mut cursor = 2;

        trim_output_buffer_to_max_bytes(&mut output, &mut cursor, 5);

        assert_eq!(output, "tail");
        assert_eq!(cursor, 0);
    }
}