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(Serialize)]
438#[serde(rename_all = "camelCase")]
439pub struct EditToolResponse;
440
441#[derive(Deserialize, JsonSchema, Debug)]
442pub struct ReadToolParams {
443 /// The absolute path to the file to read.
444 pub abs_path: PathBuf,
445 /// Which line to start reading from. Omit to start from the beginning.
446 #[serde(skip_serializing_if = "Option::is_none")]
447 pub offset: Option<u32>,
448 /// How many lines to read. Omit for the whole file.
449 #[serde(skip_serializing_if = "Option::is_none")]
450 pub limit: Option<u32>,
451}
452
453#[derive(Serialize)]
454#[serde(rename_all = "camelCase")]
455pub struct ReadToolResponse {
456 pub content: String,
457}
458
459#[derive(Deserialize, JsonSchema, Debug)]
460pub struct WriteToolParams {
461 /// Absolute path for new file
462 pub file_path: PathBuf,
463 /// File content
464 pub content: String,
465}
466
467#[derive(Deserialize, JsonSchema, Debug)]
468pub struct BashToolParams {
469 /// Shell command to execute
470 pub command: String,
471 /// 5-10 word description of what command does
472 #[serde(skip_serializing_if = "Option::is_none")]
473 pub description: Option<String>,
474 /// Timeout in ms (max 600000ms/10min, default 120000ms)
475 #[serde(skip_serializing_if = "Option::is_none")]
476 pub timeout: Option<u32>,
477}
478
479#[derive(Deserialize, JsonSchema, Debug)]
480pub struct GlobToolParams {
481 /// Glob pattern like **/*.js or src/**/*.ts
482 pub pattern: String,
483 /// Directory to search in (omit for current directory)
484 #[serde(skip_serializing_if = "Option::is_none")]
485 pub path: Option<PathBuf>,
486}
487
488impl std::fmt::Display for GlobToolParams {
489 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
490 if let Some(path) = &self.path {
491 write!(f, "{}", path.display())?;
492 }
493 write!(f, "{}", self.pattern)
494 }
495}
496
497#[derive(Deserialize, JsonSchema, Debug)]
498pub struct LsToolParams {
499 /// Absolute path to directory
500 pub path: PathBuf,
501 /// Array of glob patterns to ignore
502 #[serde(default, skip_serializing_if = "Vec::is_empty")]
503 pub ignore: Vec<String>,
504}
505
506#[derive(Deserialize, JsonSchema, Debug)]
507pub struct GrepToolParams {
508 /// Regex pattern to search for
509 pub pattern: String,
510 /// File/directory to search (defaults to current directory)
511 #[serde(skip_serializing_if = "Option::is_none")]
512 pub path: Option<String>,
513 /// "content" (shows lines), "files_with_matches" (default), "count"
514 #[serde(skip_serializing_if = "Option::is_none")]
515 pub output_mode: Option<GrepOutputMode>,
516 /// Filter files with glob pattern like "*.js"
517 #[serde(skip_serializing_if = "Option::is_none")]
518 pub glob: Option<String>,
519 /// File type filter like "js", "py", "rust"
520 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
521 pub file_type: Option<String>,
522 /// Case insensitive search
523 #[serde(rename = "-i", default, skip_serializing_if = "is_false")]
524 pub case_insensitive: bool,
525 /// Show line numbers (content mode only)
526 #[serde(rename = "-n", default, skip_serializing_if = "is_false")]
527 pub line_numbers: bool,
528 /// Lines after match (content mode only)
529 #[serde(rename = "-A", skip_serializing_if = "Option::is_none")]
530 pub after_context: Option<u32>,
531 /// Lines before match (content mode only)
532 #[serde(rename = "-B", skip_serializing_if = "Option::is_none")]
533 pub before_context: Option<u32>,
534 /// Lines before and after match (content mode only)
535 #[serde(rename = "-C", skip_serializing_if = "Option::is_none")]
536 pub context: Option<u32>,
537 /// Enable multiline/cross-line matching
538 #[serde(default, skip_serializing_if = "is_false")]
539 pub multiline: bool,
540 /// Limit output to first N results
541 #[serde(skip_serializing_if = "Option::is_none")]
542 pub head_limit: Option<u32>,
543}
544
545impl std::fmt::Display for GrepToolParams {
546 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
547 write!(f, "grep")?;
548
549 // Boolean flags
550 if self.case_insensitive {
551 write!(f, " -i")?;
552 }
553 if self.line_numbers {
554 write!(f, " -n")?;
555 }
556
557 // Context options
558 if let Some(after) = self.after_context {
559 write!(f, " -A {}", after)?;
560 }
561 if let Some(before) = self.before_context {
562 write!(f, " -B {}", before)?;
563 }
564 if let Some(context) = self.context {
565 write!(f, " -C {}", context)?;
566 }
567
568 // Output mode
569 if let Some(mode) = &self.output_mode {
570 match mode {
571 GrepOutputMode::FilesWithMatches => write!(f, " -l")?,
572 GrepOutputMode::Count => write!(f, " -c")?,
573 GrepOutputMode::Content => {} // Default mode
574 }
575 }
576
577 // Head limit
578 if let Some(limit) = self.head_limit {
579 write!(f, " | head -{}", limit)?;
580 }
581
582 // Glob pattern
583 if let Some(glob) = &self.glob {
584 write!(f, " --include=\"{}\"", glob)?;
585 }
586
587 // File type
588 if let Some(file_type) = &self.file_type {
589 write!(f, " --type={}", file_type)?;
590 }
591
592 // Multiline
593 if self.multiline {
594 write!(f, " -P")?; // Perl-compatible regex for multiline
595 }
596
597 // Pattern (escaped if contains special characters)
598 write!(f, " \"{}\"", self.pattern)?;
599
600 // Path
601 if let Some(path) = &self.path {
602 write!(f, " {}", path)?;
603 }
604
605 Ok(())
606 }
607}
608
609#[derive(Deserialize, Serialize, JsonSchema, strum::Display, Debug)]
610#[serde(rename_all = "snake_case")]
611pub enum TodoPriority {
612 High,
613 Medium,
614 Low,
615}
616
617impl Into<acp::PlanEntryPriority> for TodoPriority {
618 fn into(self) -> acp::PlanEntryPriority {
619 match self {
620 TodoPriority::High => acp::PlanEntryPriority::High,
621 TodoPriority::Medium => acp::PlanEntryPriority::Medium,
622 TodoPriority::Low => acp::PlanEntryPriority::Low,
623 }
624 }
625}
626
627#[derive(Deserialize, Serialize, JsonSchema, Debug)]
628#[serde(rename_all = "snake_case")]
629pub enum TodoStatus {
630 Pending,
631 InProgress,
632 Completed,
633}
634
635impl Into<acp::PlanEntryStatus> for TodoStatus {
636 fn into(self) -> acp::PlanEntryStatus {
637 match self {
638 TodoStatus::Pending => acp::PlanEntryStatus::Pending,
639 TodoStatus::InProgress => acp::PlanEntryStatus::InProgress,
640 TodoStatus::Completed => acp::PlanEntryStatus::Completed,
641 }
642 }
643}
644
645#[derive(Deserialize, Serialize, JsonSchema, Debug)]
646pub struct Todo {
647 /// Unique identifier
648 pub id: String,
649 /// Task description
650 pub content: String,
651 /// Priority level of the todo
652 pub priority: TodoPriority,
653 /// Current status of the todo
654 pub status: TodoStatus,
655}
656
657impl Into<acp::PlanEntry> for Todo {
658 fn into(self) -> acp::PlanEntry {
659 acp::PlanEntry {
660 content: self.content,
661 priority: self.priority.into(),
662 status: self.status.into(),
663 }
664 }
665}
666
667#[derive(Deserialize, JsonSchema, Debug)]
668pub struct TodoWriteToolParams {
669 pub todos: Vec<Todo>,
670}
671
672#[derive(Deserialize, JsonSchema, Debug)]
673pub struct ExitPlanModeToolParams {
674 /// Implementation plan in markdown format
675 pub plan: String,
676}
677
678#[derive(Deserialize, JsonSchema, Debug)]
679pub struct TaskToolParams {
680 /// Short 3-5 word description of task
681 pub description: String,
682 /// Detailed task for agent to perform
683 pub prompt: String,
684}
685
686#[derive(Deserialize, JsonSchema, Debug)]
687pub struct NotebookReadToolParams {
688 /// Absolute path to .ipynb file
689 pub notebook_path: PathBuf,
690 /// Specific cell ID to read
691 #[serde(skip_serializing_if = "Option::is_none")]
692 pub cell_id: Option<String>,
693}
694
695#[derive(Deserialize, Serialize, JsonSchema, Debug)]
696#[serde(rename_all = "snake_case")]
697pub enum CellType {
698 Code,
699 Markdown,
700}
701
702#[derive(Deserialize, Serialize, JsonSchema, Debug)]
703#[serde(rename_all = "snake_case")]
704pub enum EditMode {
705 Replace,
706 Insert,
707 Delete,
708}
709
710#[derive(Deserialize, JsonSchema, Debug)]
711pub struct NotebookEditToolParams {
712 /// Absolute path to .ipynb file
713 pub notebook_path: PathBuf,
714 /// New cell content
715 pub new_source: String,
716 /// Cell ID to edit
717 #[serde(skip_serializing_if = "Option::is_none")]
718 pub cell_id: Option<String>,
719 /// Type of cell (code or markdown)
720 #[serde(skip_serializing_if = "Option::is_none")]
721 pub cell_type: Option<CellType>,
722 /// Edit operation mode
723 #[serde(skip_serializing_if = "Option::is_none")]
724 pub edit_mode: Option<EditMode>,
725}
726
727#[derive(Deserialize, Serialize, JsonSchema, Debug)]
728pub struct MultiEditItem {
729 /// The text to search for and replace
730 pub old_string: String,
731 /// The replacement text
732 pub new_string: String,
733 /// Whether to replace all occurrences or just the first
734 #[serde(default, skip_serializing_if = "is_false")]
735 pub replace_all: bool,
736}
737
738#[derive(Deserialize, JsonSchema, Debug)]
739pub struct MultiEditToolParams {
740 /// Absolute path to file
741 pub file_path: PathBuf,
742 /// List of edits to apply
743 pub edits: Vec<MultiEditItem>,
744}
745
746fn is_false(v: &bool) -> bool {
747 !*v
748}
749
750#[derive(Deserialize, JsonSchema, Debug)]
751#[serde(rename_all = "snake_case")]
752pub enum GrepOutputMode {
753 Content,
754 FilesWithMatches,
755 Count,
756}
757
758#[derive(Deserialize, JsonSchema, Debug)]
759pub struct WebFetchToolParams {
760 /// Valid URL to fetch
761 #[serde(rename = "url")]
762 pub url: String,
763 /// What to extract from content
764 pub prompt: String,
765}
766
767#[derive(Deserialize, JsonSchema, Debug)]
768pub struct WebSearchToolParams {
769 /// Search query (min 2 chars)
770 pub query: String,
771 /// Only include these domains
772 #[serde(default, skip_serializing_if = "Vec::is_empty")]
773 pub allowed_domains: Vec<String>,
774 /// Exclude these domains
775 #[serde(default, skip_serializing_if = "Vec::is_empty")]
776 pub blocked_domains: Vec<String>,
777}
778
779impl std::fmt::Display for WebSearchToolParams {
780 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
781 write!(f, "\"{}\"", self.query)?;
782
783 if !self.allowed_domains.is_empty() {
784 write!(f, " (allowed: {})", self.allowed_domains.join(", "))?;
785 }
786
787 if !self.blocked_domains.is_empty() {
788 write!(f, " (blocked: {})", self.blocked_domains.join(", "))?;
789 }
790
791 Ok(())
792 }
793}