use std::path::Path;
#[derive(Debug, Clone, Default)]
pub(crate) struct ProviderOptions {
pub allowed_tools: Vec<String>,
pub model: Option<String>,
pub system_prompt: Option<String>,
pub agent_name: Option<String>,
pub session_id: Option<String>,
pub continue_session: bool,
pub max_budget: Option<f64>,
pub worktree_name: Option<String>,
pub claude_stream_json: bool,
pub codex_json: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SameThreadContinuation {
Unsupported,
ExplicitSessionId,
ProviderAssignedId,
}
impl SameThreadContinuation {
pub(crate) fn supports_multi_turn(self) -> bool {
!matches!(self, Self::Unsupported)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ProviderKind {
Claude,
Codex,
Copilot,
}
impl ProviderKind {
pub fn parse(s: &str) -> Option<Self> {
match s.to_ascii_lowercase().as_str() {
"claude" | "claude-code" => Some(Self::Claude),
"codex" => Some(Self::Codex),
"copilot" | "gh-copilot" | "github-copilot" => Some(Self::Copilot),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
Self::Claude => "claude",
Self::Codex => "codex",
Self::Copilot => "copilot",
}
}
}
pub(crate) trait AgentProvider {
fn kind(&self) -> ProviderKind;
fn same_thread_continuation(&self) -> SameThreadContinuation {
SameThreadContinuation::Unsupported
}
fn build_command(
&self,
prompt: &str,
working_dir: &Path,
options: &ProviderOptions,
) -> tokio::process::Command;
}
pub mod claude;
pub(crate) mod claude_stream;
pub mod codex;
pub(crate) mod codex_stream;
pub mod copilot;
#[cfg(test)]
mod tests;
pub(crate) fn resolve(kind: ProviderKind) -> Box<dyn AgentProvider + Send + Sync> {
match kind {
ProviderKind::Claude => Box::new(claude::ClaudeProvider),
ProviderKind::Codex => Box::new(codex::CodexProvider),
ProviderKind::Copilot => Box::new(copilot::CopilotProvider),
}
}
pub(crate) fn capitalize_first(s: &str) -> String {
let mut chars = s.chars();
match chars.next() {
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
None => String::new(),
}
}