use tracing::{info, warn};
use crate::llm::LlmClient;
use super::super::command::{Command, parse_command};
use super::super::config::{DocContext, Step};
use super::super::events::EventEmitter;
use super::super::prompts::{check_sufficiency, parse_sufficiency_response};
use super::super::state::WorkerState;
use super::super::tools::worker as tools;
pub async fn execute_command(
command: &Command,
ctx: &DocContext<'_>,
state: &mut WorkerState,
query: &str,
llm: &LlmClient,
llm_calls: &mut u32,
emitter: &EventEmitter,
) -> Step {
info!(
doc = ctx.doc_name,
command = ?command,
"Executing tool"
);
match command {
Command::Ls => {
let result = tools::ls(ctx, state);
info!(doc = ctx.doc_name, feedback = %truncate_log(&result.feedback), "ls result");
state.set_feedback(result.feedback);
Step::Continue
}
Command::Cd { target } => {
let result = tools::cd(target, ctx, state);
info!(doc = ctx.doc_name, target, feedback = %truncate_log(&result.feedback), "cd result");
state.set_feedback(result.feedback);
Step::Continue
}
Command::CdUp => {
let result = tools::cd_up(ctx, state);
info!(doc = ctx.doc_name, feedback = %truncate_log(&result.feedback), "cd_up result");
state.set_feedback(result.feedback);
Step::Continue
}
Command::Cat { target } => {
let evidence_before = state.evidence.len();
let result = tools::cat(target, ctx, state);
info!(doc = ctx.doc_name, target, feedback = %truncate_log(&result.feedback), "cat result");
state.set_feedback(result.feedback);
if state.evidence.len() > evidence_before {
if let Some(ev) = state.evidence.last() {
info!(
doc = ctx.doc_name,
node = %ev.node_title,
path = %ev.source_path,
len = ev.content.len(),
total = state.evidence.len(),
"Evidence collected"
);
emitter.emit_evidence(
ctx.doc_name,
&ev.node_title,
&ev.source_path,
ev.content.len(),
state.evidence.len(),
);
}
}
Step::Continue
}
Command::Find { keyword } => {
let feedback = match ctx.find(keyword) {
Some(hit) => {
let mut entries = hit.entries.clone();
entries.sort_by(|a, b| {
b.weight
.partial_cmp(&a.weight)
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut seen_nodes = std::collections::HashSet::new();
let mut output = format!("Results for '{}':\n", keyword);
for entry in &entries {
if !seen_nodes.insert(entry.node_id) {
continue;
}
let title = ctx.node_title(entry.node_id).unwrap_or("unknown");
let summary = ctx
.nav_entry(entry.node_id)
.map(|e| e.overview.as_str())
.unwrap_or("");
output.push_str(&format!(
" - {} (depth {}, weight {:.2})",
title, entry.depth, entry.weight
));
if !summary.is_empty() {
output.push_str(&format!(" — {}", summary));
}
output.push('\n');
}
output
}
None => format!("No results for '{}'", keyword),
};
info!(doc = ctx.doc_name, keyword, feedback = %truncate_log(&feedback), "find result");
state.set_feedback(feedback);
Step::Continue
}
Command::Pwd => {
let result = tools::pwd(state);
state.set_feedback(result.feedback);
Step::Continue
}
Command::Check => {
let evidence_summary = state.evidence_summary();
let (system, user) = check_sufficiency(query, &evidence_summary);
info!(
doc = ctx.doc_name,
system = %system,
user = %user,
"Check prompt"
);
match llm.complete(&system, &user).await {
Ok(response) => {
*llm_calls += 1;
state.check_count += 1;
let sufficient = parse_sufficiency_response(&response);
info!(
doc = ctx.doc_name,
sufficient,
evidence = state.evidence.len(),
response = %response,
"Sufficiency check"
);
emitter.emit_worker_sufficiency_check(
ctx.doc_name,
sufficient,
state.evidence.len(),
None,
);
if sufficient {
state.last_feedback =
"Evidence is sufficient. Use done to finish.".to_string();
Step::Done
} else {
let reason = response
.trim()
.strip_prefix("INSUFFICIENT")
.unwrap_or(response.trim())
.trim()
.trim_start_matches(|c: char| c == '-' || c == ' ');
if !reason.is_empty() {
state.missing_info = reason.to_string();
}
state.set_feedback(format!(
"Evidence not yet sufficient: {}",
response.trim()
));
Step::Continue
}
}
Err(e) => {
warn!(error = %e, "Check LLM call failed");
state.last_feedback = "Could not evaluate sufficiency.".to_string();
Step::Continue
}
}
}
Command::Done => {
state.last_feedback = "Navigation complete.".to_string();
Step::Done
}
Command::Grep { pattern } => {
let result = tools::grep(pattern, ctx, state);
info!(doc = ctx.doc_name, pattern, feedback = %truncate_log(&result.feedback), "grep result");
state.set_feedback(result.feedback);
Step::Continue
}
Command::Head { target, lines } => {
let result = tools::head(target, *lines, ctx, state);
info!(doc = ctx.doc_name, target, lines, feedback = %truncate_log(&result.feedback), "head result");
state.set_feedback(result.feedback);
Step::Continue
}
Command::FindTree { pattern } => {
let result = tools::find_tree(pattern, ctx);
info!(doc = ctx.doc_name, pattern, feedback = %truncate_log(&result.feedback), "find_tree result");
state.set_feedback(result.feedback);
Step::Continue
}
Command::Wc { target } => {
let result = tools::wc(target, ctx, state);
info!(doc = ctx.doc_name, target, feedback = %truncate_log(&result.feedback), "wc result");
state.set_feedback(result.feedback);
Step::Continue
}
}
}
fn truncate_log(s: &str) -> std::borrow::Cow<'_, str> {
const MAX: usize = 300;
if s.len() <= MAX {
std::borrow::Cow::Borrowed(s)
} else {
std::borrow::Cow::Owned(format!(
"{}...(truncated, {} chars total)",
&s[..MAX],
s.len()
))
}
}
pub fn parse_and_detect_failure(llm_output: &str) -> (Command, bool) {
let command = parse_command(llm_output);
let trimmed = llm_output.trim();
let is_parse_failure =
matches!(command, Command::Ls) && !trimmed.starts_with("ls") && !trimmed.is_empty();
(command, is_parse_failure)
}