Skip to main content

mars_agents/build/
tool_normalize.rs

1pub struct NormalizedTool {
2    pub name: String,
3    pub status: ToolProjectionStatus,
4}
5
6#[derive(Clone, Copy, PartialEq, Eq)]
7pub enum ToolProjectionStatus {
8    Canonical,
9    Normalized,
10    Unknown,
11}
12
13pub fn normalize_tool_for_harness(raw: &str, target_harness: &str) -> NormalizedTool {
14    let trimmed = raw.trim();
15    if trimmed.is_empty() {
16        return NormalizedTool {
17            name: String::new(),
18            status: ToolProjectionStatus::Unknown,
19        };
20    }
21
22    let (head, payload) = match trimmed.find('(') {
23        Some(index) => (&trimmed[..index], &trimmed[index..]),
24        None => (trimmed, ""),
25    };
26
27    let canonical = canonical_tool_name(head, target_harness);
28    match canonical {
29        Some(value) => {
30            let status = if head == value {
31                ToolProjectionStatus::Canonical
32            } else {
33                ToolProjectionStatus::Normalized
34            };
35            NormalizedTool {
36                name: format!("{value}{payload}"),
37                status,
38            }
39        }
40        None => NormalizedTool {
41            name: trimmed.to_string(),
42            status: ToolProjectionStatus::Unknown,
43        },
44    }
45}
46
47pub fn is_first_class_harness(harness: &str) -> bool {
48    matches!(
49        harness.trim().to_ascii_lowercase().as_str(),
50        "claude" | "codex" | "opencode"
51    )
52}
53
54fn canonical_tool_name(head: &str, target_harness: &str) -> Option<&'static str> {
55    let key = head.trim().to_ascii_lowercase();
56    if key.is_empty() {
57        return None;
58    }
59
60    match target_harness.trim().to_ascii_lowercase().as_str() {
61        "claude" => canonical_claude_tool(&key),
62        "codex" => canonical_codex_tool(&key),
63        "opencode" => canonical_opencode_tool(&key),
64        "cursor" | "pi" => canonical_generic_tool(&key),
65        _ => canonical_generic_tool(&key),
66    }
67}
68
69fn canonical_claude_tool(key: &str) -> Option<&'static str> {
70    match key {
71        "bash" | "shell" | "terminal" => Some("Bash"),
72        "read" | "cat" | "view" => Some("Read"),
73        "write" => Some("Write"),
74        "edit" | "sed" => Some("Edit"),
75        "agent" | "subagent" => Some("Agent"),
76        "glob" | "find" => Some("Glob"),
77        "grep" | "search" | "rg" => Some("Grep"),
78        "notebook" | "jupyter" => Some("Notebook"),
79        "task" | "task_tool" => Some("Task"),
80        "web_search" | "websearch" => Some("WebSearch"),
81        "web_fetch" | "webfetch" => Some("WebFetch"),
82        "todo_read" | "todoread" => Some("TodoRead"),
83        "todo_write" | "todowrite" => Some("TodoWrite"),
84        _ => None,
85    }
86}
87
88fn canonical_codex_tool(key: &str) -> Option<&'static str> {
89    match key {
90        "shell" | "bash" | "terminal" => Some("shell"),
91        "file_read" | "read" | "cat" => Some("file_read"),
92        "file_write" | "write" | "edit" => Some("file_write"),
93        "agent" | "subagent" => Some("agent"),
94        _ => None,
95    }
96}
97
98fn canonical_opencode_tool(key: &str) -> Option<&'static str> {
99    match key {
100        "bash" | "shell" | "terminal" => Some("bash"),
101        "read" | "cat" => Some("read"),
102        "write" => Some("write"),
103        "edit" => Some("edit"),
104        "agent" | "subagent" => Some("agent"),
105        "browser" | "web_search" | "websearch" => Some("browser"),
106        "fetch" | "web_fetch" | "webfetch" => Some("fetch"),
107        _ => None,
108    }
109}
110
111fn canonical_generic_tool(key: &str) -> Option<&'static str> {
112    match key {
113        "bash" => Some("Bash"),
114        "read" => Some("Read"),
115        "write" => Some("Write"),
116        "edit" => Some("Edit"),
117        "agent" => Some("Agent"),
118        _ => None,
119    }
120}