Skip to main content

nucel_agent_sdk/
lib.rs

1//! Nucel Agent SDK — Unified
2//!
3//! One import for all providers. Swap coding agents via configuration.
4//!
5//! # Quick Start
6//!
7//! ```rust,no_run
8//! use nucel_agent_sdk::{AgentExecutor, ClaudeCodeExecutor, SpawnConfig};
9//! use std::path::Path;
10//!
11//! # async fn example() -> nucel_agent_sdk::Result<()> {
12//! let executor = ClaudeCodeExecutor::new();
13//!
14//! let session = executor.spawn(
15//!     Path::new("/my/repo"),
16//!     "Fix the failing tests",
17//!     &SpawnConfig {
18//!         model: Some("claude-opus-4-6".into()),
19//!         budget_usd: Some(5.0),
20//!         ..Default::default()
21//!     },
22//! ).await?;
23//!
24//! println!("Response: {}", session.query("Check if CI passes now").await?.content);
25//! session.close().await?;
26//! # Ok(())
27//! # }
28//! ```
29//!
30//! # Provider Selection
31//!
32//! ```rust,no_run
33//! use nucel_agent_sdk::*;
34//!
35//! # fn example() {
36//! // Via config string (like agent-operator does)
37//! let executor = build_executor("claude-code", None);
38//! let executor = build_executor("codex", Some("sk-...".into()));
39//! let executor = build_executor("opencode", Some("http://localhost:4096".into()));
40//! # }
41//! ```
42
43// Re-export core types.
44pub use nucel_agent_core::{
45    AgentCapabilities, AgentCost, AgentError, AgentExecutor, AgentResponse, AgentSession,
46    AvailabilityStatus, ExecutorType, PermissionMode, Result, SessionMetadata, SpawnConfig,
47};
48
49// Re-export provider executors.
50pub use nucel_agent_claude_code::ClaudeCodeExecutor;
51pub use nucel_agent_codex::CodexExecutor;
52pub use nucel_agent_opencode::OpencodeExecutor;
53
54/// Build an executor from a config string (like `providers.agent = "claude-code"`).
55///
56/// - `"claude-code"` → `ClaudeCodeExecutor`
57/// - `"codex"` → `CodexExecutor`
58/// - `"opencode"` → `OpencodeExecutor` (second arg is base URL)
59///
60/// Returns `None` for unknown providers.
61pub fn build_executor(
62    provider: &str,
63    api_key_or_url: Option<String>,
64) -> Option<Box<dyn AgentExecutor>> {
65    match provider {
66        "claude-code" | "claude_code" | "claudecode" => Some(Box::new(ClaudeCodeExecutor::new())),
67        "codex" => Some(Box::new(CodexExecutor::new())),
68        "opencode" => {
69            let mut exec = OpencodeExecutor::new();
70            if let Some(url) = api_key_or_url {
71                exec = OpencodeExecutor::with_base_url(url);
72            }
73            Some(Box::new(exec))
74        }
75        _ => None,
76    }
77}
78
79/// List all available provider names.
80pub fn available_providers() -> &'static [&'static str] {
81    &["claude-code", "codex", "opencode"]
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn build_claude_code_executor() {
90        let exec = build_executor("claude-code", None).unwrap();
91        assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
92    }
93
94    #[test]
95    fn build_codex_executor() {
96        let exec = build_executor("codex", None).unwrap();
97        assert_eq!(exec.executor_type(), ExecutorType::Codex);
98    }
99
100    #[test]
101    fn build_opencode_executor() {
102        let exec = build_executor("opencode", None).unwrap();
103        assert_eq!(exec.executor_type(), ExecutorType::OpenCode);
104    }
105
106    #[test]
107    fn build_opencode_with_url() {
108        let exec = build_executor("opencode", Some("http://my-server:8080".into())).unwrap();
109        assert_eq!(exec.executor_type(), ExecutorType::OpenCode);
110    }
111
112    #[test]
113    fn unknown_provider_returns_none() {
114        assert!(build_executor("gpt-4", None).is_none());
115    }
116
117    #[test]
118    fn claude_code_aliases_work() {
119        assert!(build_executor("claude_code", None).is_some());
120        assert!(build_executor("claudecode", None).is_some());
121    }
122
123    #[test]
124    fn available_providers_list() {
125        let providers = available_providers();
126        assert_eq!(providers.len(), 3);
127        assert!(providers.contains(&"claude-code"));
128        assert!(providers.contains(&"codex"));
129        assert!(providers.contains(&"opencode"));
130    }
131
132    #[test]
133    fn build_executor_empty_string_returns_none() {
134        assert!(build_executor("", None).is_none());
135    }
136
137    #[test]
138    fn build_executor_case_sensitive() {
139        assert!(build_executor("Claude-Code", None).is_none());
140        assert!(build_executor("CODEX", None).is_none());
141        assert!(build_executor("OpenCode", None).is_none());
142    }
143
144    #[test]
145    fn all_executors_have_capabilities() {
146        for provider in available_providers() {
147            let exec = build_executor(provider, None).unwrap();
148            let caps = exec.capabilities();
149            // All providers should support token usage
150            assert!(caps.token_usage, "{provider} should support token_usage");
151            // All providers should support autonomous mode
152            assert!(caps.autonomous_mode, "{provider} should support autonomous_mode");
153        }
154    }
155
156    #[test]
157    fn all_executors_report_availability() {
158        for provider in available_providers() {
159            let exec = build_executor(provider, None).unwrap();
160            let status = exec.availability();
161            // Either available or has a reason
162            if !status.available {
163                assert!(status.reason.is_some(), "{provider} unavailable but no reason");
164            }
165        }
166    }
167
168    #[test]
169    fn claude_code_api_key_ignored_by_build_executor() {
170        // build_executor for claude-code ignores the api_key_or_url param
171        let exec = build_executor("claude-code", Some("sk-test".into())).unwrap();
172        assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
173    }
174
175    #[test]
176    fn codex_api_key_ignored_by_build_executor() {
177        let exec = build_executor("codex", Some("sk-test".into())).unwrap();
178        assert_eq!(exec.executor_type(), ExecutorType::Codex);
179    }
180}