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//! # Runnable examples
44//!
45//! - [`examples/claude_basic.rs`](https://github.com/nucel-dev/agent-sdk/blob/main/crates/unified/examples/claude_basic.rs) — spawn + query + close against Claude Code.
46//! - [`examples/codex_resume.rs`](https://github.com/nucel-dev/agent-sdk/blob/main/crates/unified/examples/codex_resume.rs) — spawn, save the `session_id`, resume, query.
47//! - [`examples/opencode_http.rs`](https://github.com/nucel-dev/agent-sdk/blob/main/crates/unified/examples/opencode_http.rs) — point at a local `opencode serve` and send a prompt.
48//! - [`examples/build_executor.rs`](https://github.com/nucel-dev/agent-sdk/blob/main/crates/unified/examples/build_executor.rs) — runtime provider selection via [`build_executor`].
49//!
50//! Run any of them with:
51//!
52//! ```bash
53//! cargo run -p nucel-agent-sdk --example claude_basic
54//! ```
55//!
56//! # See also
57//!
58//! - [Workspace README](https://github.com/nucel-dev/agent-sdk#readme)
59//! - [`docs/tutorials/`](https://github.com/nucel-dev/agent-sdk/tree/main/docs/tutorials) — getting started, multi-turn, budget control, provider comparison.
60//! - [`CONTRIBUTING.md`](https://github.com/nucel-dev/agent-sdk/blob/main/CONTRIBUTING.md) — adding a new provider.
61
62#![cfg_attr(docsrs, feature(doc_cfg))]
63
64// Re-export core types.
65pub use nucel_agent_core::{
66    AgentCapabilities, AgentCost, AgentError, AgentExecutor, AgentResponse, AgentSession,
67    AvailabilityStatus, CachePoint, EventStream, ExecutorType, HookConfig, HookHandler,
68    MessageEvent, PermissionMode, Result, SessionImpl, SessionMetadata, SpawnConfig,
69};
70
71// Re-export provider executors.
72pub use nucel_agent_claude_code::ClaudeCodeExecutor;
73pub use nucel_agent_codex::CodexExecutor;
74pub use nucel_agent_opencode::OpencodeExecutor;
75
76/// Build an executor from a config string (like `providers.agent = "claude-code"`).
77///
78/// - `"claude-code"` → `ClaudeCodeExecutor`
79/// - `"codex"` → `CodexExecutor`
80/// - `"opencode"` → `OpencodeExecutor` (second arg is base URL)
81///
82/// Returns `None` for unknown providers.
83pub fn build_executor(
84    provider: &str,
85    api_key_or_url: Option<String>,
86) -> Option<Box<dyn AgentExecutor>> {
87    match provider {
88        "claude-code" | "claude_code" | "claudecode" => Some(Box::new(ClaudeCodeExecutor::new())),
89        "codex" => Some(Box::new(CodexExecutor::new())),
90        "opencode" => {
91            let mut exec = OpencodeExecutor::new();
92            if let Some(url) = api_key_or_url {
93                exec = OpencodeExecutor::with_base_url(url);
94            }
95            Some(Box::new(exec))
96        }
97        _ => None,
98    }
99}
100
101/// List all available provider names.
102pub fn available_providers() -> &'static [&'static str] {
103    &["claude-code", "codex", "opencode"]
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn build_claude_code_executor() {
112        let exec = build_executor("claude-code", None).unwrap();
113        assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
114    }
115
116    #[test]
117    fn build_codex_executor() {
118        let exec = build_executor("codex", None).unwrap();
119        assert_eq!(exec.executor_type(), ExecutorType::Codex);
120    }
121
122    #[test]
123    fn build_opencode_executor() {
124        let exec = build_executor("opencode", None).unwrap();
125        assert_eq!(exec.executor_type(), ExecutorType::OpenCode);
126    }
127
128    #[test]
129    fn build_opencode_with_url() {
130        let exec = build_executor("opencode", Some("http://my-server:8080".into())).unwrap();
131        assert_eq!(exec.executor_type(), ExecutorType::OpenCode);
132    }
133
134    #[test]
135    fn unknown_provider_returns_none() {
136        assert!(build_executor("gpt-4", None).is_none());
137    }
138
139    #[test]
140    fn claude_code_aliases_work() {
141        assert!(build_executor("claude_code", None).is_some());
142        assert!(build_executor("claudecode", None).is_some());
143    }
144
145    #[test]
146    fn available_providers_list() {
147        let providers = available_providers();
148        assert_eq!(providers.len(), 3);
149        assert!(providers.contains(&"claude-code"));
150        assert!(providers.contains(&"codex"));
151        assert!(providers.contains(&"opencode"));
152    }
153
154    #[test]
155    fn build_executor_empty_string_returns_none() {
156        assert!(build_executor("", None).is_none());
157    }
158
159    #[test]
160    fn build_executor_case_sensitive() {
161        assert!(build_executor("Claude-Code", None).is_none());
162        assert!(build_executor("CODEX", None).is_none());
163        assert!(build_executor("OpenCode", None).is_none());
164    }
165
166    #[test]
167    fn all_executors_have_capabilities() {
168        for provider in available_providers() {
169            let exec = build_executor(provider, None).unwrap();
170            let caps = exec.capabilities();
171            // All providers should support token usage
172            assert!(caps.token_usage, "{provider} should support token_usage");
173            // All providers should support autonomous mode
174            assert!(caps.autonomous_mode, "{provider} should support autonomous_mode");
175        }
176    }
177
178    #[test]
179    fn all_executors_report_availability() {
180        for provider in available_providers() {
181            let exec = build_executor(provider, None).unwrap();
182            let status = exec.availability();
183            // Either available or has a reason
184            if !status.available {
185                assert!(status.reason.is_some(), "{provider} unavailable but no reason");
186            }
187        }
188    }
189
190    #[test]
191    fn claude_code_api_key_ignored_by_build_executor() {
192        // build_executor for claude-code ignores the api_key_or_url param
193        let exec = build_executor("claude-code", Some("sk-test".into())).unwrap();
194        assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
195    }
196
197    #[test]
198    fn codex_api_key_ignored_by_build_executor() {
199        let exec = build_executor("codex", Some("sk-test".into())).unwrap();
200        assert_eq!(exec.executor_type(), ExecutorType::Codex);
201    }
202}