Skip to main content

synwire_dap/
tools.rs

1//! DAP tool definitions exposed to the agent tool registry.
2//!
3//! Each tool wraps a [`DapClient`] method and converts errors into
4//! [`SynwireError::Tool`] for the agent runtime.
5
6use std::sync::Arc;
7
8use synwire_core::error::{SynwireError, ToolError};
9use synwire_core::tools::{StructuredTool, Tool, ToolOutput, ToolSchema};
10
11use crate::client::DapClient;
12
13/// Convert a [`crate::error::DapError`] into a [`SynwireError`].
14#[allow(clippy::needless_pass_by_value)] // Required by `map_err` signature.
15fn dap_err(e: crate::error::DapError) -> SynwireError {
16    SynwireError::Tool(ToolError::InvocationFailed {
17        message: e.to_string(),
18    })
19}
20
21/// Convert a serialization error into a [`SynwireError`].
22#[allow(clippy::needless_pass_by_value)] // Required by `map_err` signature.
23fn json_err(e: serde_json::Error) -> SynwireError {
24    SynwireError::Tool(ToolError::InvocationFailed {
25        message: e.to_string(),
26    })
27}
28
29/// Create all 14 DAP tools bound to the given client.
30///
31/// # Errors
32///
33/// Returns [`SynwireError`] if any tool fails validation (should not happen
34/// with the hard-coded names).
35pub fn create_tools(client: Arc<DapClient>) -> Result<Vec<Arc<dyn Tool>>, SynwireError> {
36    let tools: Vec<Arc<dyn Tool>> = vec![
37        Arc::new(dap_status_tool(Arc::clone(&client))?),
38        Arc::new(dap_launch_tool(Arc::clone(&client))?),
39        Arc::new(dap_attach_tool(Arc::clone(&client))?),
40        Arc::new(dap_set_breakpoints_tool(Arc::clone(&client))?),
41        Arc::new(dap_continue_tool(Arc::clone(&client))?),
42        Arc::new(dap_step_over_tool(Arc::clone(&client))?),
43        Arc::new(dap_step_in_tool(Arc::clone(&client))?),
44        Arc::new(dap_step_out_tool(Arc::clone(&client))?),
45        Arc::new(dap_pause_tool(Arc::clone(&client))?),
46        Arc::new(dap_threads_tool(Arc::clone(&client))?),
47        Arc::new(dap_stack_trace_tool(Arc::clone(&client))?),
48        Arc::new(dap_variables_tool(Arc::clone(&client))?),
49        Arc::new(dap_evaluate_tool(Arc::clone(&client))?),
50        Arc::new(dap_disconnect_tool(client)?),
51    ];
52    Ok(tools)
53}
54
55fn dap_status_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
56    StructuredTool::builder()
57        .name("debug.status")
58        .description("Show the current debug session state, capabilities, and active breakpoints")
59        .schema(ToolSchema {
60            name: "debug.status".into(),
61            description:
62                "Show the current debug session state, capabilities, and active breakpoints".into(),
63            parameters: serde_json::json!({
64                "type": "object",
65                "properties": {},
66                "additionalProperties": false,
67            }),
68        })
69        .func(move |_input| {
70            let client = Arc::clone(&client);
71            Box::pin(async move {
72                let state = client.status().await;
73                let caps = client.capabilities().await;
74                let bps = client.active_breakpoints().await;
75
76                let result = serde_json::json!({
77                    "state": format!("{state}"),
78                    "capabilities": caps,
79                    "active_breakpoints": bps,
80                });
81
82                let content = serde_json::to_string_pretty(&result).map_err(json_err)?;
83                Ok(ToolOutput {
84                    content,
85                    ..Default::default()
86                })
87            })
88        })
89        .build()
90}
91
92fn dap_launch_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
93    StructuredTool::builder()
94        .name("debug.launch")
95        .description(
96            "Launch a program under the debugger. Pass launch configuration as JSON arguments.",
97        )
98        .schema(ToolSchema {
99            name: "debug.launch".into(),
100            description: "Launch a program under the debugger".into(),
101            parameters: serde_json::json!({
102                "type": "object",
103                "properties": {
104                    "program": {
105                        "type": "string",
106                        "description": "Path to the program to debug"
107                    },
108                    "args": {
109                        "type": "array",
110                        "items": { "type": "string" },
111                        "description": "Command-line arguments for the program"
112                    },
113                    "cwd": {
114                        "type": "string",
115                        "description": "Working directory for the program"
116                    },
117                    "env": {
118                        "type": "object",
119                        "description": "Environment variables for the program"
120                    },
121                    "stopOnEntry": {
122                        "type": "boolean",
123                        "description": "Whether to stop at the program entry point"
124                    }
125                },
126                "required": ["program"],
127            }),
128        })
129        .func(move |input| {
130            let client = Arc::clone(&client);
131            Box::pin(async move {
132                client.launch(input).await.map_err(dap_err)?;
133                Ok(ToolOutput {
134                    content: "Program launched successfully under debugger.".into(),
135                    ..Default::default()
136                })
137            })
138        })
139        .build()
140}
141
142fn dap_attach_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
143    StructuredTool::builder()
144        .name("debug.attach")
145        .description("Attach to a running process for debugging")
146        .schema(ToolSchema {
147            name: "debug.attach".into(),
148            description: "Attach to a running process for debugging".into(),
149            parameters: serde_json::json!({
150                "type": "object",
151                "properties": {
152                    "processId": {
153                        "type": "integer",
154                        "description": "Process ID to attach to"
155                    },
156                    "port": {
157                        "type": "integer",
158                        "description": "Port to connect to (for remote debugging)"
159                    },
160                    "host": {
161                        "type": "string",
162                        "description": "Host for remote debugging"
163                    }
164                },
165            }),
166        })
167        .func(move |input| {
168            let client = Arc::clone(&client);
169            Box::pin(async move {
170                client.attach(input).await.map_err(dap_err)?;
171                Ok(ToolOutput {
172                    content: "Attached to process successfully.".into(),
173                    ..Default::default()
174                })
175            })
176        })
177        .build()
178}
179
180fn dap_set_breakpoints_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
181    StructuredTool::builder()
182        .name("debug.set_breakpoints")
183        .description("Set breakpoints in a source file at specified line numbers")
184        .schema(ToolSchema {
185            name: "debug.set_breakpoints".into(),
186            description: "Set breakpoints in a source file at specified line numbers".into(),
187            parameters: serde_json::json!({
188                "type": "object",
189                "properties": {
190                    "source_path": {
191                        "type": "string",
192                        "description": "Absolute path to the source file"
193                    },
194                    "lines": {
195                        "type": "array",
196                        "items": { "type": "integer" },
197                        "description": "Line numbers to set breakpoints on"
198                    }
199                },
200                "required": ["source_path", "lines"],
201            }),
202        })
203        .func(move |input| {
204            let client = Arc::clone(&client);
205            Box::pin(async move {
206                let source_path = input
207                    .get("source_path")
208                    .and_then(serde_json::Value::as_str)
209                    .ok_or_else(|| {
210                        dap_err(crate::error::DapError::Transport(
211                            "missing source_path parameter".into(),
212                        ))
213                    })?;
214
215                let lines: Vec<i64> = input
216                    .get("lines")
217                    .and_then(serde_json::Value::as_array)
218                    .map(|arr| arr.iter().filter_map(serde_json::Value::as_i64).collect())
219                    .unwrap_or_default();
220
221                let result = client
222                    .set_breakpoints(source_path, &lines)
223                    .await
224                    .map_err(dap_err)?;
225
226                let content = serde_json::to_string_pretty(&serde_json::json!({
227                    "breakpoints": result,
228                }))
229                .map_err(json_err)?;
230
231                Ok(ToolOutput {
232                    content,
233                    ..Default::default()
234                })
235            })
236        })
237        .build()
238}
239
240fn dap_continue_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
241    StructuredTool::builder()
242        .name("debug.continue")
243        .description("Continue execution of a paused thread")
244        .schema(ToolSchema {
245            name: "debug.continue".into(),
246            description: "Continue execution of a paused thread".into(),
247            parameters: serde_json::json!({
248                "type": "object",
249                "properties": {
250                    "thread_id": {
251                        "type": "integer",
252                        "description": "Thread ID to continue (use debug.threads to list)"
253                    }
254                },
255                "required": ["thread_id"],
256            }),
257        })
258        .func(move |input| {
259            let client = Arc::clone(&client);
260            Box::pin(async move {
261                let thread_id = input
262                    .get("thread_id")
263                    .and_then(serde_json::Value::as_i64)
264                    .ok_or_else(|| {
265                        dap_err(crate::error::DapError::Transport(
266                            "missing thread_id parameter".into(),
267                        ))
268                    })?;
269
270                client
271                    .continue_execution(thread_id)
272                    .await
273                    .map_err(dap_err)?;
274                Ok(ToolOutput {
275                    content: format!("Thread {thread_id} resumed."),
276                    ..Default::default()
277                })
278            })
279        })
280        .build()
281}
282
283fn dap_step_over_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
284    StructuredTool::builder()
285        .name("debug.step_over")
286        .description("Step over the current line (next) for a thread")
287        .schema(ToolSchema {
288            name: "debug.step_over".into(),
289            description: "Step over the current line (next) for a thread".into(),
290            parameters: serde_json::json!({
291                "type": "object",
292                "properties": {
293                    "thread_id": {
294                        "type": "integer",
295                        "description": "Thread ID to step over"
296                    }
297                },
298                "required": ["thread_id"],
299            }),
300        })
301        .func(move |input| {
302            let client = Arc::clone(&client);
303            Box::pin(async move {
304                let thread_id = input
305                    .get("thread_id")
306                    .and_then(serde_json::Value::as_i64)
307                    .ok_or_else(|| {
308                        dap_err(crate::error::DapError::Transport(
309                            "missing thread_id parameter".into(),
310                        ))
311                    })?;
312
313                client.next(thread_id).await.map_err(dap_err)?;
314                Ok(ToolOutput {
315                    content: format!("Thread {thread_id} stepped over."),
316                    ..Default::default()
317                })
318            })
319        })
320        .build()
321}
322
323fn dap_step_in_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
324    StructuredTool::builder()
325        .name("debug.step_in")
326        .description("Step into the current function call for a thread")
327        .schema(ToolSchema {
328            name: "debug.step_in".into(),
329            description: "Step into the current function call for a thread".into(),
330            parameters: serde_json::json!({
331                "type": "object",
332                "properties": {
333                    "thread_id": {
334                        "type": "integer",
335                        "description": "Thread ID to step into"
336                    }
337                },
338                "required": ["thread_id"],
339            }),
340        })
341        .func(move |input| {
342            let client = Arc::clone(&client);
343            Box::pin(async move {
344                let thread_id = input
345                    .get("thread_id")
346                    .and_then(serde_json::Value::as_i64)
347                    .ok_or_else(|| {
348                        dap_err(crate::error::DapError::Transport(
349                            "missing thread_id parameter".into(),
350                        ))
351                    })?;
352
353                client.step_in(thread_id).await.map_err(dap_err)?;
354                Ok(ToolOutput {
355                    content: format!("Thread {thread_id} stepped in."),
356                    ..Default::default()
357                })
358            })
359        })
360        .build()
361}
362
363fn dap_step_out_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
364    StructuredTool::builder()
365        .name("debug.step_out")
366        .description("Step out of the current function for a thread")
367        .schema(ToolSchema {
368            name: "debug.step_out".into(),
369            description: "Step out of the current function for a thread".into(),
370            parameters: serde_json::json!({
371                "type": "object",
372                "properties": {
373                    "thread_id": {
374                        "type": "integer",
375                        "description": "Thread ID to step out of"
376                    }
377                },
378                "required": ["thread_id"],
379            }),
380        })
381        .func(move |input| {
382            let client = Arc::clone(&client);
383            Box::pin(async move {
384                let thread_id = input
385                    .get("thread_id")
386                    .and_then(serde_json::Value::as_i64)
387                    .ok_or_else(|| {
388                        dap_err(crate::error::DapError::Transport(
389                            "missing thread_id parameter".into(),
390                        ))
391                    })?;
392
393                client.step_out(thread_id).await.map_err(dap_err)?;
394                Ok(ToolOutput {
395                    content: format!("Thread {thread_id} stepped out."),
396                    ..Default::default()
397                })
398            })
399        })
400        .build()
401}
402
403fn dap_pause_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
404    StructuredTool::builder()
405        .name("debug.pause")
406        .description("Pause execution of a running thread")
407        .schema(ToolSchema {
408            name: "debug.pause".into(),
409            description: "Pause execution of a running thread".into(),
410            parameters: serde_json::json!({
411                "type": "object",
412                "properties": {
413                    "thread_id": {
414                        "type": "integer",
415                        "description": "Thread ID to pause"
416                    }
417                },
418                "required": ["thread_id"],
419            }),
420        })
421        .func(move |input| {
422            let client = Arc::clone(&client);
423            Box::pin(async move {
424                let thread_id = input
425                    .get("thread_id")
426                    .and_then(serde_json::Value::as_i64)
427                    .ok_or_else(|| {
428                        dap_err(crate::error::DapError::Transport(
429                            "missing thread_id parameter".into(),
430                        ))
431                    })?;
432
433                client.pause(thread_id).await.map_err(dap_err)?;
434                Ok(ToolOutput {
435                    content: format!("Thread {thread_id} paused."),
436                    ..Default::default()
437                })
438            })
439        })
440        .build()
441}
442
443fn dap_threads_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
444    StructuredTool::builder()
445        .name("debug.threads")
446        .description("List all threads in the debuggee process")
447        .schema(ToolSchema {
448            name: "debug.threads".into(),
449            description: "List all threads in the debuggee process".into(),
450            parameters: serde_json::json!({
451                "type": "object",
452                "properties": {},
453                "additionalProperties": false,
454            }),
455        })
456        .func(move |_input| {
457            let client = Arc::clone(&client);
458            Box::pin(async move {
459                let threads = client.threads().await.map_err(dap_err)?;
460
461                let content = serde_json::to_string_pretty(&serde_json::json!({
462                    "threads": threads,
463                }))
464                .map_err(json_err)?;
465
466                Ok(ToolOutput {
467                    content,
468                    ..Default::default()
469                })
470            })
471        })
472        .build()
473}
474
475fn dap_stack_trace_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
476    StructuredTool::builder()
477        .name("debug.stack_trace")
478        .description("Get the stack trace for a specific thread")
479        .schema(ToolSchema {
480            name: "debug.stack_trace".into(),
481            description: "Get the stack trace for a specific thread".into(),
482            parameters: serde_json::json!({
483                "type": "object",
484                "properties": {
485                    "thread_id": {
486                        "type": "integer",
487                        "description": "Thread ID to get stack trace for"
488                    }
489                },
490                "required": ["thread_id"],
491            }),
492        })
493        .func(move |input| {
494            let client = Arc::clone(&client);
495            Box::pin(async move {
496                let thread_id = input
497                    .get("thread_id")
498                    .and_then(serde_json::Value::as_i64)
499                    .ok_or_else(|| {
500                        dap_err(crate::error::DapError::Transport(
501                            "missing thread_id parameter".into(),
502                        ))
503                    })?;
504
505                let frames = client.stack_trace(thread_id).await.map_err(dap_err)?;
506
507                let content = serde_json::to_string_pretty(&serde_json::json!({
508                    "stack_frames": frames,
509                }))
510                .map_err(json_err)?;
511
512                Ok(ToolOutput {
513                    content,
514                    ..Default::default()
515                })
516            })
517        })
518        .build()
519}
520
521fn dap_variables_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
522    StructuredTool::builder()
523        .name("debug.variables")
524        .description("Get variables for a scope or structured variable reference. Use debug.stack_trace and then debug.scopes to get variable references.")
525        .schema(ToolSchema {
526            name: "debug.variables".into(),
527            description: "Get variables for a scope or structured variable reference".into(),
528            parameters: serde_json::json!({
529                "type": "object",
530                "properties": {
531                    "variables_reference": {
532                        "type": "integer",
533                        "description": "Variables reference ID (from scopes or structured variables)"
534                    }
535                },
536                "required": ["variables_reference"],
537            }),
538        })
539        .func(move |input| {
540            let client = Arc::clone(&client);
541            Box::pin(async move {
542                let variables_ref = input
543                    .get("variables_reference")
544                    .and_then(serde_json::Value::as_i64)
545                    .ok_or_else(|| dap_err(crate::error::DapError::Transport(
546                        "missing variables_reference parameter".into(),
547                    )))?;
548
549                let variables = client.variables(variables_ref).await.map_err(dap_err)?;
550
551                let content = serde_json::to_string_pretty(&serde_json::json!({
552                    "variables": variables,
553                }))
554                .map_err(json_err)?;
555
556                Ok(ToolOutput {
557                    content,
558                    ..Default::default()
559                })
560            })
561        })
562        .build()
563}
564
565fn dap_evaluate_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
566    StructuredTool::builder()
567        .name("debug.evaluate")
568        .description("Evaluate an expression in the debuggee context (REPL mode)")
569        .schema(ToolSchema {
570            name: "debug.evaluate".into(),
571            description: "Evaluate an expression in the debuggee context".into(),
572            parameters: serde_json::json!({
573                "type": "object",
574                "properties": {
575                    "expression": {
576                        "type": "string",
577                        "description": "Expression to evaluate"
578                    },
579                    "frame_id": {
580                        "type": "integer",
581                        "description": "Optional stack frame ID for context"
582                    }
583                },
584                "required": ["expression"],
585            }),
586        })
587        .func(move |input| {
588            let client = Arc::clone(&client);
589            Box::pin(async move {
590                let expression = input
591                    .get("expression")
592                    .and_then(serde_json::Value::as_str)
593                    .ok_or_else(|| {
594                        dap_err(crate::error::DapError::Transport(
595                            "missing expression parameter".into(),
596                        ))
597                    })?;
598
599                let frame_id = input.get("frame_id").and_then(serde_json::Value::as_i64);
600
601                let result = client
602                    .evaluate(expression, frame_id)
603                    .await
604                    .map_err(dap_err)?;
605
606                let content = serde_json::to_string_pretty(&result).map_err(json_err)?;
607
608                Ok(ToolOutput {
609                    content,
610                    ..Default::default()
611                })
612            })
613        })
614        .build()
615}
616
617fn dap_disconnect_tool(client: Arc<DapClient>) -> Result<StructuredTool, SynwireError> {
618    StructuredTool::builder()
619        .name("debug.disconnect")
620        .description("Disconnect from the debug session and terminate the debuggee")
621        .schema(ToolSchema {
622            name: "debug.disconnect".into(),
623            description: "Disconnect from the debug session and terminate the debuggee".into(),
624            parameters: serde_json::json!({
625                "type": "object",
626                "properties": {},
627                "additionalProperties": false,
628            }),
629        })
630        .func(move |_input| {
631            let client = Arc::clone(&client);
632            Box::pin(async move {
633                client.disconnect().await.map_err(dap_err)?;
634                Ok(ToolOutput {
635                    content: "Debug session disconnected.".into(),
636                    ..Default::default()
637                })
638            })
639        })
640        .build()
641}