Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:34:29.601Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

find_path_tool.rs

276 lines · 9.1 KB · rust
1use crate::{AgentTool, ToolCallEventStream, ToolInput};
2use acp_thread::MentionUri;
3use agent_client_protocol::schema::v1 as acp;
4use anyhow::{Result, anyhow};
5use futures::FutureExt as _;
6use gpui::{App, AppContext, Entity, SharedString, Task};
7use language_model::LanguageModelToolResultContent;
8use project::Project;
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11use std::fmt::Write;
12use std::{cmp, path::PathBuf, sync::Arc};
13use util::paths::PathMatcher;
14
15/// Find file paths that match a given pattern.
16///
17/// - Supports glob patterns like "**/*.js" or "src/**/*.ts"
18/// - Returns matching file paths sorted alphabetically
19/// - Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths.
20/// - Use this tool when you need to find files by name patterns
21/// - Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages.
22#[derive(Debug, Serialize, Deserialize, JsonSchema)]
23pub struct FindPathToolInput {
24    /// The glob to match against every path in the project.
25    ///
26    /// <example>
27    /// If the project has the following root directories:
28    ///
29    /// - directory1/a/something.txt
30    /// - directory2/a/things.txt
31    /// - directory3/a/other.txt
32    ///
33    /// You can get back the first two paths by providing a glob of "*thing*.txt"
34    /// </example>
35    pub glob: String,
36    /// Optional starting position for paginated results (0-based).
37    /// When not provided, starts from the beginning.
38    #[serde(default)]
39    pub offset: usize,
40}
41
42#[derive(Debug, Serialize, Deserialize)]
43#[serde(untagged)]
44pub enum FindPathToolOutput {
45    Success {
46        offset: usize,
47        current_matches_page: Vec<PathBuf>,
48        all_matches_len: usize,
49    },
50    Error {
51        error: String,
52    },
53}
54
55impl From<FindPathToolOutput> for LanguageModelToolResultContent {
56    fn from(output: FindPathToolOutput) -> Self {
57        match output {
58            FindPathToolOutput::Success {
59                offset,
60                current_matches_page,
61                all_matches_len,
62            } => {
63                if current_matches_page.is_empty() {
64                    "No matches found".into()
65                } else {
66                    let mut llm_output = format!("Found {} total matches.", all_matches_len);
67                    if all_matches_len > RESULTS_PER_PAGE {
68                        write!(
69                            &mut llm_output,
70                            "\nShowing results {}-{} (provide 'offset' parameter for more results):",
71                            offset + 1,
72                            offset + current_matches_page.len()
73                        )
74                        .ok();
75                    }
76
77                    for mat in current_matches_page {
78                        write!(&mut llm_output, "\n{}", mat.display()).ok();
79                    }
80
81                    llm_output.into()
82                }
83            }
84            FindPathToolOutput::Error { error } => error.into(),
85        }
86    }
87}
88
89const RESULTS_PER_PAGE: usize = 50;
90
91pub struct FindPathTool {
92    project: Entity<Project>,
93}
94
95impl FindPathTool {
96    pub fn new(project: Entity<Project>) -> Self {
97        Self { project }
98    }
99}
100
101impl AgentTool for FindPathTool {
102    type Input = FindPathToolInput;
103    type Output = FindPathToolOutput;
104
105    const NAME: &'static str = "find_path";
106
107    fn kind() -> acp::ToolKind {
108        acp::ToolKind::Search
109    }
110
111    fn initial_title(
112        &self,
113        input: Result<Self::Input, serde_json::Value>,
114        _cx: &mut App,
115    ) -> SharedString {
116        let mut title = "Find paths".to_string();
117        if let Ok(input) = input {
118            title.push_str(&format!(" matching “`{}`”", input.glob));
119        }
120        title.into()
121    }
122
123    fn run(
124        self: Arc<Self>,
125        input: ToolInput<Self::Input>,
126        event_stream: ToolCallEventStream,
127        cx: &mut App,
128    ) -> Task<Result<Self::Output, Self::Output>> {
129        let project = self.project.clone();
130        cx.spawn(async move |cx| {
131            let input = input.recv().await.map_err(|e| FindPathToolOutput::Error {
132                error: e.to_string(),
133            })?;
134
135            let search_paths_task = cx.update(|cx| search_paths(&input.glob, project, cx));
136
137            let matches = futures::select! {
138                result = search_paths_task.fuse() => result.map_err(|e| FindPathToolOutput::Error { error: e.to_string() })?,
139                _ = event_stream.cancelled_by_user().fuse() => {
140                    return Err(FindPathToolOutput::Error { error: "Path search cancelled by user".to_string() });
141                }
142            };
143            let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len())
144                ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
145
146            event_stream.update_fields(
147                acp::ToolCallUpdateFields::new()
148                    .title(if paginated_matches.is_empty() {
149                        "No matches".into()
150                    } else if paginated_matches.len() == 1 {
151                        "1 match".into()
152                    } else {
153                        format!("{} matches", paginated_matches.len())
154                    })
155                    .content(
156                        paginated_matches
157                            .iter()
158                            .map(|path| {
159                                let uri = MentionUri::File {
160                                    abs_path: path.clone(),
161                                };
162                                acp::ToolCallContent::Content(acp::Content::new(
163                                    acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
164                                        path.to_string_lossy(),
165                                        uri.to_uri().to_string(),
166                                    )),
167                                ))
168                            })
169                            .collect::<Vec<_>>(),
170                    ),
171            );
172
173            Ok(FindPathToolOutput::Success {
174                offset: input.offset,
175                current_matches_page: paginated_matches.to_vec(),
176                all_matches_len: matches.len(),
177            })
178        })
179    }
180}
181
182fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Result<Vec<PathBuf>>> {
183    let path_style = project.read(cx).path_style(cx);
184    let path_matcher = match PathMatcher::new(
185        [
186            // Sometimes models try to search for "". In this case, return all paths in the project.
187            if glob.is_empty() { "*" } else { glob },
188        ],
189        path_style,
190    ) {
191        Ok(matcher) => matcher,
192        Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))),
193    };
194    let snapshots: Vec<_> = project
195        .read(cx)
196        .worktrees(cx)
197        .map(|worktree| worktree.read(cx).snapshot())
198        .collect();
199
200    cx.background_spawn(async move {
201        let mut results = Vec::new();
202        for snapshot in snapshots {
203            for entry in snapshot.entries(false, 0) {
204                if path_matcher.is_match(&snapshot.root_name().join(&entry.path)) {
205                    results.push(snapshot.absolutize(&entry.path));
206                }
207            }
208        }
209
210        Ok(results)
211    })
212}
213
214#[cfg(test)]
215mod test {
216    use super::*;
217    use gpui::TestAppContext;
218    use project::{FakeFs, Project};
219    use settings::SettingsStore;
220    use util::path;
221
222    #[gpui::test]
223    async fn test_find_path_tool(cx: &mut TestAppContext) {
224        init_test(cx);
225
226        let fs = FakeFs::new(cx.executor());
227        fs.insert_tree(
228            "/root",
229            serde_json::json!({
230                "apple": {
231                    "banana": {
232                        "carrot": "1",
233                    },
234                    "bandana": {
235                        "carbonara": "2",
236                    },
237                    "endive": "3"
238                }
239            }),
240        )
241        .await;
242        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
243
244        let matches = cx
245            .update(|cx| search_paths("root/**/car*", project.clone(), cx))
246            .await
247            .unwrap();
248        assert_eq!(
249            matches,
250            &[
251                PathBuf::from(path!("/root/apple/banana/carrot")),
252                PathBuf::from(path!("/root/apple/bandana/carbonara"))
253            ]
254        );
255
256        let matches = cx
257            .update(|cx| search_paths("**/car*", project.clone(), cx))
258            .await
259            .unwrap();
260        assert_eq!(
261            matches,
262            &[
263                PathBuf::from(path!("/root/apple/banana/carrot")),
264                PathBuf::from(path!("/root/apple/bandana/carbonara"))
265            ]
266        );
267    }
268
269    fn init_test(cx: &mut TestAppContext) {
270        cx.update(|cx| {
271            let settings_store = SettingsStore::test(cx);
272            cx.set_global(settings_store);
273        });
274    }
275}
276
Served at tenant.openagents/omega Member data and write actions are omitted.