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