vtcode 0.108.0

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use serde_json::{Value, json};
use std::time::Duration;
use vtcode_core::config::constants::tools as tool_names;
use vtcode_core::tools::registry::ToolRegistry;
use vtcode_core::tools::tool_intent;

use crate::agent::runloop::unified::tool_reads::{is_read_file_style_call, read_file_path_arg};

pub(super) use crate::agent::runloop::unified::tool_reads::spool_chunk_read_path;

const READ_FILE_OFFSET_KEYS: &[&str] = &["offset", "offset_lines", "offset_bytes"];
const READ_FILE_LIMIT_KEYS: &[&str] = &["limit", "page_size_lines", "max_lines", "chunk_lines"];

fn compact_loop_key_part(value: &str, max_chars: usize) -> String {
    value.trim().chars().take(max_chars).collect()
}

fn compact_loop_text(value: &str, max_chars: usize) -> String {
    compact_loop_key_part(
        &value.split_whitespace().collect::<Vec<_>>().join(" "),
        max_chars,
    )
}

fn normalize_shell_command_text(value: &str, max_chars: usize) -> String {
    compact_loop_text(
        &value
            .chars()
            .filter(|ch| !matches!(ch, '\'' | '"'))
            .collect::<String>(),
        max_chars,
    )
}

fn normalized_shell_command_arg(args: &Value, max_chars: usize) -> Option<String> {
    vtcode_core::tools::command_args::command_text(args)
        .ok()
        .flatten()
        .map(|command| normalize_shell_command_text(&command, max_chars))
        .filter(|command| !command.is_empty())
}

fn unified_search_globs_arg(args: &Value) -> Option<String> {
    let globs = args.get("globs")?;
    match globs {
        Value::String(value) => {
            let trimmed = value.trim();
            if trimmed.is_empty() {
                None
            } else {
                Some(compact_loop_text(trimmed, 120))
            }
        }
        Value::Array(items) => {
            let joined = items
                .iter()
                .filter_map(Value::as_str)
                .map(str::trim)
                .filter(|value| !value.is_empty())
                .collect::<Vec<_>>()
                .join(",");
            if joined.is_empty() {
                None
            } else {
                Some(compact_loop_text(&joined, 120))
            }
        }
        _ => None,
    }
}

fn first_arg_value_by_keys<'a>(args: &'a Value, keys: &[&str]) -> Option<&'a Value> {
    keys.iter().find_map(|key| args.get(*key))
}

fn has_any_arg_by_keys(args: &Value, keys: &[&str]) -> bool {
    first_arg_value_by_keys(args, keys).is_some()
}

fn read_file_has_offset_arg(args: &Value) -> bool {
    has_any_arg_by_keys(args, READ_FILE_OFFSET_KEYS)
}

fn read_file_offset_value(args: &Value) -> Option<usize> {
    first_arg_value_by_keys(args, READ_FILE_OFFSET_KEYS).and_then(|value| {
        value
            .as_u64()
            .and_then(|n| usize::try_from(n).ok())
            .or_else(|| value.as_str().and_then(|s| s.parse::<usize>().ok()))
    })
}

fn read_file_has_limit_arg(args: &Value) -> bool {
    has_any_arg_by_keys(args, READ_FILE_LIMIT_KEYS)
}

pub(super) fn shell_run_signature(canonical_tool_name: &str, args: &Value) -> Option<String> {
    if !tool_intent::is_command_run_tool_call(canonical_tool_name, args) {
        return None;
    }

    let command = normalized_shell_command_arg(args, 200)?;
    Some(format!("{}::{}", tool_names::UNIFIED_EXEC, command))
}

pub(super) fn maybe_apply_spool_read_offset_hint(
    tool_registry: &mut ToolRegistry,
    canonical_tool_name: &str,
    args: &Value,
) -> Value {
    if !is_read_file_style_call(canonical_tool_name, args) {
        return args.clone();
    }

    let Some(path) = spool_chunk_read_path(canonical_tool_name, args) else {
        return args.clone();
    };

    let Some((next_offset, chunk_limit)) =
        tool_registry.find_recent_read_file_spool_progress(path, Duration::from_secs(180))
    else {
        return args.clone();
    };

    let requested_offset = read_file_offset_value(args);
    let should_advance_offset = match requested_offset {
        Some(existing) => existing < next_offset,
        None => true,
    };
    let should_fill_offset = !read_file_has_offset_arg(args);

    let mut adjusted = args.clone();
    let keep_existing_limit = read_file_has_limit_arg(&adjusted);
    if let Some(obj) = adjusted.as_object_mut() {
        if should_fill_offset || should_advance_offset {
            obj.insert("offset".to_string(), json!(next_offset));
        }
        if !keep_existing_limit {
            obj.insert("limit".to_string(), json!(chunk_limit));
        }
        if should_fill_offset || should_advance_offset || !keep_existing_limit {
            tracing::debug!(
                tool = canonical_tool_name,
                path = path,
                requested_offset = requested_offset.unwrap_or(0),
                next_offset,
                chunk_limit,
                "Applied spool read continuation hint to avoid repeated identical chunk reads"
            );
        }
    }
    adjusted
}

