ai-agent 0.13.4

Idiomatic agent sdk inspired by the claude code source leak
Documentation
// Source: /data/home/swei/claudecode/openclaudecode/src/services/compact/microCompact.ts
//! Microcompact module for truncating large tool results.
//!
//! This is a simplified implementation that truncates large tool results
//! to prevent 413 Payload Too Large errors.

use crate::types::Message;

/// Message shown when tool result content is cleared
pub const TIME_BASED_MC_CLEARED_MESSAGE: &str = "[Old tool result content cleared]";

/// Maximum tokens for images/documents in tool results
const IMAGE_MAX_TOKEN_SIZE: usize = 2000;

/// Maximum characters to keep per tool result (rough estimate: ~4000 tokens)
const MAX_TOOL_RESULT_CHARS: usize = 16_000;

/// Maximum number of glob results to keep
const MAX_GLOB_RESULTS: usize = 100;

/// Truncate tool result content if it's too large
/// This prevents 413 errors when the tool result is too big for the API
pub fn truncate_tool_result_content(content: &str, tool_name: &str) -> String {
    // For Glob results, always limit to 100 files (matching GlobTool behavior)
    if tool_name == "Glob" {
        let total_lines = content.lines().count();
        if total_lines <= MAX_GLOB_RESULTS {
            return content.to_string();
        }

        // Keep first 100 lines
        let lines: Vec<&str> = content.lines().take(MAX_GLOB_RESULTS).collect();
        let truncated = lines.join("\n");

        return format!(
            "{}\n\n... ({} more files not shown. Use more specific glob patterns to reduce results)",
            truncated,
            total_lines.saturating_sub(MAX_GLOB_RESULTS)
        );
    }

    // Check if the content exceeds the threshold for other tools
    if content.len() <= MAX_TOOL_RESULT_CHARS {
        return content.to_string();
    }

    // For other tools, truncate to max chars
    let chars: Vec<char> = content.chars().take(MAX_TOOL_RESULT_CHARS).collect();
    format!(
        "{}\n\n... (truncated {} characters)",
        chars.into_iter().collect::<String>(),
        content.len().saturating_sub(MAX_TOOL_RESULT_CHARS)
    )
}

/// Process messages to truncate large tool results
/// This is called before sending to the API
pub fn microcompact_messages(messages: &mut [Message]) {
    // For now, we only process the most recent tool results
    // to avoid excessive truncation on older messages

    for msg in messages.iter_mut() {
        if let crate::types::MessageRole::Tool = msg.role {
            // Check if content is too large
            if msg.content.len() > MAX_TOOL_RESULT_CHARS {
                // Truncate and add note
                let truncated = truncate_tool_result_content(&msg.content, "Tool");
                msg.content = truncated;
            }
        }
    }
}

/// Calculate estimated token count for tool result content
pub fn calculate_tool_result_tokens(content: &str) -> usize {
    // Rough estimation: 1 token per 4 characters for text
    content.len() / 4
}

/// Check if messages need microcompact (rough estimation)
pub fn needs_microcompact(messages: &[Message], threshold: usize) -> bool {
    let total_tool_chars: usize = messages
        .iter()
        .filter(|m| m.role == crate::types::MessageRole::Tool)
        .map(|m| m.content.len())
        .sum();

    // Rough token estimate: 1 token per 4 chars
    let estimated_tokens = total_tool_chars / 4;
    estimated_tokens > threshold
}

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

    #[test]
    fn test_truncate_tool_result_small() {
        let content = "small content";
        let result = truncate_tool_result_content(content, "Read");
        assert_eq!(result, "small content");
    }

    #[test]
    fn test_truncate_tool_result_large() {
        let content = "x".repeat(20000);
        let result = truncate_tool_result_content(&content, "Read");
        assert!(result.len() < content.len());
        assert!(result.contains("truncated"));
    }

    #[test]
    fn test_truncate_glob_results() {
        let files: Vec<String> = (0..200).map(|i| format!("file{}.txt", i)).collect();
        let content = files.join("\n");
        let result = truncate_tool_result_content(&content, "Glob");
        assert!(result.len() < content.len());
        assert!(result.contains("not shown"));
    }

    #[test]
    fn test_calculate_tool_result_tokens() {
        let content = "test content";
        let tokens = calculate_tool_result_tokens(content);
        assert!(tokens > 0);
    }
}