mars_agents/build/
tool_normalize.rs1pub 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}