Skip to main content

synwire_dap/
plugin.rs

1//! DAP plugin for the Synwire agent runtime.
2//!
3//! Integrates debug adapter management with the agent lifecycle,
4//! contributing DAP tools and signal routes for debug events.
5
6use std::collections::HashMap;
7use std::sync::Arc;
8
9use tokio::sync::RwLock;
10
11use synwire_core::agents::plugin::Plugin;
12use synwire_core::agents::signal::{Action, SignalKind, SignalRoute};
13use synwire_core::tools::Tool;
14
15use crate::client::{DapClient, DapSessionState};
16use crate::config::{DapAdapterConfig, DapPluginConfig};
17use crate::error::DapError;
18use crate::registry::DebugAdapterRegistry;
19use crate::tools::create_tools;
20use crate::transport::EventHandler;
21
22/// Plugin providing Debug Adapter Protocol integration for agents.
23///
24/// Manages debug adapter client lifecycles, registers DAP tools into
25/// the agent's tool registry, and routes debug events (stopped, terminated)
26/// as signals.
27pub struct DapPlugin {
28    clients: Arc<RwLock<HashMap<String, Arc<DapClient>>>>,
29    registry: Arc<DebugAdapterRegistry>,
30    config: DapPluginConfig,
31    tools: Vec<Arc<dyn Tool>>,
32}
33
34impl DapPlugin {
35    /// Create a new DAP plugin with the given configuration.
36    ///
37    /// The plugin starts with no active clients. Use [`start_adapter`](Self::start_adapter)
38    /// to spawn debug adapter processes.
39    #[must_use]
40    pub fn new(config: DapPluginConfig) -> Self {
41        Self {
42            clients: Arc::new(RwLock::new(HashMap::new())),
43            registry: Arc::new(DebugAdapterRegistry::with_builtins()),
44            config,
45            tools: Vec::new(),
46        }
47    }
48
49    /// Create a plugin with a custom adapter registry.
50    #[must_use]
51    pub fn with_registry(config: DapPluginConfig, registry: DebugAdapterRegistry) -> Self {
52        Self {
53            clients: Arc::new(RwLock::new(HashMap::new())),
54            registry: Arc::new(registry),
55            config,
56            tools: Vec::new(),
57        }
58    }
59
60    /// Start a debug adapter and register its tools.
61    ///
62    /// The adapter is identified by `session_id`. If a session with the same
63    /// ID already exists, it is disconnected first.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`DapError`] if the adapter fails to spawn or initialize.
68    pub async fn start_adapter(
69        &mut self,
70        session_id: &str,
71        adapter_config: &DapAdapterConfig,
72    ) -> Result<Arc<DapClient>, DapError> {
73        // Disconnect existing session if present.
74        {
75            let clients = self.clients.read().await;
76            if let Some(existing) = clients.get(session_id) {
77                let _ = existing.disconnect().await;
78            }
79        }
80
81        let clients_ref = Arc::clone(&self.clients);
82        let session_id_owned = session_id.to_string();
83
84        // Create event handler that updates client state on debug events.
85        let event_handler: EventHandler = Arc::new(move |event: serde_json::Value| {
86            let event_name = event
87                .get("event")
88                .and_then(serde_json::Value::as_str)
89                .unwrap_or("")
90                .to_string();
91
92            let clients_ref = Arc::clone(&clients_ref);
93            let session_id_owned = session_id_owned.clone();
94
95            // Spawn a task to handle state updates asynchronously.
96            drop(tokio::spawn(async move {
97                let clients = clients_ref.read().await;
98                if let Some(client) = clients.get(&session_id_owned) {
99                    match event_name.as_str() {
100                        "stopped" => {
101                            client.set_status(DapSessionState::Stopped).await;
102                            tracing::debug!(
103                                session_id = %session_id_owned,
104                                "Debug session stopped (breakpoint/step)"
105                            );
106                        }
107                        "terminated" => {
108                            client.set_status(DapSessionState::Terminated).await;
109                            tracing::debug!(
110                                session_id = %session_id_owned,
111                                "Debug session terminated"
112                            );
113                        }
114                        "continued" => {
115                            client.set_status(DapSessionState::Running).await;
116                        }
117                        _ => {}
118                    }
119                }
120            }));
121        });
122
123        let client = DapClient::start(adapter_config, event_handler)?;
124        client.initialize().await?;
125
126        let client = Arc::new(client);
127
128        // Register client.
129        {
130            let mut clients = self.clients.write().await;
131            let _ = clients.insert(session_id.to_string(), Arc::clone(&client));
132        }
133
134        // Create tools for this client.
135        let new_tools = create_tools(Arc::clone(&client))
136            .map_err(|e| DapError::InitializationFailed(format!("failed to create tools: {e}")))?;
137        self.tools = new_tools;
138
139        tracing::debug!(session_id, "DAP adapter started and initialized");
140
141        Ok(client)
142    }
143
144    /// Get a client by session ID.
145    pub async fn client(&self, session_id: &str) -> Option<Arc<DapClient>> {
146        let clients = self.clients.read().await;
147        clients.get(session_id).cloned()
148    }
149
150    /// Disconnect all active sessions (called on plugin shutdown).
151    pub async fn disconnect_all(&self) {
152        let clients = self.clients.read().await;
153        for (session_id, client) in clients.iter() {
154            if let Err(e) = client.disconnect().await {
155                tracing::warn!(
156                    session_id,
157                    error = %e,
158                    "Failed to disconnect DAP session"
159                );
160            }
161        }
162    }
163
164    /// Access the adapter registry.
165    #[must_use]
166    pub fn registry(&self) -> &DebugAdapterRegistry {
167        &self.registry
168    }
169
170    /// Access the plugin configuration.
171    #[must_use]
172    pub const fn config(&self) -> &DapPluginConfig {
173        &self.config
174    }
175}
176
177impl Plugin for DapPlugin {
178    #[allow(clippy::unnecessary_literal_bound)] // Trait signature requires `&str`.
179    fn name(&self) -> &str {
180        "dap"
181    }
182
183    fn tools(&self) -> Vec<Arc<dyn Tool>> {
184        self.tools.clone()
185    }
186
187    fn signal_routes(&self) -> Vec<SignalRoute> {
188        vec![
189            SignalRoute::new(
190                SignalKind::Custom("dap_stopped".into()),
191                Action::Continue,
192                0,
193            ),
194            SignalRoute::new(
195                SignalKind::Custom("dap_terminated".into()),
196                Action::Continue,
197                0,
198            ),
199        ]
200    }
201}