Skip to repository content276 lines · 9.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:34:29.601Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
find_path_tool.rs
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