pub(super) fn task_tracker_create_signature(tool_name: &str, args: &Value) -> Option<String> {
    if tool_name != tool_names::TASK_TRACKER {
        return None;
    }

    let action = args.get("action").and_then(Value::as_str)?;
    if action != "create" {
        return None;
    }

    #[derive(serde::Serialize)]
    struct TaskTrackerCreateSignature<'a> {
        title: Option<&'a Value>,
        items: Option<&'a Value>,
        notes: Option<&'a Value>,
    }

    let payload = TaskTrackerCreateSignature {
        title: args.get("title"),
        items: args.get("items"),
        notes: args.get("notes"),
    };
    let payload_str = serde_json::to_string(&payload).ok()?;
    let mut signature = String::with_capacity("task_tracker::create::".len() + payload_str.len());
    signature.push_str("task_tracker::create::");
    signature.push_str(&payload_str);

    Some(signature)
}
pub(crate) fn low_signal_family_key(canonical_tool_name: &str, args: &Value) -> Option<String> {
    match canonical_tool_name {
        tool_names::READ_FILE => read_file_path_arg(args).map(|path| {
            format!(
                "{canonical_tool_name}::{}",
                compact_loop_key_part(path, 120)
            )
        }),
        tool_names::UNIFIED_FILE => {
            let action = tool_intent::unified_file_action(args).unwrap_or("read");
            if !action.eq_ignore_ascii_case("read") {
                return None;
            }
            read_file_path_arg(args).map(|path| {
                format!(
                    "{canonical_tool_name}::read::{}",
                    compact_loop_key_part(path, 120)
                )
            })
        }
        tool_names::UNIFIED_EXEC => normalized_shell_command_arg(args, 160)
            .map(|command| format!("{canonical_tool_name}::run::{command}")),
        tool_names::UNIFIED_SEARCH => {
            let normalized = tool_intent::normalize_unified_search_args(args);
            let mut key = canonical_tool_name.to_string();
            if let Some(globs) = unified_search_globs_arg(&normalized) {
                key.push_str("::globs=");
                key.push_str(&globs);
            } else {
                let path = normalized
                    .get("path")
                    .and_then(Value::as_str)
                    .map(|value| compact_loop_key_part(value, 120))
                    .unwrap_or_else(|| ".".to_string());
                key.push_str("::");
                key.push_str(&path);
            }
            Some(key)
        }
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::{read_file_has_limit_arg, read_file_has_offset_arg, read_file_offset_value};
    use serde_json::json;

    #[test]
    fn read_file_offset_value_accepts_alias_keys() {
        assert_eq!(read_file_offset_value(&json!({"offset": 7})), Some(7));
        assert_eq!(
            read_file_offset_value(&json!({"offset_lines": "8"})),
            Some(8)
        );
        assert_eq!(read_file_offset_value(&json!({"offset_bytes": 9})), Some(9));
    }

    #[test]
    fn read_file_has_offset_arg_accepts_alias_keys() {
        assert!(read_file_has_offset_arg(&json!({"offset_lines": 1})));
        assert!(read_file_has_offset_arg(&json!({"offset_bytes": 1})));
        assert!(!read_file_has_offset_arg(&json!({"path": "src/main.rs"})));
    }

    #[test]
    fn read_file_has_limit_arg_accepts_alias_keys() {
        assert!(read_file_has_limit_arg(&json!({"limit": 10})));
        assert!(read_file_has_limit_arg(&json!({"page_size_lines": 10})));
        assert!(read_file_has_limit_arg(&json!({"max_lines": 10})));
        assert!(read_file_has_limit_arg(&json!({"chunk_lines": 10})));
        assert!(!read_file_has_limit_arg(&json!({"path": "src/main.rs"})));
    }
}