1pub mod bash;
9pub mod edit;
10pub mod edit_diff;
11pub mod find;
12pub mod grep;
13pub mod ls;
14pub mod mutation_queue;
15pub mod output_accumulator;
16pub mod path_utils;
17pub mod read;
18pub mod truncate;
19pub mod write;
20
21pub use bash::{
22 BashOperations, BashSpawnContext, BashSpawnHook, BashTool, BashToolDetails, BashToolInput,
23 BashToolOptions, LocalBashOperations, create_bash_tool,
24};
25pub use edit::{
26 EditTool, EditToolDetails, EditToolInput, EditToolOptions, ReplaceEditInput, create_edit_tool,
27};
28pub use find::{FindTool, FindToolDetails, FindToolInput, FindToolOptions, create_find_tool};
29pub use grep::{GrepTool, GrepToolDetails, GrepToolInput, GrepToolOptions, create_grep_tool};
30pub use ls::{LsTool, LsToolDetails, LsToolInput, LsToolOptions, create_ls_tool};
31pub use mutation_queue::{MutationQueueError, with_file_mutation_queue};
32pub use output_accumulator::{
33 DEFAULT_TEMP_FILE_PREFIX, OutputAccumulator, OutputAccumulatorError, OutputAccumulatorOptions,
34 OutputSnapshot,
35};
36pub use path_utils::{
37 PathResolveError, expand_path, path_exists, resolve_read_path, resolve_read_path_async,
38 resolve_to_cwd,
39};
40pub use read::{
41 ReadTool, ReadToolDetails, ReadToolInput, ReadToolOptions, create_read_tool,
42 detect_supported_image_mime_type,
43};
44pub use truncate::{
45 DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, GREP_MAX_LINE_LENGTH, TruncatedBy, TruncatedLine,
46 TruncationOptions, TruncationResult, format_size, truncate_head, truncate_line,
47 truncate_line_with, truncate_tail,
48};
49pub use write::{WriteTool, WriteToolDetails, WriteToolInput, WriteToolOptions, create_write_tool};
50
51use std::path::Path;
52use std::sync::Arc;
53
54use pi_agent::AgentTool;
55use std::error::Error;
56use std::fmt;
57use std::str::FromStr;
58
59use serde::{Deserialize, Serialize};
60
61#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
66#[serde(rename_all = "lowercase")]
67pub enum ToolName {
68 Read,
70 Bash,
72 Edit,
74 Write,
76 Grep,
78 Find,
80 Ls,
82}
83
84pub const ALL_TOOL_NAMES: [ToolName; 7] = [
88 ToolName::Read,
89 ToolName::Bash,
90 ToolName::Edit,
91 ToolName::Write,
92 ToolName::Grep,
93 ToolName::Find,
94 ToolName::Ls,
95];
96
97pub const DEFAULT_ACTIVE_TOOL_NAMES: [ToolName; 4] = [
101 ToolName::Read,
102 ToolName::Bash,
103 ToolName::Edit,
104 ToolName::Write,
105];
106
107pub const READ_ONLY_TOOL_NAMES: [ToolName; 4] =
110 [ToolName::Read, ToolName::Grep, ToolName::Find, ToolName::Ls];
111
112#[must_use]
114pub fn create_all_tool_definitions(cwd: impl AsRef<Path>) -> [Arc<dyn AgentTool>; 7] {
115 let cwd = cwd.as_ref();
116 [
117 create_read_tool(cwd),
118 create_bash_tool(cwd),
119 create_edit_tool(cwd),
120 create_write_tool(cwd),
121 create_grep_tool(cwd),
122 create_find_tool(cwd),
123 create_ls_tool(cwd),
124 ]
125}
126
127#[must_use]
129pub fn create_coding_tool_definitions(cwd: impl AsRef<Path>) -> [Arc<dyn AgentTool>; 4] {
130 let cwd = cwd.as_ref();
131 [
132 create_read_tool(cwd),
133 create_bash_tool(cwd),
134 create_edit_tool(cwd),
135 create_write_tool(cwd),
136 ]
137}
138
139#[must_use]
141pub fn create_read_only_tool_definitions(cwd: impl AsRef<Path>) -> [Arc<dyn AgentTool>; 4] {
142 let cwd = cwd.as_ref();
143 [
144 create_read_tool(cwd),
145 create_grep_tool(cwd),
146 create_find_tool(cwd),
147 create_ls_tool(cwd),
148 ]
149}
150
151impl ToolName {
152 #[must_use]
154 pub const fn as_str(self) -> &'static str {
155 match self {
156 Self::Read => "read",
157 Self::Bash => "bash",
158 Self::Edit => "edit",
159 Self::Write => "write",
160 Self::Grep => "grep",
161 Self::Find => "find",
162 Self::Ls => "ls",
163 }
164 }
165
166 #[must_use]
168 pub const fn is_default_active(self) -> bool {
169 matches!(self, Self::Read | Self::Bash | Self::Edit | Self::Write)
170 }
171
172 #[must_use]
174 pub const fn is_read_only(self) -> bool {
175 matches!(self, Self::Read | Self::Grep | Self::Find | Self::Ls)
176 }
177}
178
179impl fmt::Display for ToolName {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 f.write_str(self.as_str())
182 }
183}
184
185#[derive(Clone, Debug, Eq, PartialEq)]
188pub struct UnknownToolName(String);
189
190impl UnknownToolName {
191 #[must_use]
193 pub fn name(&self) -> &str {
194 &self.0
195 }
196}
197
198impl fmt::Display for UnknownToolName {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 write!(f, "Unknown tool name: {}", self.0)
201 }
202}
203
204impl Error for UnknownToolName {}
205
206impl FromStr for ToolName {
207 type Err = UnknownToolName;
208
209 fn from_str(value: &str) -> Result<Self, Self::Err> {
210 match value {
211 "read" => Ok(Self::Read),
212 "bash" => Ok(Self::Bash),
213 "edit" => Ok(Self::Edit),
214 "write" => Ok(Self::Write),
215 "grep" => Ok(Self::Grep),
216 "find" => Ok(Self::Find),
217 "ls" => Ok(Self::Ls),
218 other => Err(UnknownToolName(other.to_owned())),
219 }
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226
227 type TestResult = Result<(), Box<dyn Error + Send + Sync>>;
228
229 #[test]
230 fn registry_order_is_read_bash_edit_write_grep_find_ls() {
231 assert_eq!(
232 ALL_TOOL_NAMES,
233 [
234 ToolName::Read,
235 ToolName::Bash,
236 ToolName::Edit,
237 ToolName::Write,
238 ToolName::Grep,
239 ToolName::Find,
240 ToolName::Ls,
241 ]
242 );
243 assert_eq!(
244 ALL_TOOL_NAMES.map(ToolName::as_str),
245 ["read", "bash", "edit", "write", "grep", "find", "ls"]
246 );
247 }
248
249 #[test]
250 fn default_active_is_read_bash_edit_write() {
251 assert_eq!(
252 DEFAULT_ACTIVE_TOOL_NAMES,
253 [
254 ToolName::Read,
255 ToolName::Bash,
256 ToolName::Edit,
257 ToolName::Write,
258 ]
259 );
260 for name in ALL_TOOL_NAMES {
261 assert_eq!(
262 name.is_default_active(),
263 DEFAULT_ACTIVE_TOOL_NAMES.contains(&name)
264 );
265 }
266 }
267
268 #[test]
269 fn read_only_is_read_grep_find_ls() {
270 assert_eq!(
271 READ_ONLY_TOOL_NAMES,
272 [ToolName::Read, ToolName::Grep, ToolName::Find, ToolName::Ls,]
273 );
274 for name in ALL_TOOL_NAMES {
275 assert_eq!(name.is_read_only(), READ_ONLY_TOOL_NAMES.contains(&name));
276 }
277 }
278
279 #[test]
280 fn from_str_and_display_roundtrip() -> TestResult {
281 for name in ALL_TOOL_NAMES {
282 let parsed: ToolName = name.as_str().parse()?;
283 assert_eq!(parsed, name);
284 assert_eq!(parsed.to_string(), name.as_str());
285 }
286 Ok(())
287 }
288
289 #[test]
290 fn unknown_tool_name_error_matches_typescript() -> TestResult {
291 let Err(err) = "foo".parse::<ToolName>() else {
292 return Err(std::io::Error::other("foo must not parse as a tool name").into());
293 };
294 assert_eq!(err.to_string(), "Unknown tool name: foo");
295 assert_eq!(err.name(), "foo");
296 Ok(())
297 }
298
299 #[test]
300 fn serde_roundtrip_uses_lowercase_wire_names() -> TestResult {
301 for name in ALL_TOOL_NAMES {
302 let json = serde_json::to_string(&name)?;
303 assert_eq!(json, format!("\"{}\"", name.as_str()));
304 let parsed: ToolName = serde_json::from_str(&json)?;
305 assert_eq!(parsed, name);
306 }
307 let bad: Result<ToolName, _> = serde_json::from_str("\"unknown\"");
309 assert!(bad.is_err());
310 Ok(())
311 }
312}