use super::*;
fn path_targets_plan_artifact(path: &str) -> bool {
let normalized = path.trim().replace('\\', "/");
normalized == ".vtcode/plans"
|| normalized.starts_with(".vtcode/plans/")
|| normalized.contains("/.vtcode/plans/")
|| normalized == "/tmp/vtcode-plans"
|| normalized.starts_with("/tmp/vtcode-plans/")
|| normalized.contains("/tmp/vtcode-plans/")
}
fn path_is_docs_only(path: &str) -> bool {
let normalized = path.trim().replace('\\', "/");
let lower = normalized.to_ascii_lowercase();
let file_name = lower.rsplit('/').next().unwrap_or_default();
let stem = file_name.split('.').next().unwrap_or_default();
if matches!(stem, "readme" | "changelog" | "license") {
let ext = file_name.rsplit('.').next().unwrap_or_default();
if ext == stem || matches!(ext, "md" | "mdx" | "txt" | "rst") {
return true;
}
}
matches!(file_name.rsplit('.').next(), Some("md" | "mdx" | "txt" | "rst"))
}
pub(crate) fn is_docs_only_write(name: &str, args: &serde_json::Value) -> bool {
use vtcode_core::config::constants::tools as tool_names;
use vtcode_core::tools::names::canonical_tool_name;
use vtcode_core::tools::tool_intent::file_operation_action;
let canonical = canonical_tool_name(name);
let is_file_write = matches!(
canonical,
tool_names::APPLY_PATCH
| tool_names::WRITE_FILE
| tool_names::EDIT_FILE
| tool_names::CREATE_FILE
| tool_names::SEARCH_REPLACE
| tool_names::DELETE_FILE
| tool_names::MOVE_FILE
| tool_names::COPY_FILE
| tool_names::UNIFIED_FILE
);
if !is_file_write {
return false;
}
if canonical == tool_names::UNIFIED_FILE
&& file_operation_action(args)
.map(|action| action.eq_ignore_ascii_case("read"))
.unwrap_or(false)
{
return false;
}
let mut paths = vtcode_core::tools::apply_patch::mutation_target_paths(canonical, args);
if let Some(extra) = args.get("filePath").and_then(|value| value.as_str()) {
let trimmed = extra.trim();
if !trimmed.is_empty() {
paths.push(PathBuf::from(trimmed));
}
}
if paths.is_empty() {
return false;
}
paths.iter().all(|path| path.to_str().is_some_and(path_is_docs_only))
}
pub(crate) fn is_plan_artifact_write(name: &str, args: &serde_json::Value) -> bool {
use vtcode_core::config::constants::tools as tool_names;
use vtcode_core::tools::names::canonical_tool_name;
use vtcode_core::tools::tool_intent::file_operation_action;
let canonical = canonical_tool_name(name);
match canonical {
tool_names::TASK_TRACKER => true,
tool_names::UNIFIED_FILE => {
if !file_operation_action(args)
.map(|action| action.eq_ignore_ascii_case("read"))
.unwrap_or(false)
{
[
"path",
"file_path",
"filepath",
"filePath",
"target_path",
"destination",
"destination_path",
]
.iter()
.filter_map(|key| args.get(*key).and_then(|value| value.as_str()))
.any(path_targets_plan_artifact)
} else {
false
}
}
tool_names::WRITE_FILE | tool_names::EDIT_FILE | tool_names::CREATE_FILE | tool_names::SEARCH_REPLACE => {
["path", "file_path", "filepath", "filePath"]
.iter()
.filter_map(|key| args.get(*key).and_then(|value| value.as_str()))
.any(path_targets_plan_artifact)
}
_ => false,
}
}
fn is_execution_tool(name: &str) -> bool {
use vtcode_core::config::constants::tools as tool_names;
matches!(
name,
tool_names::UNIFIED_EXEC
| tool_names::EXEC_COMMAND
| tool_names::EXEC_PTY_CMD
| tool_names::RUN_PTY_CMD
| tool_names::EXECUTE_CODE
| tool_names::SHELL
)
}
pub(crate) fn mutation_blocked_until_verification(
loop_tracker: &LoopTracker,
name: &str,
args: &serde_json::Value,
) -> bool {
if !loop_tracker.verification_is_pending() || is_plan_artifact_write(name, args) || is_docs_only_write(name, args) {
return false;
}
let canonical_name = canonical_tool_name(name);
if vtcode_core::tools::tool_intent::is_exec_session_cleanup_call(canonical_name, args) {
return false;
}
if is_execution_tool(canonical_name) {
let executed = shell_args_as_executed(canonical_name, args);
let activity = classify_shell_activity(canonical_name, &executed);
if !matches!(activity, ShellActivity::Mutation) || shell_command_is_admitted_verification_attempt(&executed) {
return false;
}
return loop_tracker.fix_edits_remaining == 0;
}
if !vtcode_core::tools::tool_intent::classify_tool_intent(canonical_name, args).mutating {
return false;
}
loop_tracker.fix_edits_remaining == 0
}
pub(crate) fn update_repetition_tracker(
loop_tracker: &mut LoopTracker,
outcome: &ToolPipelineOutcome,
name: &str,
args: &serde_json::Value,
) -> bool {
if matches!(&outcome.status, ToolExecutionStatus::Cancelled) {
return false;
}
let canonical_name = canonical_tool_name(name);
let signature_key = signature_key_for(canonical_name, args);
loop_tracker.record(signature_key.clone());
let session_id = vtcode_core::tools::command_args::session_id_text(args);
let targets_pending_verifier = loop_tracker
.pending_verifier_session_id
.as_deref()
.is_some_and(|pending_id| session_id == Some(pending_id));
let targets_diagnostic_checker = is_session_follow_up(canonical_name, args)
&& session_id.is_some_and(|id| loop_tracker.pending_checker_session_ids.contains(id));
let session_result_lost = matches!(&outcome.status, ToolExecutionStatus::Failure { error }
if error.is_exec_session_not_found() || error_text_indicates_lost_session(&error.message));
if vtcode_core::tools::tool_intent::is_exec_session_cleanup_call(canonical_name, args) {
if matches!(&outcome.status, ToolExecutionStatus::Success { .. }) && targets_pending_verifier {
loop_tracker.pending_verifier_session_id = None;
}
if (matches!(&outcome.status, ToolExecutionStatus::Success { .. }) || session_result_lost)
&& let Some(session_id) = session_id
{
loop_tracker.pending_checker_session_ids.remove(session_id);
}
return false;
}
if targets_diagnostic_checker {
let completed = matches!(&outcome.status, ToolExecutionStatus::Success { output, .. }
if output.get("exit_code").and_then(serde_json::Value::as_i64).is_some());
if (completed || session_result_lost)
&& let Some(session_id) = session_id
{
loop_tracker.pending_checker_session_ids.remove(session_id);
}
if completed {
loop_tracker.piped_verification_notice_pending = true;
}
if session_result_lost && !targets_pending_verifier {
return false;
}
}
if is_session_follow_up(canonical_name, args)
&& targets_pending_verifier
&& let ToolExecutionStatus::Success { output, .. } = &outcome.status
&& let Some(exit_code) = output.get("exit_code").and_then(serde_json::Value::as_i64)
{
if exit_code == 0 {
loop_tracker.mark_verification_complete();
} else {
loop_tracker.record_failed_verification();
}
loop_tracker.reset_navigation_window(true);
return exit_code != 0;
}
let navigation_family =
crate::agent::runloop::unified::turn::tool_outcomes::handlers::low_signal_family_key(canonical_name, args);
let low_signal_family = navigation_family
.clone()
.filter(|_| is_low_signal_outcome(outcome, canonical_name, args));
let navigation_signature_key = navigation_family
.map(|family| format!("navigation::{family}"))
.unwrap_or_else(|| signature_key.clone());
let coarse_family = coarse_inspection_family_key(canonical_name, args);
let coarse_repeat = coarse_family.as_ref().map(|family| {
let entry = loop_tracker
.coarse_inspection_attempts
.entry(family.clone())
.or_insert((0, Instant::now()));
entry.0 = entry.0.saturating_add(1);
entry.1 = Instant::now();
entry.0
});
let is_coarse_duplicate =
coarse_repeat.is_some_and(|count| count >= 3) && matches!(&outcome.status, ToolExecutionStatus::Success { .. });
let mut low_signal_family = low_signal_family;
if let Some(evidence) = navigation_evidence(outcome, canonical_name, args) {
if loop_tracker.record_navigation_evidence(&evidence.signatures) {
low_signal_family = Some("navigation::retained_evidence".to_string());
}
if let Some(path) = evidence.focused_search_path {
loop_tracker.record_focused_search(path);
}
}
if low_signal_family.is_none() && is_coarse_duplicate {
low_signal_family = coarse_family.clone();
}
let is_low_signal_navigation = low_signal_family.is_some();
if let Some(low_signal_family) = low_signal_family.as_ref() {
loop_tracker.record_low_signal(low_signal_family.clone());
}
if is_session_follow_up(canonical_name, args)
&& loop_tracker.verification_is_pending()
&& (loop_tracker.pending_verifier_session_id.is_none() || targets_pending_verifier)
&& session_result_lost
{
loop_tracker.verification_result_lost_notice_pending = true;
loop_tracker.record_failed_verification();
loop_tracker.reset_navigation_window(low_signal_family.is_none());
return true;
}
if is_execution_tool(canonical_name) {
let executed = shell_args_as_executed(canonical_name, args);
match classify_shell_activity(canonical_name, &executed) {
ShellActivity::Inspection => {
loop_tracker.consecutive_navigations = loop_tracker.consecutive_navigations.saturating_add(1);
loop_tracker.nav_signatures.insert(navigation_signature_key);
loop_tracker.record_navigation_signal(is_low_signal_navigation);
}
ShellActivity::Verification => {
loop_tracker.clear_navigation_evidence();
if let ToolExecutionStatus::Success { output, .. } = &outcome.status
&& output.get("exit_code").and_then(serde_json::Value::as_i64).is_none()
&& let Some(session_id) = output.get("session_id").and_then(serde_json::Value::as_str)
{
loop_tracker.mark_verification_pending();
loop_tracker.fix_edits_remaining = 0;
loop_tracker.pending_verifier_session_id = Some(session_id.to_owned());
loop_tracker.reset_navigation_window(true);
return false;
}
if matches!(&outcome.status, ToolExecutionStatus::Success { output, .. } if output.get("exit_code").and_then(serde_json::Value::as_i64) == Some(0))
{
loop_tracker.mark_verification_complete();
} else if matches!(&outcome.status, ToolExecutionStatus::Success { command_success: false, .. }) {
loop_tracker.record_failed_verification();
loop_tracker.reset_navigation_window(low_signal_family.is_none());
return true;
} else if loop_tracker.verification_is_pending()
&& matches!(
&outcome.status,
ToolExecutionStatus::Failure { error } | ToolExecutionStatus::Timeout { error }
if !matches!(error.error_type, vtcode_core::tools::registry::ToolErrorType::InvalidParameters
| vtcode_core::tools::registry::ToolErrorType::PermissionDenied
| vtcode_core::tools::registry::ToolErrorType::PolicyViolation
| vtcode_core::tools::registry::ToolErrorType::ToolNotFound)
)
{
loop_tracker.verification_result_lost_notice_pending = true;
loop_tracker.record_failed_verification();
loop_tracker.reset_navigation_window(low_signal_family.is_none());
return true;
}
loop_tracker.reset_navigation_window(low_signal_family.is_none());
}
ShellActivity::Mutation => {
if shell_command_contains_verifier(&executed)
&& let ToolExecutionStatus::Success { output, .. } = &outcome.status
{
if output.get("exit_code").and_then(serde_json::Value::as_i64).is_some() {
loop_tracker.piped_verification_notice_pending = true;
} else if let Some(session_id) = vtcode_core::tools::command_args::session_id_text(output) {
loop_tracker.pending_checker_session_ids.insert(session_id.into());
}
}
if shell_command_is_admitted_verification_attempt(&executed) {
let ran_and_failed =
matches!(&outcome.status, ToolExecutionStatus::Success { command_success: false, .. });
if ran_and_failed {
loop_tracker.record_failed_verification();
loop_tracker.reset_navigation_window(low_signal_family.is_none());
return true;
}
if loop_tracker.verification_is_pending()
&& matches!(&outcome.status, ToolExecutionStatus::Success { command_success: true, .. })
{
loop_tracker.reset_navigation_window(low_signal_family.is_none());
return true;
}
loop_tracker.reset_navigation_window(low_signal_family.is_none());
} else {
if mutation_was_applied(outcome) {
loop_tracker.record_successful_mutation();
}
loop_tracker.reset_navigation_window(low_signal_family.is_none());
}
}
}
} else if is_plan_artifact_write(canonical_name, args) || is_docs_only_write(canonical_name, args) {
if is_docs_only_write(canonical_name, args) && mutation_was_applied(outcome) {
loop_tracker.clear_navigation_evidence();
}
loop_tracker.reset_navigation_window(false);
} else {
let intent = vtcode_core::tools::tool_intent::classify_tool_intent(canonical_name, args);
if intent.mutating {
if mutation_was_applied(outcome) {
loop_tracker.record_successful_mutation();
}
loop_tracker.reset_navigation_window(low_signal_family.is_none());
} else {
loop_tracker.consecutive_navigations += 1;
loop_tracker.nav_signatures.insert(navigation_signature_key);
loop_tracker.record_navigation_signal(is_low_signal_navigation);
}
}
false
}
fn mutation_was_applied(outcome: &ToolPipelineOutcome) -> bool {
match &outcome.status {
ToolExecutionStatus::Success { output, command_success, modified_files, .. } => {
if let Some(effective_change) = vtcode_core::tools::file_ops::diff_output_has_effective_change(output) {
return effective_change;
}
*command_success || !modified_files.is_empty()
}
ToolExecutionStatus::Failure { .. } | ToolExecutionStatus::Timeout { .. } | ToolExecutionStatus::Cancelled => {
false
}
}
}
pub(crate) fn serialize_output(output: &serde_json::Value) -> String {
if let Some(s) = output.as_str() {
s.to_string()
} else {
serde_json::to_string(output).unwrap_or_else(|_| "{}".to_string())
}
}
pub(crate) fn check_is_argument_error(error_str: &str) -> bool {
error_str.contains("Missing required")
|| error_str.contains("Invalid arguments")
|| error_str.contains("Tool argument validation failed")
|| error_str.contains("required path parameter")
|| error_str.contains("is required for '")
|| error_str.contains("is required for \"")
|| error_str.contains("'index' is required")
|| error_str.contains("'index_path' is required")
|| error_str.contains("'status' is required")
|| error_str.contains("expected ")
|| error_str.contains("Expected:")
}