use crate::config::AppConfig;
use crate::permissions::CliPermissionPolicy;
use crate::prompt;
use crate::repl;
use crate::sessions;
use crate::theme::Theme;
use crate::Cli;
use cersei_agent::effort::EffortLevel;
use cersei_mcp::McpServerConfig;
use cersei_memory::manager::MemoryManager;
use cersei_tools::permissions::AllowAll;
use cersei_types::Message;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
pub async fn run(cli: Cli, mut config: AppConfig) -> anyhow::Result<()> {
let theme = Theme::from_name(&config.theme);
let session_id = if let Some(ref resume) = cli.resume {
if resume == "last" {
sessions::last_session_id(&config)
.ok_or_else(|| anyhow::anyhow!("No previous session found"))?
} else {
resume.clone()
}
} else {
uuid::Uuid::new_v4().to_string()
};
let memory_manager = build_memory_manager(&config)?;
let cancel_token = CancellationToken::new();
let running = Arc::new(AtomicBool::new(false));
crate::signals::install(cancel_token.clone(), running.clone())?;
let shared_mode = crate::permissions::new_shared_mode();
let (perm_tx, perm_rx) = crate::permissions::permission_channel();
let (agent, resolved_model) = build_agent(
&config.model,
&config,
&memory_manager,
&session_id,
cancel_token.clone(),
None,
Some(shared_mode.clone()),
Some(perm_tx),
)?;
config.model = resolved_model;
let effort = EffortLevel::from_str(&config.effort);
let json_mode = cli.json || config.output_format == "stream-json";
if !json_mode {
print_banner(&config, &session_id, &effort);
}
let prompt = cli.prompt.as_deref().filter(|p| *p != ".");
if let Some(prompt_text) = prompt {
let prompt_text = prompt_text.to_string();
repl::run_single_shot(
agent,
&prompt_text,
&theme,
&session_id,
&config,
&memory_manager,
json_mode,
running,
cancel_token,
)
.await
} else if json_mode {
repl::run_repl(
agent,
&theme,
&session_id,
&config,
&memory_manager,
json_mode,
running,
cancel_token.clone(),
)
.await
} else {
crate::tui::run(
agent,
&config,
&memory_manager,
&session_id,
cancel_token,
shared_mode,
perm_rx,
)
.await
}
}
fn detect_proxy(config: &AppConfig) -> Option<String> {
if !config.proxy.enabled {
return None;
}
if !config.proxy.force {
let has_anthropic = std::env::var("ANTHROPIC_API_KEY")
.ok()
.filter(|k| !k.is_empty())
.is_some();
let has_openai = std::env::var("OPENAI_API_KEY")
.ok()
.filter(|k| !k.is_empty())
.is_some();
if has_anthropic || has_openai {
return None; }
}
let base = config
.proxy
.url
.trim_end_matches("/v1")
.trim_end_matches('/');
let addr = base
.trim_start_matches("http://")
.trim_start_matches("https://");
if let Ok(addr) = addr.parse::<std::net::SocketAddr>() {
if std::net::TcpStream::connect_timeout(&addr, std::time::Duration::from_millis(200))
.is_ok()
{
return Some(config.proxy.url.clone());
}
} else {
use std::net::ToSocketAddrs;
if let Ok(mut addrs) = addr.to_socket_addrs() {
if let Some(sock_addr) = addrs.next() {
if std::net::TcpStream::connect_timeout(
&sock_addr,
std::time::Duration::from_millis(200),
)
.is_ok()
{
return Some(config.proxy.url.clone());
}
}
}
}
None
}
pub fn build_agent(
model_string: &str,
config: &AppConfig,
memory_manager: &MemoryManager,
session_id: &str,
cancel_token: CancellationToken,
existing_messages: Option<Vec<Message>>,
shared_mode: Option<crate::permissions::SharedPermissionMode>,
perm_tx: Option<tokio::sync::mpsc::Sender<crate::permissions::TuiPermissionRequest>>,
) -> anyhow::Result<(cersei::Agent, String)> {
let (provider, resolved_model) = if let Some(proxy_url) = detect_proxy(config) {
let model = if model_string == "auto" {
"claude-sonnet-4-6"
} else {
model_string
};
let provider = cersei_provider::OpenAi::builder()
.api_key("vibeproxy")
.base_url(&proxy_url)
.model(model)
.build()
.map_err(|e| anyhow::anyhow!("{e}"))?;
(
Box::new(provider) as Box<dyn cersei_provider::Provider>,
format!("{model} via proxy"),
)
} else {
cersei_provider::from_model_string(model_string).map_err(|e| anyhow::anyhow!("{e}"))?
};
let system_prompt = prompt::build_cli_system_prompt(config, memory_manager, &resolved_model);
let effort = EffortLevel::from_str(&config.effort);
let mcp_configs: Vec<McpServerConfig> = config
.mcp_servers
.iter()
.map(|s| {
let args_ref: Vec<&str> = s.args.iter().map(|a| a.as_str()).collect();
let mut cfg = McpServerConfig::stdio(&s.name, &s.command, &args_ref);
cfg.env = s.env.clone();
cfg
})
.collect();
let mut tools = cersei_tools::all();
tools.push(Box::new(cersei_tools::lsp_tool::LspTool::new(
&config.working_dir,
)));
if config.embedding_api {
match cersei_embeddings::auto_from_model(&resolved_model) {
Ok(provider) => {
tools.retain(|t| t.name() != "CodeSearch");
tools.push(Box::new(
cersei_tools::code_search::CodeSearchTool::with_embeddings(Arc::from(provider)),
));
}
Err(e) => tracing::warn!("Embedding provider unavailable, BM25 only: {e}"),
}
}
let compression_level = config
.compression_level
.parse::<cersei_compression::CompressionLevel>()
.unwrap_or_else(|e| {
tracing::warn!(
"invalid compression_level '{}': {e}; using off",
config.compression_level
);
cersei_compression::CompressionLevel::Off
});
let mut builder = cersei::Agent::builder()
.provider(provider)
.tools(tools)
.system_prompt(system_prompt)
.model(&resolved_model)
.max_turns(config.max_turns)
.max_tokens(config.max_tokens)
.auto_compact(config.auto_compact)
.compression_level(compression_level)
.enable_broadcast(512)
.cancel_token(cancel_token)
.session_id(session_id)
.working_dir(&config.working_dir)
.benchmark_mode(config.benchmark_mode);
if config.permissions_mode == "allow_all" {
builder = builder.permission_policy(AllowAll);
} else if let (Some(mode), Some(tx)) = (shared_mode, perm_tx) {
builder = builder.permission_policy(crate::permissions::TuiPermissionPolicy::new(mode, tx));
} else {
builder = builder.permission_policy(CliPermissionPolicy::new());
}
let budget = effort.thinking_budget_tokens();
builder = builder.thinking_budget(budget);
if let Some(temp) = effort.temperature() {
builder = builder.temperature(temp);
}
for mcp in mcp_configs {
builder = builder.mcp_server(mcp);
}
if let Some(msgs) = existing_messages {
builder = builder.with_messages(msgs);
}
let agent = builder.build()?;
Ok((agent, resolved_model))
}
fn build_memory_manager(config: &AppConfig) -> anyhow::Result<MemoryManager> {
let mut mm = MemoryManager::new(&config.working_dir);
#[cfg(feature = "graph")]
if config.graph_memory {
let graph_path = crate::config::graph_db_path();
if let Some(parent) = graph_path.parent() {
std::fs::create_dir_all(parent)?;
}
mm = mm
.with_graph(&graph_path)
.map_err(|e| anyhow::anyhow!("Failed to open graph memory: {e}"))?;
}
Ok(mm)
}
fn print_banner(config: &AppConfig, session_id: &str, effort: &EffortLevel) {
let short_id = if session_id.len() > 8 {
&session_id[..8]
} else {
session_id
};
eprintln!(
"\x1b[36;1mabstract\x1b[0m \x1b[90mv{} | {} | {:?} effort | session {}\x1b[0m",
env!("CARGO_PKG_VERSION"),
config.model,
effort,
short_id,
);
eprintln!("\x1b[90mType /help for commands, Ctrl+C to cancel, Ctrl+C×2 to exit\x1b[0m\n");
}