vv-agent 0.6.2

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use serde_json::{json, Value};

use crate::types::{ToolDirective, ToolExecutionResult, ToolResultStatus};

use super::format::{
    cap_file_counts, cap_file_paths, cap_match_rows, render_grep_content, truncate_result_text,
    MAX_STRUCTURED_CHARS, MAX_STRUCTURED_ITEMS,
};
use super::local_rg::RgGrepResult;
use super::request::SearchFilesRequest;

pub(super) fn search_files_success_response(
    request: &SearchFilesRequest,
    mut result: RgGrepResult,
) -> ToolExecutionResult {
    result.files_with_matches.sort();
    let files_with_match_count = result.files_with_matches.len();
    let total_result_items = match request.output_mode.as_str() {
        "files_with_matches" => result.files_with_matches.len(),
        "count" => result.file_counts.len(),
        _ => result.rows.len(),
    };
    let head_limited;
    match request.output_mode.as_str() {
        "files_with_matches" => {
            let (start, end, limited) = slice_bounds(
                result.files_with_matches.len(),
                request.offset,
                request.head_limit,
            );
            head_limited = limited;
            result.files_with_matches = result.files_with_matches[start..end].to_vec();
        }
        "count" => {
            let count_items = result.file_counts.into_iter().collect::<Vec<_>>();
            let (start, end, limited) =
                slice_bounds(count_items.len(), request.offset, request.head_limit);
            head_limited = limited;
            result.file_counts = count_items[start..end].iter().cloned().collect();
        }
        _ => {
            let (start, end, limited) =
                slice_bounds(result.rows.len(), request.offset, request.head_limit);
            head_limited = limited;
            result.rows = result.rows[start..end].to_vec();
        }
    }
    let structured_capped;
    match request.output_mode.as_str() {
        "files_with_matches" => {
            let (capped_files, capped) = cap_file_paths(result.files_with_matches);
            result.files_with_matches = capped_files;
            structured_capped = capped;
        }
        "count" => {
            let (capped_counts, capped) = cap_file_counts(result.file_counts);
            result.file_counts = capped_counts;
            structured_capped = capped;
        }
        _ => {
            let (capped_rows, capped) = cap_match_rows(result.rows);
            result.rows = capped_rows;
            structured_capped = capped;
        }
    }
    let structured_truncated = head_limited || structured_capped;

    let summary = json!({
        "files_searched": result.files_searched,
        "files_with_matches": files_with_match_count,
        "total_matches": result.total_matches,
    });
    let mut payload = json!({
        "summary": summary,
        "pattern": request.pattern.clone(),
        "path": request.path.clone(),
        "glob": request.glob_pattern.clone(),
        "type": request.file_type.clone(),
        "output_mode": request.output_mode.clone(),
        "literal": request.literal,
        "offset": request.offset,
        "head_limit": request.head_limit,
        "head_limited": head_limited,
        "total_result_items": total_result_items,
        "returned_count": match request.output_mode.as_str() {
            "files_with_matches" => result.files_with_matches.len(),
            "count" => result.file_counts.len(),
            _ => result.rows.len(),
        },
        "content_truncated": false,
        "structured_truncated": structured_truncated,
        "truncated": structured_truncated,
        "sensitive_files_omitted": result.sensitive_files_omitted,
    });
    if structured_capped {
        payload["structured_item_limit"] = json!(MAX_STRUCTURED_ITEMS);
        payload["structured_char_limit"] = json!(MAX_STRUCTURED_CHARS);
    }
    match request.output_mode.as_str() {
        "files_with_matches" => payload["files"] = json!(result.files_with_matches),
        "count" => payload["file_counts"] = json!(result.file_counts),
        _ => payload["matches"] = Value::Array(result.rows),
    }
    let content = render_grep_content(
        &request.output_mode,
        &request.pattern,
        &payload,
        request.show_line_numbers,
        structured_truncated,
    );
    let (content, content_truncated) =
        truncate_result_text(content, result.total_matches, files_with_match_count);
    payload["content_truncated"] = json!(content_truncated);
    payload["truncated"] = json!(content_truncated || structured_truncated);
    let metadata = payload
        .as_object()
        .map(|object| {
            object
                .iter()
                .map(|(key, value)| (key.clone(), value.clone()))
                .collect()
        })
        .unwrap_or_default();
    ToolExecutionResult {
        tool_call_id: String::new(),
        content,
        status: ToolResultStatus::Success,
        directive: ToolDirective::Continue,
        error_code: None,
        metadata,
        image_url: None,
        image_path: None,
    }
}

fn slice_bounds(total: usize, offset: usize, head_limit: usize) -> (usize, usize, bool) {
    if offset >= total {
        return (total, total, false);
    }
    if head_limit == 0 {
        return (offset, total, false);
    }
    let end = offset.saturating_add(head_limit).min(total);
    (offset, end, end < total)
}