Skip to main content

gopher_mcp_rust/
agent.rs

1//! GopherAgent implementation.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4
5use crate::config::Config;
6use crate::error::{Error, Result};
7use crate::ffi::{self, AgentHandle};
8use crate::init;
9use crate::result::AgentResult;
10
11/// Default timeout for agent queries (60 seconds).
12const DEFAULT_TIMEOUT_MS: u64 = 60_000;
13
14/// A gopher-orch agent for running AI queries.
15pub struct GopherAgent {
16    handle: AgentHandle,
17    disposed: AtomicBool,
18}
19
20// Safety: The native handle is thread-safe according to the C++ implementation
21unsafe impl Send for GopherAgent {}
22unsafe impl Sync for GopherAgent {}
23
24impl GopherAgent {
25    /// Create a new GopherAgent with the given configuration.
26    pub fn create(config: Config) -> Result<Self> {
27        init()?;
28
29        let handle = if config.has_api_key() {
30            ffi::agent_create_by_api_key(
31                config.provider(),
32                config.model(),
33                config.api_key().unwrap_or(""),
34            )
35        } else if config.has_server_config() {
36            ffi::agent_create_by_json(
37                config.provider(),
38                config.model(),
39                config.server_config().unwrap_or(""),
40            )
41        } else {
42            return Err(Error::config(
43                "Either API key or server config must be provided",
44            ));
45        };
46
47        if handle.is_null() {
48            let err_msg = ffi::get_last_error();
49            ffi::clear_error();
50            let msg = if err_msg.is_empty() {
51                "Failed to create agent".to_string()
52            } else {
53                err_msg
54            };
55            return Err(Error::agent(msg));
56        }
57
58        Ok(GopherAgent {
59            handle,
60            disposed: AtomicBool::new(false),
61        })
62    }
63
64    /// Create a new GopherAgent with an API key.
65    pub fn create_with_api_key(provider: &str, model: &str, api_key: &str) -> Result<Self> {
66        let config = crate::ConfigBuilder::new()
67            .with_provider(provider)
68            .with_model(model)
69            .with_api_key(api_key)
70            .build();
71        Self::create(config)
72    }
73
74    /// Create a new GopherAgent with a server config.
75    pub fn create_with_server_config(
76        provider: &str,
77        model: &str,
78        server_config: &str,
79    ) -> Result<Self> {
80        let config = crate::ConfigBuilder::new()
81            .with_provider(provider)
82            .with_model(model)
83            .with_server_config(server_config)
84            .build();
85        Self::create(config)
86    }
87
88    /// Run a query against the agent with the default timeout (60 seconds).
89    pub fn run(&self, query: &str) -> Result<String> {
90        self.run_with_timeout(query, DEFAULT_TIMEOUT_MS)
91    }
92
93    /// Run a query against the agent with a custom timeout.
94    pub fn run_with_timeout(&self, query: &str, timeout_ms: u64) -> Result<String> {
95        self.ensure_not_disposed()?;
96
97        let response = ffi::agent_run(self.handle, query, timeout_ms);
98        if response.is_empty() {
99            Ok(format!("No response for query: \"{}\"", query))
100        } else {
101            Ok(response)
102        }
103    }
104
105    /// Run a query and return detailed result information.
106    pub fn run_detailed(&self, query: &str) -> AgentResult {
107        self.run_detailed_with_timeout(query, DEFAULT_TIMEOUT_MS)
108    }
109
110    /// Run a query with custom timeout and return detailed result.
111    pub fn run_detailed_with_timeout(&self, query: &str, timeout_ms: u64) -> AgentResult {
112        match self.run_with_timeout(query, timeout_ms) {
113            Ok(response) => AgentResult::success(response),
114            Err(Error::Timeout(msg)) => AgentResult::timeout(msg),
115            Err(e) => AgentResult::error(e.to_string()),
116        }
117    }
118
119    /// Check if the agent has been disposed.
120    pub fn is_disposed(&self) -> bool {
121        self.disposed.load(Ordering::SeqCst)
122    }
123
124    /// Ensure the agent has not been disposed.
125    fn ensure_not_disposed(&self) -> Result<()> {
126        if self.is_disposed() {
127            Err(Error::Disposed)
128        } else {
129            Ok(())
130        }
131    }
132
133    /// Dispose of the agent, releasing native resources.
134    fn dispose(&self) {
135        if self
136            .disposed
137            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
138            .is_ok()
139            && !self.handle.is_null()
140        {
141            ffi::agent_release(self.handle);
142        }
143    }
144}
145
146impl Drop for GopherAgent {
147    fn drop(&mut self) {
148        self.dispose();
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    const TEST_SERVER_CONFIG: &str = r#"{
157        "succeeded": true,
158        "code": 200000000,
159        "message": "success",
160        "data": {
161            "servers": [
162                {
163                    "version": "2025-01-09",
164                    "serverId": "1",
165                    "name": "test-server",
166                    "transport": "http_sse",
167                    "config": {"url": "http://127.0.0.1:9999/mcp", "headers": {}},
168                    "connectTimeout": 5000,
169                    "requestTimeout": 30000
170                }
171            ]
172        }
173    }"#;
174
175    fn skip_if_native_library_not_available() -> bool {
176        !ffi::is_available()
177    }
178
179    #[test]
180    fn test_create_with_empty_config() {
181        if skip_if_native_library_not_available() {
182            return;
183        }
184
185        let config = crate::ConfigBuilder::new().build();
186        let result = GopherAgent::create(config);
187        assert!(result.is_err());
188    }
189
190    #[test]
191    fn test_create_with_server_config() {
192        if skip_if_native_library_not_available() {
193            return;
194        }
195
196        let result = GopherAgent::create_with_server_config(
197            "AnthropicProvider",
198            "claude-3-haiku-20240307",
199            TEST_SERVER_CONFIG,
200        );
201
202        // May fail without API key, but should not panic
203        if let Ok(agent) = result {
204            assert!(!agent.is_disposed());
205        }
206    }
207
208    #[test]
209    fn test_disposed_after_drop() {
210        if skip_if_native_library_not_available() {
211            return;
212        }
213
214        let result = GopherAgent::create_with_server_config(
215            "AnthropicProvider",
216            "claude-3-haiku-20240307",
217            TEST_SERVER_CONFIG,
218        );
219
220        if let Ok(agent) = result {
221            assert!(!agent.is_disposed());
222            drop(agent);
223            // Agent is now disposed (dropped)
224        }
225    }
226}