Skip to main content

pi/core/tools/
mod.rs

1//! Shared infrastructure and seven concrete coding tools.
2//!
3//! Ports `.references/pi/packages/coding-agent/src/core/tools/`: the ordered
4//! built-in registry, read/bash/edit/write/grep/find/ls implementations,
5//! truncation, rolling-output spill, per-realpath mutation serialization, and
6//! path resolution.
7
8pub 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/// Name of one of the seven built-in coding tools (TypeScript `ToolName`).
62///
63/// Wire form is the lowercase string used in settings and session activation
64/// lists (`"read"`, `"bash"`, …).
65#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
66#[serde(rename_all = "lowercase")]
67pub enum ToolName {
68    /// Read file contents (and images).
69    Read,
70    /// Execute a bash command.
71    Bash,
72    /// Exact multi-edit of a single file.
73    Edit,
74    /// Create or overwrite a file.
75    Write,
76    /// Search file contents (ripgrep).
77    Grep,
78    /// Find files by glob (fd).
79    Find,
80    /// List a directory.
81    Ls,
82}
83
84/// Full registry order (TypeScript `allToolNames` insertion order and
85/// `createAllToolDefinitions` key order): read → bash → edit → write → grep
86/// → find → ls.
87pub 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
97/// Default session-active tools (TypeScript `defaultActiveToolNames` /
98/// `createCodingToolDefinitions`): read, bash, edit, write. The full
99/// registry still contains grep/find/ls; they stay inactive until activated.
100pub const DEFAULT_ACTIVE_TOOL_NAMES: [ToolName; 4] = [
101    ToolName::Read,
102    ToolName::Bash,
103    ToolName::Edit,
104    ToolName::Write,
105];
106
107/// Read-only tool set (TypeScript `createReadOnlyToolDefinitions`): read,
108/// grep, find, ls.
109pub const READ_ONLY_TOOL_NAMES: [ToolName; 4] =
110    [ToolName::Read, ToolName::Grep, ToolName::Find, ToolName::Ls];
111
112/// Builds all seven tools in provider-visible registry order.
113#[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/// Builds the four tools enabled by default for coding sessions.
128#[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/// Builds the read-only tool set in registry order.
140#[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    /// Lowercase wire name matching the TypeScript discriminant.
153    #[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    /// Whether this tool is part of the default-active coding set.
167    #[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    /// Whether this tool is part of the read-only set.
173    #[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/// Error returned by [`ToolName`]'s [`FromStr`] implementation. Display text
186/// matches TypeScript `Unknown tool name: ${toolName}` exactly.
187#[derive(Clone, Debug, Eq, PartialEq)]
188pub struct UnknownToolName(String);
189
190impl UnknownToolName {
191    /// The unrecognized tool name string.
192    #[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        // Unknown variants are rejected by serde (settings validation).
308        let bad: Result<ToolName, _> = serde_json::from_str("\"unknown\"");
309        assert!(bad.is_err());
310        Ok(())
311    }
312}