roder-core 0.1.6

Agentic software development tools and SDKs for Roder.
Documentation
const MAX_TOOL_OUTPUT_LINES: usize = 200;
const MAX_TOOL_OUTPUT_CHARS: usize = 20_000;
const ARTIFACT_INLINE_CHARS: usize = 6_000;
const NO_CONTINUATION_NOTE: &str = "No continuation cursor is available for this capped aggregate output; rerun the original tool with narrower pagination or filters when available.";

pub(crate) fn cap_tool_output_lines(output: String) -> String {
    let original_line_count = output.lines().count();
    let line_capped = cap_tool_output_line_count(output);
    cap_tool_output_chars(line_capped, original_line_count)
}

pub(crate) fn should_spill_tool_output(output: &str) -> bool {
    output.lines().count() > MAX_TOOL_OUTPUT_LINES || output.chars().count() > MAX_TOOL_OUTPUT_CHARS
}

pub(crate) fn artifact_backed_tool_output(
    output: &str,
    artifact_reference: &str,
    label: &str,
) -> String {
    let line_count = output.lines().count();
    let byte_count = output.len();
    let snippet = truncate_middle_chars(output, ARTIFACT_INLINE_CHARS);
    format!(
        "Tool output was stored in a local context artifact because it exceeded inline limits.\n\
         Label: {label}\n\
         Total output lines: {line_count}\n\
         Total output bytes: {byte_count}\n\n\
         {artifact_reference}\n\n\
         Inline excerpt:\n{snippet}"
    )
}

fn cap_tool_output_line_count(output: String) -> String {
    let mut lines = output.lines();
    let mut kept = Vec::new();
    for _ in 0..MAX_TOOL_OUTPUT_LINES {
        let Some(line) = lines.next() else {
            return output;
        };
        kept.push(line.to_string());
    }

    let remaining = lines.count();
    if remaining == 0 {
        return output;
    }

    kept.push(format!(
        "[tool output truncated after {MAX_TOOL_OUTPUT_LINES} lines; {remaining} more lines omitted. {NO_CONTINUATION_NOTE}]"
    ));
    kept.join("\n")
}

fn cap_tool_output_chars(output: String, original_line_count: usize) -> String {
    if output.chars().count() <= MAX_TOOL_OUTPUT_CHARS {
        return output;
    }

    let prefix = format!("Total output lines: {original_line_count}\n\n");
    let content_budget = MAX_TOOL_OUTPUT_CHARS.saturating_sub(prefix.chars().count());
    format!("{prefix}{}", truncate_middle_chars(&output, content_budget))
}

fn truncate_middle_chars(text: &str, max_chars: usize) -> String {
    let total_chars = text.chars().count();
    if total_chars <= max_chars {
        return text.to_string();
    }

    let marker = format!(
        "\n[tool output truncated; {} chars omitted. {NO_CONTINUATION_NOTE}]\n",
        total_chars.saturating_sub(max_chars)
    );
    let marker_chars = marker.chars().count();
    if marker_chars >= max_chars {
        return marker.chars().take(max_chars).collect();
    }

    let remaining = max_chars - marker_chars;
    let head_chars = remaining / 2;
    let tail_chars = remaining - head_chars;
    let head = text.chars().take(head_chars).collect::<String>();
    let tail = text
        .chars()
        .rev()
        .take(tail_chars)
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .collect::<String>();

    format!("{head}{marker}{tail}")
}

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

    #[test]
    fn caps_tool_output_lines_with_notice() {
        let output = (1..=205)
            .map(|line| format!("line {line}"))
            .collect::<Vec<_>>()
            .join("\n");

        let capped = cap_tool_output_lines(output);

        assert_eq!(capped.lines().count(), 201);
        assert!(capped.contains("line 200"));
        assert!(capped.contains("5 more lines omitted"));
        assert!(capped.contains("No continuation cursor is available"));
    }

    #[test]
    fn caps_single_huge_line_by_char_budget_with_notice() {
        let output = format!("start{}end", "x".repeat(MAX_TOOL_OUTPUT_CHARS * 2));

        let capped = cap_tool_output_lines(output);

        assert!(capped.chars().count() <= MAX_TOOL_OUTPUT_CHARS);
        assert!(capped.starts_with("Total output lines: 1\n\nstart"));
        assert!(capped.ends_with("end"));
        assert!(capped.contains("chars omitted"));
        assert!(capped.contains("No continuation cursor is available"));
    }

    #[test]
    fn caps_multi_line_output_by_char_budget_even_under_line_limit() {
        let output = (1..=50)
            .map(|line| format!("line {line}: {}", "x".repeat(1_000)))
            .collect::<Vec<_>>()
            .join("\n");

        let capped = cap_tool_output_lines(output);

        assert!(capped.chars().count() <= MAX_TOOL_OUTPUT_CHARS);
        assert!(capped.starts_with("Total output lines: 50\n\nline 1:"));
        assert!(capped.contains("chars omitted"));
        assert!(capped.contains("No continuation cursor is available"));
    }

    #[test]
    fn leaves_empty_tool_output_unchanged() {
        let capped = cap_tool_output_lines(String::new());

        assert_eq!(capped, "");
    }

    #[test]
    fn artifact_backed_summary_stays_bounded_and_names_reference() {
        let output = format!("start{}end", "x".repeat(MAX_TOOL_OUTPUT_CHARS * 2));
        let summary = artifact_backed_tool_output(
            &output,
            "[artifact: tool_output stdout lines=1 bytes=40008 id=artifact-1]",
            "stdout",
        );

        assert!(should_spill_tool_output(&output));
        assert!(summary.chars().count() < MAX_TOOL_OUTPUT_CHARS);
        assert!(summary.contains("artifact-1"));
        assert!(summary.contains("Inline excerpt"));
        assert!(summary.ends_with("end"));
    }
}