mod process;
mod protocol;
use std::path::Path;
use std::sync::Arc;
use async_trait::async_trait;
use tokio::sync::Mutex;
use nucel_agent_core::{
AgentCapabilities, AgentCost, AgentError, AgentExecutor, AgentResponse, AgentSession,
AvailabilityStatus, ExecutorType, Result, SessionImpl, SpawnConfig,
};
use process::ClaudeProcess;
pub struct ClaudeCodeExecutor {
api_key: Option<String>,
}
impl ClaudeCodeExecutor {
pub fn new() -> Self {
Self { api_key: None }
}
pub fn with_api_key(api_key: impl Into<String>) -> Self {
Self {
api_key: Some(api_key.into()),
}
}
fn check_cli_available() -> bool {
std::process::Command::new("which")
.arg("claude")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
}
impl Default for ClaudeCodeExecutor {
fn default() -> Self {
Self::new()
}
}
struct ClaudeSessionImpl {
process: Arc<Mutex<ClaudeProcess>>,
cost: Arc<std::sync::Mutex<AgentCost>>,
budget: f64,
}
#[async_trait]
impl SessionImpl for ClaudeSessionImpl {
async fn query(&self, prompt: &str) -> Result<AgentResponse> {
{
let c = self.cost.lock().unwrap();
if c.total_usd >= self.budget {
return Err(AgentError::BudgetExceeded {
limit: self.budget,
spent: c.total_usd,
});
}
}
let mut proc = self.process.lock().await;
proc.send_query(prompt).await?;
let resp = proc.read_response(self.budget).await?;
{
let mut c = self.cost.lock().unwrap();
c.input_tokens += resp.cost.input_tokens;
c.output_tokens += resp.cost.output_tokens;
c.total_usd += resp.cost.total_usd;
}
Ok(resp)
}
async fn total_cost(&self) -> Result<AgentCost> {
Ok(self.cost.lock().unwrap().clone())
}
async fn close(&self) -> Result<()> {
let mut proc = self.process.lock().await;
proc.shutdown().await
}
}
#[async_trait]
impl AgentExecutor for ClaudeCodeExecutor {
fn executor_type(&self) -> ExecutorType {
ExecutorType::ClaudeCode
}
async fn spawn(
&self,
working_dir: &Path,
prompt: &str,
config: &SpawnConfig,
) -> Result<AgentSession> {
let cost = Arc::new(std::sync::Mutex::new(AgentCost::default()));
let budget = config.budget_usd.unwrap_or(f64::MAX);
if budget <= 0.0 {
return Err(AgentError::BudgetExceeded {
limit: budget,
spent: 0.0,
});
}
let mut proc = ClaudeProcess::start(
working_dir,
prompt,
config,
self.api_key.as_deref(),
)
.await?;
let session_id = proc.session_id().to_string();
let response = proc.read_response(budget).await?;
{
let mut c = cost.lock().unwrap();
*c = response.cost.clone();
}
let inner = Arc::new(ClaudeSessionImpl {
process: Arc::new(Mutex::new(proc)),
cost: cost.clone(),
budget,
});
Ok(AgentSession::new(
session_id,
ExecutorType::ClaudeCode,
working_dir.to_path_buf(),
config.model.clone(),
inner,
))
}
async fn resume(
&self,
working_dir: &Path,
session_id: &str,
prompt: &str,
config: &SpawnConfig,
) -> Result<AgentSession> {
let cost = Arc::new(std::sync::Mutex::new(AgentCost::default()));
let budget = config.budget_usd.unwrap_or(f64::MAX);
if budget <= 0.0 {
return Err(AgentError::BudgetExceeded {
limit: budget,
spent: 0.0,
});
}
let mut proc = ClaudeProcess::start_resume(
working_dir,
session_id,
prompt,
config,
self.api_key.as_deref(),
)
.await?;
let resumed_session_id = proc.session_id().to_string();
let response = proc.read_response(budget).await?;
{
let mut c = cost.lock().unwrap();
*c = response.cost.clone();
}
let inner = Arc::new(ClaudeSessionImpl {
process: Arc::new(Mutex::new(proc)),
cost: cost.clone(),
budget,
});
Ok(AgentSession::new(
resumed_session_id,
ExecutorType::ClaudeCode,
working_dir.to_path_buf(),
config.model.clone(),
inner,
))
}
fn capabilities(&self) -> AgentCapabilities {
AgentCapabilities {
session_resume: true,
token_usage: true,
mcp_support: true,
autonomous_mode: true,
structured_output: false,
}
}
fn availability(&self) -> AvailabilityStatus {
if Self::check_cli_available() {
AvailabilityStatus {
available: true,
reason: None,
}
} else {
AvailabilityStatus {
available: false,
reason: Some(
"`claude` CLI not found. Install: npm install -g @anthropic-ai/claude-code"
.to_string(),
),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn executor_type_is_claude_code() {
let exec = ClaudeCodeExecutor::new();
assert_eq!(exec.executor_type(), ExecutorType::ClaudeCode);
}
#[test]
fn capabilities_declares_autonomous_mode() {
let exec = ClaudeCodeExecutor::new();
let caps = exec.capabilities();
assert!(caps.autonomous_mode);
assert!(caps.token_usage);
assert!(caps.mcp_support);
assert!(caps.session_resume, "Claude Code supports --resume flag");
}
#[tokio::test]
async fn budget_zero_returns_error_before_spawn() {
let exec = ClaudeCodeExecutor::new();
let result = exec
.spawn(
Path::new("/tmp"),
"test",
&SpawnConfig {
budget_usd: Some(0.0),
..Default::default()
},
)
.await;
assert!(matches!(result, Err(AgentError::BudgetExceeded { .. })));
}
}