systemprompt-models 0.1.22

Shared data models and types for systemprompt.io OS
Documentation
use rmcp::model::{CallToolResult, RawContent};

use super::tools::ToolCall;
use crate::text::truncate_with_ellipsis;

#[derive(Debug, Clone, Copy, Default)]
pub struct ToolResultFormatter;

impl ToolResultFormatter {
    pub fn format_for_ai(tool_calls: &[ToolCall], tool_results: &[CallToolResult]) -> String {
        tool_calls
            .iter()
            .zip(tool_results.iter())
            .map(|(call, result)| Self::format_single_for_ai(call, result))
            .collect::<Vec<_>>()
            .join("\n")
    }

    pub fn format_single_for_ai(call: &ToolCall, result: &CallToolResult) -> String {
        let status = Self::status_string(result);
        let content_text = Self::extract_text_content(result);

        format!(
            "Tool '{}' [{}]: {}",
            call.name,
            status,
            truncate_with_ellipsis(&content_text, 500)
        )
    }

    pub fn format_for_synthesis(
        tool_calls: &[ToolCall],
        tool_results: &[CallToolResult],
    ) -> String {
        tool_calls
            .iter()
            .zip(tool_results.iter())
            .map(|(call, result)| Self::format_single_for_synthesis(call, result))
            .collect::<Vec<_>>()
            .join("\n---\n\n")
    }

    pub fn format_single_for_synthesis(call: &ToolCall, result: &CallToolResult) -> String {
        let status = Self::status_string(result);
        let is_success = !result.is_error.unwrap_or(false);
        let content_text = Self::extract_text_content(result);

        let summary = content_text
            .lines()
            .find(|l| !l.trim().is_empty())
            .unwrap_or("No summary available")
            .chars()
            .take(200)
            .collect::<String>();

        let truncated = truncate_with_ellipsis(&content_text, 1000);

        let completion_note = if is_success {
            "\n\n**IMPORTANT**: This tool completed successfully. The action has been performed. \
             Do NOT call this tool again with the same parameters."
        } else {
            ""
        };

        format!(
            "### Tool: {} [{}]\n\n**Summary**: {}\n**Details** (truncated):\n{}{}",
            call.name, status, summary, truncated, completion_note
        )
    }

    pub fn format_for_display(tool_calls: &[ToolCall], tool_results: &[CallToolResult]) -> String {
        tool_calls
            .iter()
            .zip(tool_results.iter())
            .enumerate()
            .map(|(i, (call, result))| Self::format_single_for_display(i + 1, call, result))
            .collect::<Vec<_>>()
            .join("\n")
    }

    pub fn format_single_for_display(
        index: usize,
        call: &ToolCall,
        result: &CallToolResult,
    ) -> String {
        let status = Self::status_string(result);
        let content_text = Self::extract_text_content(result);
        let preview = truncate_with_ellipsis(&content_text, 200);

        format!("{}. {} [{}]: {}", index, call.name, status, preview)
    }

    pub fn format_fallback_summary(
        tool_calls: &[ToolCall],
        tool_results: &[CallToolResult],
    ) -> String {
        let mut texts = Vec::new();

        for (call, result) in tool_calls.iter().zip(tool_results.iter()) {
            let is_error = result.is_error.unwrap_or(true);
            if is_error {
                continue;
            }

            let content_text = Self::extract_text_content(result);
            if !content_text.is_empty() {
                texts.push(format!("**{}**:\n{}", call.name, content_text));
            }
        }

        if texts.is_empty() {
            "Tool execution completed.".to_string()
        } else {
            texts.join("\n\n")
        }
    }

    fn status_string(result: &CallToolResult) -> &'static str {
        if result.is_error.unwrap_or(false) {
            "FAILED"
        } else {
            "SUCCESS"
        }
    }

    fn extract_text_content(result: &CallToolResult) -> String {
        result
            .content
            .iter()
            .filter_map(|c| match &c.raw {
                RawContent::Text(text_content) => Some(text_content.text.as_str()),
                RawContent::Image(_)
                | RawContent::Resource(_)
                | RawContent::Audio(_)
                | RawContent::ResourceLink(_) => None,
            })
            .collect::<Vec<_>>()
            .join("\n")
    }
}