1use crate::{AgentTool, ToolCallEventStream, ToolInput};
2use agent_client_protocol as acp;
3use anyhow::{Result, anyhow};
4use dap::SteppingGranularity;
5use dap::client::SessionId;
6use gpui::{App, AsyncApp, Entity, SharedString, Task};
7use project::Project;
8use project::debugger::breakpoint_store::{
9 Breakpoint, BreakpointEditAction, BreakpointState, BreakpointWithPosition,
10};
11use project::debugger::session::{Session, ThreadId, ThreadStatus};
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::fmt::Write;
15use std::sync::Arc;
16use text::Point;
17use util::markdown::MarkdownInlineCode;
18
19/// Interact with the debugger to control debug sessions, set breakpoints, and inspect program state.
20///
21/// This tool allows you to:
22/// - Set and remove breakpoints at specific file locations
23/// - List all breakpoints in the project
24/// - List active debug sessions
25/// - Control execution (continue, pause, step over, step in, step out)
26/// - Inspect stack traces and variables when stopped at a breakpoint
27///
28/// <guidelines>
29/// - Before using debugger controls (continue, pause, step), ensure there is an active debug session
30/// - When setting breakpoints, use the exact file path as it appears in the project
31/// - Stack traces and variables are only available when the debugger is stopped at a breakpoint
32/// - Use `list_sessions` to see available debug sessions before trying to control them
33/// </guidelines>
34#[derive(Debug, Serialize, Deserialize, JsonSchema)]
35pub struct DebuggerToolInput {
36 /// The debugger operation to perform
37 pub operation: DebuggerOperation,
38 /// The path to the file (required for set_breakpoint and remove_breakpoint operations)
39 #[serde(default)]
40 pub path: Option<String>,
41 /// The 1-based line number (required for set_breakpoint and remove_breakpoint operations)
42 #[serde(default)]
43 pub line: Option<u32>,
44 /// Whether to enable or disable the breakpoint (for set_breakpoint only)
45 #[serde(default)]
46 pub enabled: Option<bool>,
47 /// Optional condition expression that must evaluate to true for the breakpoint to trigger (for set_breakpoint only)
48 #[serde(default)]
49 pub condition: Option<String>,
50 /// Optional log message to output when the breakpoint is hit (for set_breakpoint only)
51 #[serde(default)]
52 pub log_message: Option<String>,
53 /// Optional hit count condition (for set_breakpoint only)
54 #[serde(default)]
55 pub hit_condition: Option<String>,
56 /// Optional session ID. If not provided, uses the active session.
57 #[serde(default)]
58 pub session_id: Option<u32>,
59 /// Optional thread ID. If not provided, uses an appropriate thread based on the operation.
60 #[serde(default)]
61 pub thread_id: Option<i64>,
62 /// Optional stack frame index (0 = top of stack). Used for get_variables operation.
63 #[serde(default)]
64 pub frame_index: Option<usize>,
65}
66
67/// The debugger operation to perform
68#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
69#[serde(rename_all = "snake_case")]
70pub enum DebuggerOperation {
71 /// Set or update a breakpoint at a specific file and line
72 SetBreakpoint,
73 /// Remove a breakpoint at a specific file and line
74 RemoveBreakpoint,
75 /// List all breakpoints in the project
76 ListBreakpoints,
77 /// List all active debug sessions
78 ListSessions,
79 /// Continue execution of a paused thread
80 Continue,
81 /// Pause execution of a running thread
82 Pause,
83 /// Step over the current line (execute without entering functions)
84 StepOver,
85 /// Step into the current line (enter function calls)
86 StepIn,
87 /// Step out of the current function
88 StepOut,
89 /// Get the stack trace for a stopped thread
90 GetStackTrace,
91 /// Get variables in the current scope
92 GetVariables,
93}
94
95pub struct DebuggerTool {
96 project: Entity<Project>,
97}
98
99impl DebuggerTool {
100 pub fn new(project: Entity<Project>) -> Self {
101 Self { project }
102 }
103
104 fn find_session(
105 project: &Entity<Project>,
106 session_id: Option<u32>,
107 cx: &App,
108 ) -> Result<Entity<Session>> {
109 let dap_store = project.read(cx).dap_store();
110 let dap_store = dap_store.read(cx);
111
112 if let Some(id) = session_id {
113 dap_store
114 .session_by_id(SessionId(id))
115 .ok_or_else(|| anyhow!("No debug session found with ID {}", id))
116 } else {
117 if let Some((session, _)) = project.read(cx).active_debug_session(cx) {
118 return Ok(session);
119 }
120 dap_store
121 .sessions()
122 .next()
123 .cloned()
124 .ok_or_else(|| anyhow!("No active debug session. Start a debug session first."))
125 }
126 }
127
128 fn find_stopped_thread(
129 session: &Entity<Session>,
130 thread_id: Option<i64>,
131 cx: &mut App,
132 ) -> Result<ThreadId> {
133 session.update(cx, |session, cx| {
134 let threads = session.threads(cx);
135
136 if let Some(tid) = thread_id {
137 let thread_id = ThreadId::from(tid);
138 if threads
139 .iter()
140 .any(|(t, _)| ThreadId::from(t.id) == thread_id)
141 {
142 return Ok(thread_id);
143 }
144 return Err(anyhow!("Thread {} not found", tid));
145 }
146
147 threads
148 .iter()
149 .find(|(_, status)| *status == ThreadStatus::Stopped)
150 .map(|(t, _)| ThreadId::from(t.id))
151 .ok_or_else(|| {
152 anyhow!("No stopped thread found. The debugger must be paused at a breakpoint.")
153 })
154 })
155 }
156
157 fn find_running_thread(
158 session: &Entity<Session>,
159 thread_id: Option<i64>,
160 cx: &mut App,
161 ) -> Result<ThreadId> {
162 session.update(cx, |session, cx| {
163 let threads = session.threads(cx);
164
165 if let Some(tid) = thread_id {
166 let thread_id = ThreadId::from(tid);
167 if threads
168 .iter()
169 .any(|(t, _)| ThreadId::from(t.id) == thread_id)
170 {
171 return Ok(thread_id);
172 }
173 return Err(anyhow!("Thread {} not found", tid));
174 }
175
176 threads
177 .iter()
178 .find(|(_, status)| *status == ThreadStatus::Running)
179 .map(|(t, _)| ThreadId::from(t.id))
180 .ok_or_else(|| anyhow!("No running thread found."))
181 })
182 }
183
184 fn find_any_thread(
185 session: &Entity<Session>,
186 thread_id: Option<i64>,
187 cx: &mut App,
188 ) -> Result<ThreadId> {
189 session.update(cx, |session, cx| {
190 let threads = session.threads(cx);
191
192 if let Some(tid) = thread_id {
193 let thread_id = ThreadId::from(tid);
194 if threads
195 .iter()
196 .any(|(t, _)| ThreadId::from(t.id) == thread_id)
197 {
198 return Ok(thread_id);
199 }
200 return Err(anyhow!("Thread {} not found", tid));
201 }
202
203 threads
204 .iter()
205 .find(|(_, status)| *status == ThreadStatus::Stopped)
206 .or_else(|| threads.first())
207 .map(|(t, _)| ThreadId::from(t.id))
208 .ok_or_else(|| anyhow!("No threads found in the debug session."))
209 })
210 }
211
212 async fn run_operation(
213 project: Entity<Project>,
214 input: DebuggerToolInput,
215 cx: &mut AsyncApp,
216 ) -> Result<String> {
217 match input.operation {
218 DebuggerOperation::SetBreakpoint => {
219 let path = input
220 .path
221 .ok_or_else(|| anyhow!("path is required for set_breakpoint operation"))?;
222 let line = input
223 .line
224 .ok_or_else(|| anyhow!("line is required for set_breakpoint operation"))?;
225 let enabled = input.enabled;
226 let condition = input.condition;
227 let log_message = input.log_message;
228 let hit_condition = input.hit_condition;
229
230 let (buffer_task, breakpoint_store, abs_path): (_, _, _) = cx.update(|cx| {
231 let project_path = project.read(cx).find_project_path(&path, cx);
232 let Some(project_path) = project_path else {
233 return Err(anyhow!("Could not find path {} in project", path));
234 };
235
236 let breakpoint_store = project.read(cx).breakpoint_store();
237 let buffer_task = project.update(cx, |project, cx| {
238 project.open_buffer(project_path.clone(), cx)
239 });
240
241 let worktree = project
242 .read(cx)
243 .worktree_for_id(project_path.worktree_id, cx);
244 let abs_path = worktree.map(|wt| wt.read(cx).absolutize(&project_path.path));
245
246 Ok((buffer_task, breakpoint_store, abs_path))
247 })?;
248
249 let buffer = buffer_task.await?;
250 let abs_path =
251 abs_path.ok_or_else(|| anyhow!("Could not determine absolute path"))?;
252
253 Ok(cx.update(|cx| {
254 let snapshot = buffer.read(cx).snapshot();
255 let row = line.saturating_sub(1);
256 let point = Point::new(row, 0);
257 let position = snapshot.anchor_before(point);
258
259 let state = match enabled {
260 Some(true) | None => BreakpointState::Enabled,
261 Some(false) => BreakpointState::Disabled,
262 };
263
264 let breakpoint = Breakpoint {
265 message: log_message.map(|s| s.into()),
266 hit_condition: hit_condition.map(|s| s.into()),
267 condition: condition.map(|s| s.into()),
268 state,
269 };
270
271 let breakpoint_with_position = BreakpointWithPosition {
272 position,
273 bp: breakpoint,
274 };
275
276 breakpoint_store.update(cx, |store, cx| {
277 store.toggle_breakpoint(
278 buffer,
279 breakpoint_with_position,
280 BreakpointEditAction::Toggle,
281 cx,
282 );
283 });
284
285 format!("Breakpoint set at {}:{}", abs_path.display(), line)
286 }))
287 }
288
289 DebuggerOperation::RemoveBreakpoint => {
290 let path = input
291 .path
292 .ok_or_else(|| anyhow!("path is required for remove_breakpoint operation"))?;
293 let line = input
294 .line
295 .ok_or_else(|| anyhow!("line is required for remove_breakpoint operation"))?;
296
297 cx.update(|cx| {
298 let project = project.read(cx);
299 let Some(project_path) = project.find_project_path(&path, cx) else {
300 return Err(anyhow!("Could not find path {} in project", path));
301 };
302
303 let worktree = project
304 .worktree_for_id(project_path.worktree_id, cx)
305 .ok_or_else(|| anyhow!("Worktree not found"))?;
306 let abs_path = worktree.read(cx).absolutize(&project_path.path);
307
308 let breakpoint_store = project.breakpoint_store();
309 let row = line.saturating_sub(1);
310
311 let result = breakpoint_store
312 .read(cx)
313 .breakpoint_at_row(&abs_path, row, cx);
314
315 if let Some((buffer, breakpoint)) = result {
316 breakpoint_store.update(cx, |store, cx| {
317 store.toggle_breakpoint(
318 buffer,
319 breakpoint,
320 BreakpointEditAction::Toggle,
321 cx,
322 );
323 });
324 Ok(format!("Breakpoint removed at {}:{}", path, line))
325 } else {
326 Ok(format!("No breakpoint found at {}:{}", path, line))
327 }
328 })
329 }
330 DebuggerOperation::ListBreakpoints => Ok(cx.update(|cx| {
331 let breakpoint_store = project.read(cx).breakpoint_store();
332 let breakpoints = breakpoint_store.read(cx).all_source_breakpoints(cx);
333
334 let mut output = String::new();
335 if breakpoints.is_empty() {
336 output.push_str("No breakpoints set.");
337 } else {
338 writeln!(output, "Breakpoints:").ok();
339 for (path, bps) in &breakpoints {
340 for bp in bps {
341 let state = if bp.state.is_enabled() {
342 "enabled"
343 } else {
344 "disabled"
345 };
346 let mut details = vec![state.to_string()];
347
348 if let Some(ref cond) = bp.condition {
349 details.push(format!("condition: {}", cond));
350 }
351 if let Some(ref msg) = bp.message {
352 details.push(format!("log: {}", msg));
353 }
354 if let Some(ref hit) = bp.hit_condition {
355 details.push(format!("hit: {}", hit));
356 }
357
358 writeln!(
359 output,
360 " - {}:{} [{}]",
361 path.display(),
362 bp.row + 1,
363 details.join(", ")
364 )
365 .ok();
366 }
367 }
368 }
369 output
370 })),
371
372 DebuggerOperation::ListSessions => Ok(cx.update(|cx| {
373 let dap_store = project.read(cx).dap_store();
374 let sessions: Vec<_> = dap_store.read(cx).sessions().cloned().collect();
375
376 let mut output = String::new();
377 if sessions.is_empty() {
378 output.push_str("No active debug sessions.");
379 } else {
380 writeln!(output, "Debug sessions:").ok();
381 for session in sessions {
382 let session_ref = session.read(cx);
383 let session_id = session_ref.session_id();
384 let adapter = session_ref.adapter();
385 let label = session_ref.label();
386 let is_terminated = session_ref.is_terminated();
387
388 let status = if is_terminated {
389 "terminated"
390 } else if session_ref.is_building() {
391 "building"
392 } else {
393 "running"
394 };
395
396 let label_str = label.as_ref().map(|l| l.as_ref()).unwrap_or("unnamed");
397 writeln!(
398 output,
399 " - Session {} ({}): {} [{}]",
400 session_id.0, adapter, label_str, status
401 )
402 .ok();
403 }
404 }
405 output
406 })),
407
408 DebuggerOperation::Continue => cx.update(|cx| {
409 let session = Self::find_session(&project, input.session_id, cx)?;
410 let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;
411
412 session.update(cx, |session, cx| {
413 session.continue_thread(tid, cx);
414 });
415
416 Ok(format!("Continued execution of thread {}", tid.0))
417 }),
418
419 DebuggerOperation::Pause => cx.update(|cx| {
420 let session = Self::find_session(&project, input.session_id, cx)?;
421 let tid = Self::find_running_thread(&session, input.thread_id, cx)?;
422
423 session.update(cx, |session, cx| {
424 session.pause_thread(tid, cx);
425 });
426
427 Ok(format!("Paused thread {}", tid.0))
428 }),
429
430 DebuggerOperation::StepOver => cx.update(|cx| {
431 let session = Self::find_session(&project, input.session_id, cx)?;
432 let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;
433
434 session.update(cx, |session, cx| {
435 session.step_over(tid, SteppingGranularity::Line, cx);
436 });
437
438 Ok(format!("Stepped over on thread {}", tid.0))
439 }),
440
441 DebuggerOperation::StepIn => cx.update(|cx| {
442 let session = Self::find_session(&project, input.session_id, cx)?;
443 let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;
444
445 session.update(cx, |session, cx| {
446 session.step_in(tid, SteppingGranularity::Line, cx);
447 });
448
449 Ok(format!("Stepped into on thread {}", tid.0))
450 }),
451
452 DebuggerOperation::StepOut => cx.update(|cx| {
453 let session = Self::find_session(&project, input.session_id, cx)?;
454 let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;
455
456 session.update(cx, |session, cx| {
457 session.step_out(tid, SteppingGranularity::Line, cx);
458 });
459
460 Ok(format!("Stepped out on thread {}", tid.0))
461 }),
462
463 DebuggerOperation::GetStackTrace => cx.update(|cx| {
464 let session = Self::find_session(&project, input.session_id, cx)?;
465 let tid = Self::find_any_thread(&session, input.thread_id, cx)?;
466
467 let frames = session.update(cx, |session, cx| session.stack_frames(tid, cx))?;
468
469 let mut output = String::new();
470 if frames.is_empty() {
471 output.push_str("No stack frames available. The thread may be running.");
472 } else {
473 writeln!(output, "Stack trace for thread {}:", tid.0).ok();
474 for (i, frame) in frames.iter().enumerate() {
475 let location = frame
476 .dap
477 .source
478 .as_ref()
479 .and_then(|s| s.path.as_ref())
480 .map(|p| format!("{}:{}", p, frame.dap.line))
481 .unwrap_or_else(|| "unknown".to_string());
482
483 writeln!(output, " #{} {} at {}", i, frame.dap.name, location).ok();
484 }
485 }
486 Ok(output)
487 }),
488
489 DebuggerOperation::GetVariables => cx.update(|cx| {
490 let session = Self::find_session(&project, input.session_id, cx)?;
491 let tid = Self::find_stopped_thread(&session, input.thread_id, cx)?;
492 let frame_idx = input.frame_index.unwrap_or(0);
493
494 session.update(cx, |session, cx| {
495 let frames = session.stack_frames(tid, cx)?;
496
497 let frame = frames.get(frame_idx).ok_or_else(|| {
498 anyhow!(
499 "Stack frame index {} out of range (0-{})",
500 frame_idx,
501 frames.len().saturating_sub(1)
502 )
503 })?;
504
505 let frame_id = frame.dap.id;
506 let frame_name = frame.dap.name.clone();
507
508 let scopes: Vec<_> = session.scopes(frame_id, cx).to_vec();
509
510 let mut output = String::new();
511 if scopes.is_empty() {
512 output.push_str("No variables available in the current scope.");
513 } else {
514 writeln!(
515 output,
516 "Variables in frame #{} ({}):",
517 frame_idx, frame_name
518 )
519 .ok();
520
521 for scope in &scopes {
522 writeln!(output, "\n {}:", scope.name).ok();
523
524 let variables = session.variables(scope.variables_reference.into(), cx);
525 for var in variables {
526 let type_info = var
527 .type_
528 .as_ref()
529 .map(|t| format!(" ({})", t))
530 .unwrap_or_default();
531
532 writeln!(output, " {} = {}{}", var.name, var.value, type_info)
533 .ok();
534 }
535 }
536 }
537
538 Ok(output)
539 })
540 }),
541 }
542 }
543}
544
545impl AgentTool for DebuggerTool {
546 type Input = DebuggerToolInput;
547 type Output = String;
548
549 const NAME: &'static str = "debugger";
550
551 fn kind() -> acp::ToolKind {
552 acp::ToolKind::Execute
553 }
554
555 fn initial_title(
556 &self,
557 input: Result<Self::Input, serde_json::Value>,
558 _cx: &mut App,
559 ) -> SharedString {
560 match input {
561 Ok(input) => match input.operation {
562 DebuggerOperation::SetBreakpoint => {
563 if let (Some(path), Some(line)) = (&input.path, input.line) {
564 format!("Set breakpoint at {}:{}", MarkdownInlineCode(path), line).into()
565 } else {
566 "Set breakpoint".into()
567 }
568 }
569 DebuggerOperation::RemoveBreakpoint => {
570 if let (Some(path), Some(line)) = (&input.path, input.line) {
571 format!("Remove breakpoint at {}:{}", MarkdownInlineCode(path), line).into()
572 } else {
573 "Remove breakpoint".into()
574 }
575 }
576 DebuggerOperation::ListBreakpoints => "List breakpoints".into(),
577 DebuggerOperation::ListSessions => "List debug sessions".into(),
578 DebuggerOperation::Continue => "Continue execution".into(),
579 DebuggerOperation::Pause => "Pause execution".into(),
580 DebuggerOperation::StepOver => "Step over".into(),
581 DebuggerOperation::StepIn => "Step into".into(),
582 DebuggerOperation::StepOut => "Step out".into(),
583 DebuggerOperation::GetStackTrace => "Get stack trace".into(),
584 DebuggerOperation::GetVariables => "Get variables".into(),
585 },
586 Err(_) => "Debugger operation".into(),
587 }
588 }
589
590 fn run(
591 self: Arc<Self>,
592 input: ToolInput<Self::Input>,
593 _event_stream: ToolCallEventStream,
594 cx: &mut App,
595 ) -> Task<Result<Self::Output, Self::Output>> {
596 let project = self.project.clone();
597 cx.spawn(async move |cx| {
598 let input = input
599 .recv()
600 .await
601 .map_err(|e| format!("Failed to receive tool input: {e}"))?;
602 Self::run_operation(project, input, cx)
603 .await
604 .map_err(|e| e.to_string())
605 })
606 }
607}