Skip to main content

vtcode_core/tools/
error_messages.rs

1/// Centralized error messages for tool operations.
2///
3/// This module provides consistent, reusable error messages across tool
4/// implementations to ensure uniformity in user-facing error reporting.
5///
6/// Only actively-used message groups are retained. Submodules that were
7/// defined but never referenced by tool implementations have been removed.
8pub mod agent_execution {
9    /// Marker used when planning workflow blocks a mutating tool call.
10    pub const PLANNING_DENIED_CONTEXT: &str = "tool denied by planning workflow";
11    /// Prefix for loop detection failures. Classify a failure with
12    /// [`is_loop_detection_block_message`], which matches the whole generated
13    /// shape, rather than searching for this prefix inside arbitrary text.
14    pub const LOOP_DETECTION_PREFIX: &str = "Loop detection";
15    const LOOP_BLOCK_TOOL_OPEN: &str = ": Tool '";
16    const LOOP_BLOCK_CALLED: &str = "' has been called ";
17    const LOOP_BLOCK_BLOCKED: &str = " times with identical parameters and is now blocked.";
18    /// Canonical line stating the consequence of a loop detection block.
19    pub const LOOP_RETRY_BLOCKED_LINE: &str =
20        "The call was not executed, and repeating it with the same parameters will be blocked again.";
21
22    /// Build the canonical Planning workflow denial message.
23    pub fn planning_workflow_denial_message(tool_name: &str) -> String {
24        format!(
25            "Tool '{tool_name}' execution failed: tool denied by planning workflow\n\n\
26             This call is not a recognized read-only planning action, so it is blocked during planning.\n\n\
27             Available during planning:\n\
28             - Read files: exec_command with readonly shell inspection commands such as sed, rg, ls, find, and git show\n\
29             - Run recognized commands: cargo check, cargo nextest run, git status, command -v, ls, grep, find, diff\n\
30             - Search code: exec_command with rg or other readonly search commands\n\
31             - Use task_tracker\n\n\
32             To start implementation:\n\
33             1. Wait for the user to review and approve the plan in the UI\n\
34             2. After approval, use apply_patch for file edits\n\n\
35             Fallback if automatic planning handoff keeps failing: type `implement` to present the plan again."
36        )
37    }
38
39    /// Build the canonical loop-detection block message.
40    pub fn loop_detection_block_message(tool_name: &str, repeat_count: u64, original_error: Option<&str>) -> String {
41        let mut message = format!(
42            "{LOOP_DETECTION_PREFIX}{LOOP_BLOCK_TOOL_OPEN}{tool_name}{LOOP_BLOCK_CALLED}{repeat_count}{LOOP_BLOCK_BLOCKED}\n\n\
43             {LOOP_RETRY_BLOCKED_LINE}\n\n\
44             If you need the result from this tool:\n\
45             1. Check if you already have the result from a previous successful call in your conversation history\n\
46             2. If not available, use a different approach or modify your request"
47        );
48
49        if let Some(error) = original_error {
50            message.push_str("\n\nOriginal error: ");
51            message.push_str(error);
52        }
53
54        message
55    }
56
57    /// Whether `message` is a block message built by
58    /// [`loop_detection_block_message`]: it must start with the exact
59    /// generated opening, `Loop detection: Tool '<name>' has been called <n>
60    /// times with identical parameters and is now blocked.`, so tool stderr
61    /// or prose that merely mentions loop detection is not classified.
62    pub fn is_loop_detection_block_message(message: &str) -> bool {
63        let Some(rest) = message
64            .strip_prefix(LOOP_DETECTION_PREFIX)
65            .and_then(|rest| rest.strip_prefix(LOOP_BLOCK_TOOL_OPEN))
66        else {
67            return false;
68        };
69        let Some((tool_name, rest)) = rest.split_once(LOOP_BLOCK_CALLED) else {
70            return false;
71        };
72        let count_len = rest.len() - rest.trim_start_matches(|c: char| c.is_ascii_digit()).len();
73        !tool_name.is_empty() && count_len > 0 && rest[count_len..].starts_with(LOOP_BLOCK_BLOCKED)
74    }
75
76    /// Check whether an error string corresponds to planning workflow denial.
77    pub fn is_planning_active_denial(error: &str) -> bool {
78        error.contains(PLANNING_DENIED_CONTEXT)
79    }
80
81    /// Recovery guidance for policy rejection, distinct from malformed JSON.
82    pub fn preflight_policy_correction(error: &str) -> Option<&'static str> {
83        if is_planning_active_denial(error) {
84            return Some(
85                "Planning permits only recognized read-only command shapes. Do not retry the same denied command. Use sed, rg, git show, or command -v for inspection; defer unknown scripts until plan approval. This denial does not establish that the script itself mutates files.",
86            );
87        }
88        let lowered = error.to_ascii_lowercase();
89        if lowered.contains("command security check failed") || lowered.contains("command injection pattern detected") {
90            return Some(
91                "The shell command was rejected by security policy, not JSON validation. Do not retry it unchanged or bypass the policy. Use a simpler literal command without dynamic expansion, or an approved dedicated tool; report the blocker if no permitted form satisfies the task.",
92            );
93        }
94        None
95    }
96}
97
98/// Skill management error messages
99pub mod skill_ops {
100    pub const SKILL_NOT_FOUND: &str = "Skill not found";
101    pub const SKILL_ALREADY_EXISTS: &str = "Skill already exists";
102    pub const INVALID_SKILL_FORMAT: &str = "Invalid skill format";
103    pub const SKILL_SAVE_FAILED: &str = "Failed to save skill";
104    pub const SKILL_LOAD_FAILED: &str = "Failed to load skill";
105
106    /// Build a formatted "skill not found" error message.
107    pub fn skill_not_found_error(name: &str) -> anyhow::Error {
108        anyhow::anyhow!("Skill '{name}' not found")
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    fn internal_unified_tool_name(suffix: &str) -> String {
117        format!("unified_{suffix}")
118    }
119
120    #[test]
121    fn test_agent_execution_message_helpers() {
122        let planning_msg = agent_execution::planning_workflow_denial_message("write_file");
123        assert!(agent_execution::is_planning_active_denial(&planning_msg));
124        assert!(planning_msg.contains("blocked during planning"));
125        assert!(planning_msg.contains("cargo check"));
126        assert!(planning_msg.contains("exec_command"));
127        assert!(planning_msg.contains("apply_patch"));
128        assert!(!planning_msg.contains(&internal_unified_tool_name("file")));
129        assert!(!planning_msg.contains(&internal_unified_tool_name("exec")));
130        assert!(!planning_msg.contains(&internal_unified_tool_name("search")));
131        assert!(!planning_msg.contains(&format!("/{}", "mode")));
132        assert!(!planning_msg.contains("DO NOT retry this tool or use /plan off"));
133
134        let loop_msg = agent_execution::loop_detection_block_message("read_file", 3, Some("base error"));
135        assert!(loop_msg.starts_with(agent_execution::LOOP_DETECTION_PREFIX));
136        assert!(loop_msg.contains(agent_execution::LOOP_RETRY_BLOCKED_LINE));
137        assert!(loop_msg.contains("Original error: base error"));
138        assert!(agent_execution::is_loop_detection_block_message(&loop_msg));
139    }
140
141    #[test]
142    fn loop_detection_classification_ignores_prose_mentions() {
143        use agent_execution::is_loop_detection_block_message;
144
145        let generated = agent_execution::loop_detection_block_message("mcp::fs::read", 12, None);
146        assert!(is_loop_detection_block_message(&generated));
147
148        for message in [
149            "warning: loop detection disabled for this target",
150            "Loop detection is off; running tests",
151            "stderr: Loop detection: Tool 'x' has been called 3 times with identical parameters and is now blocked.",
152            "Loop detection: Tool 'x' has been called many times with identical parameters and is now blocked.",
153            "Loop detection: Tool '' has been called 3 times with identical parameters and is now blocked.",
154            "LOOP DETECTION: Tool 'x' has been called 3 times with identical parameters and is now blocked.",
155        ] {
156            assert!(!is_loop_detection_block_message(message), "{message:?}");
157        }
158    }
159}