tools.rs

  1use std::path::PathBuf;
  2
  3use agentic_coding_protocol::{self as acp, PushToolCallParams, ToolCallLocation};
  4use itertools::Itertools;
  5use schemars::JsonSchema;
  6use serde::{Deserialize, Serialize};
  7use util::ResultExt;
  8
  9pub enum ClaudeTool {
 10    Task(Option<TaskToolParams>),
 11    NotebookRead(Option<NotebookReadToolParams>),
 12    NotebookEdit(Option<NotebookEditToolParams>),
 13    Edit(Option<EditToolParams>),
 14    MultiEdit(Option<MultiEditToolParams>),
 15    ReadFile(Option<ReadToolParams>),
 16    Write(Option<WriteToolParams>),
 17    Ls(Option<LsToolParams>),
 18    Glob(Option<GlobToolParams>),
 19    Grep(Option<GrepToolParams>),
 20    Terminal(Option<BashToolParams>),
 21    WebFetch(Option<WebFetchToolParams>),
 22    WebSearch(Option<WebSearchToolParams>),
 23    TodoWrite(Option<TodoWriteToolParams>),
 24    ExitPlanMode(Option<ExitPlanModeToolParams>),
 25    Other {
 26        name: String,
 27        input: serde_json::Value,
 28    },
 29}
 30
 31impl ClaudeTool {
 32    pub fn infer(tool_name: &str, input: serde_json::Value) -> Self {
 33        match tool_name {
 34            // Known tools
 35            "mcp__zed__Read" => Self::ReadFile(serde_json::from_value(input).log_err()),
 36            "mcp__zed__Edit" => Self::Edit(serde_json::from_value(input).log_err()),
 37            "MultiEdit" => Self::MultiEdit(serde_json::from_value(input).log_err()),
 38            "Write" => Self::Write(serde_json::from_value(input).log_err()),
 39            "LS" => Self::Ls(serde_json::from_value(input).log_err()),
 40            "Glob" => Self::Glob(serde_json::from_value(input).log_err()),
 41            "Grep" => Self::Grep(serde_json::from_value(input).log_err()),
 42            "Bash" => Self::Terminal(serde_json::from_value(input).log_err()),
 43            "WebFetch" => Self::WebFetch(serde_json::from_value(input).log_err()),
 44            "WebSearch" => Self::WebSearch(serde_json::from_value(input).log_err()),
 45            "TodoWrite" => Self::TodoWrite(serde_json::from_value(input).log_err()),
 46            "exit_plan_mode" => Self::ExitPlanMode(serde_json::from_value(input).log_err()),
 47            "Task" => Self::Task(serde_json::from_value(input).log_err()),
 48            "NotebookRead" => Self::NotebookRead(serde_json::from_value(input).log_err()),
 49            "NotebookEdit" => Self::NotebookEdit(serde_json::from_value(input).log_err()),
 50            // Inferred from name
 51            _ => {
 52                let tool_name = tool_name.to_lowercase();
 53
 54                if tool_name.contains("edit") || tool_name.contains("write") {
 55                    Self::Edit(None)
 56                } else if tool_name.contains("terminal") {
 57                    Self::Terminal(None)
 58                } else {
 59                    Self::Other {
 60                        name: tool_name.to_string(),
 61                        input,
 62                    }
 63                }
 64            }
 65        }
 66    }
 67
 68    pub fn label(&self) -> String {
 69        match &self {
 70            Self::Task(Some(params)) => params.description.clone(),
 71            Self::Task(None) => "Task".into(),
 72            Self::NotebookRead(Some(params)) => {
 73                format!("Read Notebook {}", params.notebook_path.display())
 74            }
 75            Self::NotebookRead(None) => "Read Notebook".into(),
 76            Self::NotebookEdit(Some(params)) => {
 77                format!("Edit Notebook {}", params.notebook_path.display())
 78            }
 79            Self::NotebookEdit(None) => "Edit Notebook".into(),
 80            Self::Terminal(Some(params)) => format!("`{}`", params.command),
 81            Self::Terminal(None) => "Terminal".into(),
 82            Self::ReadFile(_) => "Read File".into(),
 83            Self::Ls(Some(params)) => {
 84                format!("List Directory {}", params.path.display())
 85            }
 86            Self::Ls(None) => "List Directory".into(),
 87            Self::Edit(Some(params)) => {
 88                format!("Edit {}", params.abs_path.display())
 89            }
 90            Self::Edit(None) => "Edit".into(),
 91            Self::MultiEdit(Some(params)) => {
 92                format!("Multi Edit {}", params.file_path.display())
 93            }
 94            Self::MultiEdit(None) => "Multi Edit".into(),
 95            Self::Write(Some(params)) => {
 96                format!("Write {}", params.file_path.display())
 97            }
 98            Self::Write(None) => "Write".into(),
 99            Self::Glob(Some(params)) => {
100                format!("Glob `{params}`")
101            }
102            Self::Glob(None) => "Glob".into(),
103            Self::Grep(Some(params)) => format!("`{params}`"),
104            Self::Grep(None) => "Grep".into(),
105            Self::WebFetch(Some(params)) => format!("Fetch {}", params.url),
106            Self::WebFetch(None) => "Fetch".into(),
107            Self::WebSearch(Some(params)) => format!("Web Search: {}", params),
108            Self::WebSearch(None) => "Web Search".into(),
109            Self::TodoWrite(Some(params)) => format!(
110                "Update TODOs: {}",
111                params.todos.iter().map(|todo| &todo.content).join(", ")
112            ),
113            Self::TodoWrite(None) => "Update TODOs".into(),
114            Self::ExitPlanMode(_) => "Exit Plan Mode".into(),
115            Self::Other { name, .. } => name.clone(),
116        }
117    }
118
119    pub fn content(&self) -> Option<acp::ToolCallContent> {
120        match &self {
121            Self::Other { input, .. } => Some(acp::ToolCallContent::Markdown {
122                markdown: format!(
123                    "```json\n{}```",
124                    serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
125                ),
126            }),
127            Self::Task(Some(params)) => Some(acp::ToolCallContent::Markdown {
128                markdown: params.prompt.clone(),
129            }),
130            Self::NotebookRead(Some(params)) => Some(acp::ToolCallContent::Markdown {
131                markdown: params.notebook_path.display().to_string(),
132            }),
133            Self::NotebookEdit(Some(params)) => Some(acp::ToolCallContent::Markdown {
134                markdown: params.new_source.clone(),
135            }),
136            Self::Terminal(Some(params)) => Some(acp::ToolCallContent::Markdown {
137                markdown: format!(
138                    "`{}`\n\n{}",
139                    params.command,
140                    params.description.as_deref().unwrap_or_default()
141                ),
142            }),
143            Self::ReadFile(Some(params)) => Some(acp::ToolCallContent::Markdown {
144                markdown: params.abs_path.display().to_string(),
145            }),
146            Self::Ls(Some(params)) => Some(acp::ToolCallContent::Markdown {
147                markdown: params.path.display().to_string(),
148            }),
149            Self::Glob(Some(params)) => Some(acp::ToolCallContent::Markdown {
150                markdown: params.to_string(),
151            }),
152            Self::Grep(Some(params)) => Some(acp::ToolCallContent::Markdown {
153                markdown: format!("`{params}`"),
154            }),
155            Self::WebFetch(Some(params)) => Some(acp::ToolCallContent::Markdown {
156                markdown: params.prompt.clone(),
157            }),
158            Self::WebSearch(Some(params)) => Some(acp::ToolCallContent::Markdown {
159                markdown: params.to_string(),
160            }),
161            Self::TodoWrite(Some(params)) => Some(acp::ToolCallContent::Markdown {
162                markdown: params
163                    .todos
164                    .iter()
165                    .map(|todo| {
166                        format!(
167                            "- {} {}: {}",
168                            match todo.status {
169                                TodoStatus::Completed => "",
170                                TodoStatus::InProgress => "🚧",
171                                TodoStatus::Pending => "",
172                            },
173                            todo.priority,
174                            todo.content
175                        )
176                    })
177                    .join("\n"),
178            }),
179            Self::ExitPlanMode(Some(params)) => Some(acp::ToolCallContent::Markdown {
180                markdown: params.plan.clone(),
181            }),
182            Self::Edit(Some(params)) => Some(acp::ToolCallContent::Diff {
183                diff: acp::Diff {
184                    path: params.abs_path.clone(),
185                    old_text: Some(params.old_text.clone()),
186                    new_text: params.new_text.clone(),
187                },
188            }),
189            Self::Write(Some(params)) => Some(acp::ToolCallContent::Diff {
190                diff: acp::Diff {
191                    path: params.file_path.clone(),
192                    old_text: None,
193                    new_text: params.content.clone(),
194                },
195            }),
196            Self::MultiEdit(Some(params)) => {
197                // todo: show multiple edits in a multibuffer?
198                params.edits.first().map(|edit| acp::ToolCallContent::Diff {
199                    diff: acp::Diff {
200                        path: params.file_path.clone(),
201                        old_text: Some(edit.old_string.clone()),
202                        new_text: edit.new_string.clone(),
203                    },
204                })
205            }
206            Self::Task(None)
207            | Self::NotebookRead(None)
208            | Self::NotebookEdit(None)
209            | Self::Terminal(None)
210            | Self::ReadFile(None)
211            | Self::Ls(None)
212            | Self::Glob(None)
213            | Self::Grep(None)
214            | Self::WebFetch(None)
215            | Self::WebSearch(None)
216            | Self::TodoWrite(None)
217            | Self::ExitPlanMode(None)
218            | Self::Edit(None)
219            | Self::Write(None)
220            | Self::MultiEdit(None) => None,
221        }
222    }
223
224    pub fn icon(&self) -> acp::Icon {
225        match self {
226            Self::Task(_) => acp::Icon::Hammer,
227            Self::NotebookRead(_) => acp::Icon::FileSearch,
228            Self::NotebookEdit(_) => acp::Icon::Pencil,
229            Self::Edit(_) => acp::Icon::Pencil,
230            Self::MultiEdit(_) => acp::Icon::Pencil,
231            Self::Write(_) => acp::Icon::Pencil,
232            Self::ReadFile(_) => acp::Icon::FileSearch,
233            Self::Ls(_) => acp::Icon::Folder,
234            Self::Glob(_) => acp::Icon::FileSearch,
235            Self::Grep(_) => acp::Icon::Regex,
236            Self::Terminal(_) => acp::Icon::Terminal,
237            Self::WebSearch(_) => acp::Icon::Globe,
238            Self::WebFetch(_) => acp::Icon::Globe,
239            Self::TodoWrite(_) => acp::Icon::LightBulb,
240            Self::ExitPlanMode(_) => acp::Icon::Hammer,
241            Self::Other { .. } => acp::Icon::Hammer,
242        }
243    }
244
245    pub fn confirmation(&self, description: Option<String>) -> acp::ToolCallConfirmation {
246        match &self {
247            Self::Edit(_) | Self::Write(_) | Self::NotebookEdit(_) | Self::MultiEdit(_) => {
248                acp::ToolCallConfirmation::Edit { description }
249            }
250            Self::WebFetch(params) => acp::ToolCallConfirmation::Fetch {
251                urls: params
252                    .as_ref()
253                    .map(|p| vec![p.url.clone()])
254                    .unwrap_or_default(),
255                description,
256            },
257            Self::Terminal(Some(BashToolParams {
258                description,
259                command,
260                ..
261            })) => acp::ToolCallConfirmation::Execute {
262                command: command.clone(),
263                root_command: command.clone(),
264                description: description.clone(),
265            },
266            Self::ExitPlanMode(Some(params)) => acp::ToolCallConfirmation::Other {
267                description: if let Some(description) = description {
268                    format!("{description} {}", params.plan)
269                } else {
270                    params.plan.clone()
271                },
272            },
273            Self::Task(Some(params)) => acp::ToolCallConfirmation::Other {
274                description: if let Some(description) = description {
275                    format!("{description} {}", params.description)
276                } else {
277                    params.description.clone()
278                },
279            },
280            Self::Ls(Some(LsToolParams { path, .. }))
281            | Self::ReadFile(Some(ReadToolParams { abs_path: path, .. })) => {
282                let path = path.display();
283                acp::ToolCallConfirmation::Other {
284                    description: if let Some(description) = description {
285                        format!("{description} {path}")
286                    } else {
287                        path.to_string()
288                    },
289                }
290            }
291            Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
292                let path = notebook_path.display();
293                acp::ToolCallConfirmation::Other {
294                    description: if let Some(description) = description {
295                        format!("{description} {path}")
296                    } else {
297                        path.to_string()
298                    },
299                }
300            }
301            Self::Glob(Some(params)) => acp::ToolCallConfirmation::Other {
302                description: if let Some(description) = description {
303                    format!("{description} {params}")
304                } else {
305                    params.to_string()
306                },
307            },
308            Self::Grep(Some(params)) => acp::ToolCallConfirmation::Other {
309                description: if let Some(description) = description {
310                    format!("{description} {params}")
311                } else {
312                    params.to_string()
313                },
314            },
315            Self::WebSearch(Some(params)) => acp::ToolCallConfirmation::Other {
316                description: if let Some(description) = description {
317                    format!("{description} {params}")
318                } else {
319                    params.to_string()
320                },
321            },
322            Self::TodoWrite(Some(params)) => {
323                let params = params.todos.iter().map(|todo| &todo.content).join(", ");
324                acp::ToolCallConfirmation::Other {
325                    description: if let Some(description) = description {
326                        format!("{description} {params}")
327                    } else {
328                        params
329                    },
330                }
331            }
332            Self::Terminal(None)
333            | Self::Task(None)
334            | Self::NotebookRead(None)
335            | Self::ExitPlanMode(None)
336            | Self::Ls(None)
337            | Self::Glob(None)
338            | Self::Grep(None)
339            | Self::ReadFile(None)
340            | Self::WebSearch(None)
341            | Self::TodoWrite(None)
342            | Self::Other { .. } => acp::ToolCallConfirmation::Other {
343                description: description.unwrap_or("".to_string()),
344            },
345        }
346    }
347
348    pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
349        match &self {
350            Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![ToolCallLocation {
351                path: abs_path.clone(),
352                line: None,
353            }],
354            Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
355                vec![ToolCallLocation {
356                    path: file_path.clone(),
357                    line: None,
358                }]
359            }
360            Self::Write(Some(WriteToolParams { file_path, .. })) => vec![ToolCallLocation {
361                path: file_path.clone(),
362                line: None,
363            }],
364            Self::ReadFile(Some(ReadToolParams {
365                abs_path, offset, ..
366            })) => vec![ToolCallLocation {
367                path: abs_path.clone(),
368                line: *offset,
369            }],
370            Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
371                vec![ToolCallLocation {
372                    path: notebook_path.clone(),
373                    line: None,
374                }]
375            }
376            Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
377                vec![ToolCallLocation {
378                    path: notebook_path.clone(),
379                    line: None,
380                }]
381            }
382            Self::Glob(Some(GlobToolParams {
383                path: Some(path), ..
384            })) => vec![ToolCallLocation {
385                path: path.clone(),
386                line: None,
387            }],
388            Self::Ls(Some(LsToolParams { path, .. })) => vec![ToolCallLocation {
389                path: path.clone(),
390                line: None,
391            }],
392            Self::Grep(Some(GrepToolParams {
393                path: Some(path), ..
394            })) => vec![ToolCallLocation {
395                path: PathBuf::from(path),
396                line: None,
397            }],
398            Self::Task(_)
399            | Self::NotebookRead(None)
400            | Self::NotebookEdit(None)
401            | Self::Edit(None)
402            | Self::MultiEdit(None)
403            | Self::Write(None)
404            | Self::ReadFile(None)
405            | Self::Ls(None)
406            | Self::Glob(_)
407            | Self::Grep(_)
408            | Self::Terminal(_)
409            | Self::WebFetch(_)
410            | Self::WebSearch(_)
411            | Self::TodoWrite(_)
412            | Self::ExitPlanMode(_)
413            | Self::Other { .. } => vec![],
414        }
415    }
416
417    pub fn as_acp(&self) -> PushToolCallParams {
418        PushToolCallParams {
419            label: self.label(),
420            content: self.content(),
421            icon: self.icon(),
422            locations: self.locations(),
423        }
424    }
425}
426
427#[derive(Deserialize, JsonSchema, Debug)]
428pub struct EditToolParams {
429    /// The absolute path to the file to read.
430    pub abs_path: PathBuf,
431    /// The old text to replace (must be unique in the file)
432    pub old_text: String,
433    /// The new text.
434    pub new_text: String,
435}
436
437#[derive(Deserialize, JsonSchema, Debug)]
438pub struct ReadToolParams {
439    /// The absolute path to the file to read.
440    pub abs_path: PathBuf,
441    /// Which line to start reading from. Omit to start from the beginning.
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub offset: Option<u32>,
444    /// How many lines to read. Omit for the whole file.
445    #[serde(skip_serializing_if = "Option::is_none")]
446    pub limit: Option<u32>,
447}
448
449#[derive(Deserialize, JsonSchema, Debug)]
450pub struct WriteToolParams {
451    /// Absolute path for new file
452    pub file_path: PathBuf,
453    /// File content
454    pub content: String,
455}
456
457#[derive(Deserialize, JsonSchema, Debug)]
458pub struct BashToolParams {
459    /// Shell command to execute
460    pub command: String,
461    /// 5-10 word description of what command does
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub description: Option<String>,
464    /// Timeout in ms (max 600000ms/10min, default 120000ms)
465    #[serde(skip_serializing_if = "Option::is_none")]
466    pub timeout: Option<u32>,
467}
468
469#[derive(Deserialize, JsonSchema, Debug)]
470pub struct GlobToolParams {
471    /// Glob pattern like **/*.js or src/**/*.ts
472    pub pattern: String,
473    /// Directory to search in (omit for current directory)
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub path: Option<PathBuf>,
476}
477
478impl std::fmt::Display for GlobToolParams {
479    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480        if let Some(path) = &self.path {
481            write!(f, "{}", path.display())?;
482        }
483        write!(f, "{}", self.pattern)
484    }
485}
486
487#[derive(Deserialize, JsonSchema, Debug)]
488pub struct LsToolParams {
489    /// Absolute path to directory
490    pub path: PathBuf,
491    /// Array of glob patterns to ignore
492    #[serde(default, skip_serializing_if = "Vec::is_empty")]
493    pub ignore: Vec<String>,
494}
495
496#[derive(Deserialize, JsonSchema, Debug)]
497pub struct GrepToolParams {
498    /// Regex pattern to search for
499    pub pattern: String,
500    /// File/directory to search (defaults to current directory)
501    #[serde(skip_serializing_if = "Option::is_none")]
502    pub path: Option<String>,
503    /// "content" (shows lines), "files_with_matches" (default), "count"
504    #[serde(skip_serializing_if = "Option::is_none")]
505    pub output_mode: Option<GrepOutputMode>,
506    /// Filter files with glob pattern like "*.js"
507    #[serde(skip_serializing_if = "Option::is_none")]
508    pub glob: Option<String>,
509    /// File type filter like "js", "py", "rust"
510    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
511    pub file_type: Option<String>,
512    /// Case insensitive search
513    #[serde(rename = "-i", default, skip_serializing_if = "is_false")]
514    pub case_insensitive: bool,
515    /// Show line numbers (content mode only)
516    #[serde(rename = "-n", default, skip_serializing_if = "is_false")]
517    pub line_numbers: bool,
518    /// Lines after match (content mode only)
519    #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
520    pub after_context: Option<u32>,
521    /// Lines before match (content mode only)
522    #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
523    pub before_context: Option<u32>,
524    /// Lines before and after match (content mode only)
525    #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
526    pub context: Option<u32>,
527    /// Enable multiline/cross-line matching
528    #[serde(default, skip_serializing_if = "is_false")]
529    pub multiline: bool,
530    /// Limit output to first N results
531    #[serde(skip_serializing_if = "Option::is_none")]
532    pub head_limit: Option<u32>,
533}
534
535impl std::fmt::Display for GrepToolParams {
536    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537        write!(f, "grep")?;
538
539        // Boolean flags
540        if self.case_insensitive {
541            write!(f, " -i")?;
542        }
543        if self.line_numbers {
544            write!(f, " -n")?;
545        }
546
547        // Context options
548        if let Some(after) = self.after_context {
549            write!(f, " -A {}", after)?;
550        }
551        if let Some(before) = self.before_context {
552            write!(f, " -B {}", before)?;
553        }
554        if let Some(context) = self.context {
555            write!(f, " -C {}", context)?;
556        }
557
558        // Output mode
559        if let Some(mode) = &self.output_mode {
560            match mode {
561                GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
562                GrepOutputMode::Count => write!(f, " -c")?,
563                GrepOutputMode::Content => {} // Default mode
564            }
565        }
566
567        // Head limit
568        if let Some(limit) = self.head_limit {
569            write!(f, " | head -{}", limit)?;
570        }
571
572        // Glob pattern
573        if let Some(glob) = &self.glob {
574            write!(f, " --include=\"{}\"", glob)?;
575        }
576
577        // File type
578        if let Some(file_type) = &self.file_type {
579            write!(f, " --type={}", file_type)?;
580        }
581
582        // Multiline
583        if self.multiline {
584            write!(f, " -P")?; // Perl-compatible regex for multiline
585        }
586
587        // Pattern (escaped if contains special characters)
588        write!(f, " \"{}\"", self.pattern)?;
589
590        // Path
591        if let Some(path) = &self.path {
592            write!(f, " {}", path)?;
593        }
594
595        Ok(())
596    }
597}
598
599#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
600#[serde(rename_all = "snake_case")]
601pub enum TodoPriority {
602    High,
603    Medium,
604    Low,
605}
606
607impl Into<acp::PlanEntryPriority> for TodoPriority {
608    fn into(self) -> acp::PlanEntryPriority {
609        match self {
610            TodoPriority::High => acp::PlanEntryPriority::High,
611            TodoPriority::Medium => acp::PlanEntryPriority::Medium,
612            TodoPriority::Low => acp::PlanEntryPriority::Low,
613        }
614    }
615}
616
617#[derive(Deserialize, Serialize, JsonSchema, Debug)]
618#[serde(rename_all = "snake_case")]
619pub enum TodoStatus {
620    Pending,
621    InProgress,
622    Completed,
623}
624
625impl Into<acp::PlanEntryStatus> for TodoStatus {
626    fn into(self) -> acp::PlanEntryStatus {
627        match self {
628            TodoStatus::Pending => acp::PlanEntryStatus::Pending,
629            TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
630            TodoStatus::Completed => acp::PlanEntryStatus::Completed,
631        }
632    }
633}
634
635#[derive(Deserialize, Serialize, JsonSchema, Debug)]
636pub struct Todo {
637    /// Unique identifier
638    pub id: String,
639    /// Task description
640    pub content: String,
641    /// Priority level of the todo
642    pub priority: TodoPriority,
643    /// Current status of the todo
644    pub status: TodoStatus,
645}
646
647impl Into<acp::PlanEntry> for Todo {
648    fn into(self) -> acp::PlanEntry {
649        acp::PlanEntry {
650            content: self.content,
651            priority: self.priority.into(),
652            status: self.status.into(),
653        }
654    }
655}
656
657#[derive(Deserialize, JsonSchema, Debug)]
658pub struct TodoWriteToolParams {
659    pub todos: Vec<Todo>,
660}
661
662#[derive(Deserialize, JsonSchema, Debug)]
663pub struct ExitPlanModeToolParams {
664    /// Implementation plan in markdown format
665    pub plan: String,
666}
667
668#[derive(Deserialize, JsonSchema, Debug)]
669pub struct TaskToolParams {
670    /// Short 3-5 word description of task
671    pub description: String,
672    /// Detailed task for agent to perform
673    pub prompt: String,
674}
675
676#[derive(Deserialize, JsonSchema, Debug)]
677pub struct NotebookReadToolParams {
678    /// Absolute path to .ipynb file
679    pub notebook_path: PathBuf,
680    /// Specific cell ID to read
681    #[serde(skip_serializing_if = "Option::is_none")]
682    pub cell_id: Option<String>,
683}
684
685#[derive(Deserialize, Serialize, JsonSchema, Debug)]
686#[serde(rename_all = "snake_case")]
687pub enum CellType {
688    Code,
689    Markdown,
690}
691
692#[derive(Deserialize, Serialize, JsonSchema, Debug)]
693#[serde(rename_all = "snake_case")]
694pub enum EditMode {
695    Replace,
696    Insert,
697    Delete,
698}
699
700#[derive(Deserialize, JsonSchema, Debug)]
701pub struct NotebookEditToolParams {
702    /// Absolute path to .ipynb file
703    pub notebook_path: PathBuf,
704    /// New cell content
705    pub new_source: String,
706    /// Cell ID to edit
707    #[serde(skip_serializing_if = "Option::is_none")]
708    pub cell_id: Option<String>,
709    /// Type of cell (code or markdown)
710    #[serde(skip_serializing_if = "Option::is_none")]
711    pub cell_type: Option<CellType>,
712    /// Edit operation mode
713    #[serde(skip_serializing_if = "Option::is_none")]
714    pub edit_mode: Option<EditMode>,
715}
716
717#[derive(Deserialize, Serialize, JsonSchema, Debug)]
718pub struct MultiEditItem {
719    /// The text to search for and replace
720    pub old_string: String,
721    /// The replacement text
722    pub new_string: String,
723    /// Whether to replace all occurrences or just the first
724    #[serde(default, skip_serializing_if = "is_false")]
725    pub replace_all: bool,
726}
727
728#[derive(Deserialize, JsonSchema, Debug)]
729pub struct MultiEditToolParams {
730    /// Absolute path to file
731    pub file_path: PathBuf,
732    /// List of edits to apply
733    pub edits: Vec<MultiEditItem>,
734}
735
736fn is_false(v: &bool) -> bool {
737    !*v
738}
739
740#[derive(Deserialize, JsonSchema, Debug)]
741#[serde(rename_all = "snake_case")]
742pub enum GrepOutputMode {
743    Content,
744    FilesWithMatches,
745    Count,
746}
747
748#[derive(Deserialize, JsonSchema, Debug)]
749pub struct WebFetchToolParams {
750    /// Valid URL to fetch
751    #[serde(rename = "url")]
752    pub url: String,
753    /// What to extract from content
754    pub prompt: String,
755}
756
757#[derive(Deserialize, JsonSchema, Debug)]
758pub struct WebSearchToolParams {
759    /// Search query (min 2 chars)
760    pub query: String,
761    /// Only include these domains
762    #[serde(default, skip_serializing_if = "Vec::is_empty")]
763    pub allowed_domains: Vec<String>,
764    /// Exclude these domains
765    #[serde(default, skip_serializing_if = "Vec::is_empty")]
766    pub blocked_domains: Vec<String>,
767}
768
769impl std::fmt::Display for WebSearchToolParams {
770    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
771        write!(f, "\"{}\"", self.query)?;
772
773        if !self.allowed_domains.is_empty() {
774            write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
775        }
776
777        if !self.blocked_domains.is_empty() {
778            write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
779        }
780
781        Ok(())
782    }
783}