use anyhow::Result;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use crate::agent::context::ConversationContext;
use crate::agent::r#loop::AgentEvent;
use crate::agent::prompt;
use crate::api::provider::OpenAiCompatibleProvider;
use crate::config::Config;
pub async fn run_headless(
config: Config,
client: OpenAiCompatibleProvider,
prompt_text: String,
json_metrics: bool,
) -> Result<()> {
let working_dir = std::env::current_dir()?.to_string_lossy().to_string();
crate::project_cache::global().ensure_background_tasks();
let snap = {
let wd = working_dir.clone();
let query = prompt_text.clone();
tokio::task::spawn_blocking(move || {
crate::project_cache::global()
.get_or_build(&wd)
.snapshot_with_query(&query, 10)
})
.await
.unwrap()
};
let system_prompt =
prompt::build_default_prompt(&snap.map_string, snap.file_count, snap.symbol_count, None);
let model_context_window = crate::api::model_profile::context_window_for(&config.model);
let effective_max_tokens = config.context_max_tokens.min(model_context_window);
let context = ConversationContext::with_budget(
system_prompt,
effective_max_tokens,
config.compaction_threshold,
);
let (event_tx, mut event_rx) = mpsc::unbounded_channel::<AgentEvent>();
let cancel = CancellationToken::new();
let cancel_clone = cancel.clone();
tokio::spawn(async move {
tokio::signal::ctrl_c().await.ok();
cancel_clone.cancel();
});
let file_mentions = crate::agent::mentions::extract_mentions(&prompt_text);
let (prompt_text, _mentioned) =
crate::agent::mentions::resolve_mentions(&prompt_text, &working_dir, &file_mentions);
let client_clone = client.clone();
let config_clone = config.clone();
let working_dir_clone = working_dir.clone();
let prompt_clone = prompt_text.clone();
let lsp_manager = crate::lsp::manager::LspManager::new(working_dir.clone());
let lsp_clone = lsp_manager.clone();
eprintln!("🚀 collet headless mode (model: {})", config.model);
eprintln!("📝 Prompt: {}", truncate(&prompt_text, 100));
eprintln!("---");
tokio::spawn(async move {
crate::agent::r#loop::run_with_mode(crate::agent::r#loop::AgentParams {
client: client_clone,
config: config_clone,
context,
user_msg: prompt_clone,
working_dir: working_dir_clone,
event_tx,
cancel,
lsp_manager: lsp_clone,
trust_level: crate::trust::TrustLevel::Full, approval_gate: crate::agent::approval::ApprovalGate::yolo(),
images: Vec::new(),
})
.await;
});
let start = std::time::Instant::now();
let mut total_input_tokens: u64 = 0;
let mut total_output_tokens: u64 = 0;
let mut total_cached_tokens: u64 = 0;
let mut iterations: u32 = 0;
let mut tool_calls: u64 = 0;
let mut stop_reason = "done";
let mut exit_code = 0;
let mut streamed_any_token = false;
while let Some(event) = event_rx.recv().await {
match event {
AgentEvent::Token(token) => {
streamed_any_token = true;
print!("{token}");
}
AgentEvent::Response(text) => {
if !text.is_empty() {
if streamed_any_token {
println!();
} else {
println!("{text}");
}
}
}
AgentEvent::ToolCall { name, args, .. } => {
tool_calls += 1;
let preview = if args.len() > 200 {
format!("{}...", crate::util::truncate_bytes(&args, 200))
} else {
args
};
eprintln!("🔧 {name}: {preview}");
}
AgentEvent::ToolResult {
name,
result,
success,
..
} => {
let icon = if success { "✓" } else { "✗" };
let preview = if result.len() > 300 {
format!("{}...", crate::util::truncate_bytes(&result, 300))
} else {
result
};
eprintln!(" {icon} {name}: {preview}");
if !success {
exit_code = 1;
}
}
AgentEvent::FileModified { path } => {
eprintln!("📄 Modified: {path}");
}
AgentEvent::Error(msg) => {
eprintln!("❌ Error: {msg}");
stop_reason = "error";
exit_code = 1;
}
AgentEvent::GuardStop(msg) => {
eprintln!("{msg}");
stop_reason = "guard_stop";
}
AgentEvent::Status {
iteration,
elapsed_secs,
prompt_tokens,
completion_tokens,
cached_tokens,
..
} => {
total_input_tokens += prompt_tokens as u64;
total_output_tokens += completion_tokens as u64;
total_cached_tokens += cached_tokens as u64;
iterations = iteration;
eprintln!("⏱ Iteration {iteration} ({elapsed_secs}s)");
}
AgentEvent::StreamRetry {
attempt,
max,
message,
} => {
eprintln!("⚠ {message} ({attempt}/{max})");
}
AgentEvent::PhaseChange { label } => {
eprintln!("--- {label}");
}
AgentEvent::Done { .. } => {
break;
}
AgentEvent::PlanReady { plan, .. } => {
eprintln!("--- Architect plan ---");
eprintln!("{plan}");
break;
}
AgentEvent::SwarmAgentStarted {
agent_name,
task_preview,
..
} => {
eprintln!("[Hive] Agent '{agent_name}' started: {task_preview}");
}
AgentEvent::SwarmAgentProgress {
agent_name,
iteration,
status,
..
} => {
eprintln!("[Hive] Agent '{agent_name}' iter:{iteration} — {status}");
}
AgentEvent::SwarmAgentDone {
agent_name,
success,
tool_calls,
..
} => {
let icon = if success { "OK" } else { "FAILED" };
eprintln!("[Hive] Agent '{agent_name}' {icon} ({tool_calls} tools)");
}
AgentEvent::SwarmConflict { conflicts } => {
for (path, agents) in &conflicts {
let resolution =
crate::agent::swarm::conflict::ConflictResolutionOutcome::UserResolved {
choice: "headless-last-writer".to_string(),
};
eprintln!(
"[Hive] Conflict: {path} ({}) — headless auto-resolution: {:?}",
agents.join(", "),
resolution
);
}
}
AgentEvent::SwarmDone {
merged_response,
agent_count,
total_tool_calls,
conflicts_resolved,
..
} => {
eprintln!(
"[Hive] Done: {agent_count} agents, {total_tool_calls} tools, {conflicts_resolved} conflicts"
);
if !merged_response.is_empty() {
println!("{merged_response}");
}
break;
}
AgentEvent::SwarmModeSwitch { .. } => {} AgentEvent::SwarmResolvedToSingle { .. } => {} AgentEvent::SwarmWorkerApproaching { .. } => {} AgentEvent::SwarmWorkersDispatched => {} AgentEvent::SwarmWorkerPaused { .. } => {} AgentEvent::SwarmWorkerResumed { .. } => {} AgentEvent::SwarmAgentToolCall { .. } => {} AgentEvent::SwarmAgentToolResult { .. } => {} AgentEvent::SwarmAgentToken { .. } => {} AgentEvent::SwarmAgentResponse { .. } => {} AgentEvent::PerformanceUpdate { .. } => {} AgentEvent::LspInstalled { .. } => {} AgentEvent::McpPids { .. } => {} AgentEvent::ImageNotice { .. } => {} AgentEvent::SoulReflecting { .. } => {} AgentEvent::ApprovalRequired { .. } | AgentEvent::ApprovalDenied { .. } => {} AgentEvent::Evolution(_) => {} AgentEvent::ToolBatchProgress { .. } => {}
AgentEvent::StreamWaiting { .. } => {}
AgentEvent::CompactionStarted { .. } => {}
AgentEvent::CompactionDone { .. } => {}
AgentEvent::ToolResultTruncated { .. } => {}
AgentEvent::ShellOutput {
stdout,
stderr,
exit_code,
..
} => {
if !stdout.is_empty() {
print!("{stdout}");
}
if !stderr.is_empty() {
eprint!("{stderr}");
}
if exit_code != 0 {
eprintln!("(exit {exit_code})");
}
}
}
}
if json_metrics {
let elapsed_ms = start.elapsed().as_millis() as u64;
eprintln!(
"{{\"input_tokens\":{},\"output_tokens\":{},\"cached_tokens\":{},\"iterations\":{},\"tool_calls\":{},\"elapsed_ms\":{},\"stop_reason\":\"{}\"}}",
total_input_tokens,
total_output_tokens,
total_cached_tokens,
iterations,
tool_calls,
elapsed_ms,
stop_reason
);
}
eprintln!("---");
eprintln!("✅ Done.");
if exit_code != 0 {
std::process::exit(exit_code);
}
Ok(())
}
fn truncate(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
format!("{}...", crate::util::truncate_bytes(s, max))
}
}