use std::collections::HashSet;
use crate::document::NodeId;
use super::config::{Evidence, Output};
pub struct WorkerState {
pub breadcrumb: Vec<String>,
pub current_node: NodeId,
pub evidence: Vec<Evidence>,
pub visited: HashSet<NodeId>,
pub remaining: u32,
pub max_rounds: u32,
pub last_feedback: String,
pub missing_info: String,
pub history: Vec<String>,
pub plan: String,
pub check_count: u32,
pub plan_generated: bool,
}
const MAX_HISTORY_ENTRIES: usize = 6;
const MAX_FEEDBACK_CHARS: usize = 500;
impl WorkerState {
pub fn new(root: NodeId, max_rounds: u32) -> Self {
Self {
breadcrumb: vec!["root".to_string()],
current_node: root,
evidence: Vec::new(),
visited: HashSet::new(),
remaining: max_rounds,
max_rounds,
last_feedback: String::new(),
missing_info: String::new(),
history: Vec::new(),
plan: String::new(),
check_count: 0,
plan_generated: false,
}
}
pub fn dec_round(&mut self) {
if self.remaining > 0 {
self.remaining -= 1;
}
}
pub fn set_feedback(&mut self, feedback: String) {
if feedback.len() <= MAX_FEEDBACK_CHARS {
self.last_feedback = feedback;
} else {
let truncated = &feedback[..MAX_FEEDBACK_CHARS];
let end = truncated.rfind('\n').unwrap_or(MAX_FEEDBACK_CHARS);
self.last_feedback = format!(
"{}...\n(truncated, {} chars total)",
&feedback[..end.min(MAX_FEEDBACK_CHARS)],
feedback.len()
);
}
}
pub fn cd(&mut self, node: NodeId, title: &str) {
self.breadcrumb.push(title.to_string());
self.current_node = node;
}
pub fn cd_up(&mut self, parent: NodeId) -> bool {
if self.breadcrumb.len() <= 1 {
return false;
}
self.breadcrumb.pop();
self.current_node = parent;
true
}
pub fn add_evidence(&mut self, evidence: Evidence) {
self.evidence.push(evidence);
}
pub fn push_history(&mut self, entry: String) {
if self.history.len() >= MAX_HISTORY_ENTRIES {
self.history.remove(0);
}
self.history.push(entry);
}
pub fn history_text(&self) -> String {
if self.history.is_empty() {
return "(no history yet)".to_string();
}
self.history
.iter()
.enumerate()
.map(|(i, h)| format!("{}. {}", i + 1, h))
.collect::<Vec<_>>()
.join("\n")
}
pub fn path_str(&self) -> String {
self.breadcrumb.join("/")
}
pub fn evidence_summary(&self) -> String {
if self.evidence.is_empty() {
return "(none)".to_string();
}
self.evidence
.iter()
.map(|e| format!("- [{}] {} chars", e.node_title, e.content.len()))
.collect::<Vec<_>>()
.join("\n")
}
pub fn into_worker_output(
self,
llm_calls: u32,
budget_exhausted: bool,
doc_name: &str,
) -> super::config::WorkerOutput {
let evidence_chars: usize = self.evidence.iter().map(|e| e.content.len()).sum();
super::config::WorkerOutput {
evidence: self.evidence,
metrics: super::config::WorkerMetrics {
rounds_used: self.max_rounds.saturating_sub(self.remaining),
llm_calls,
nodes_visited: self.visited.len(),
budget_exhausted,
plan_generated: self.plan_generated,
check_count: self.check_count,
evidence_chars,
},
doc_name: doc_name.to_string(),
}
}
}
pub struct OrchestratorState {
pub dispatched: Vec<usize>,
pub sub_results: Vec<Output>,
pub all_evidence: Vec<Evidence>,
pub analyze_done: bool,
pub total_llm_calls: u32,
}
impl OrchestratorState {
pub fn new() -> Self {
Self {
dispatched: Vec::new(),
sub_results: Vec::new(),
all_evidence: Vec::new(),
analyze_done: false,
total_llm_calls: 0,
}
}
pub fn record_dispatch(&mut self, doc_idx: usize) {
if !self.dispatched.contains(&doc_idx) {
self.dispatched.push(doc_idx);
}
}
pub fn collect_result(&mut self, doc_idx: usize, result: super::config::WorkerOutput) {
self.total_llm_calls += result.metrics.llm_calls;
self.all_evidence.extend(result.evidence.iter().cloned());
self.sub_results.push(result.into());
self.record_dispatch(doc_idx);
}
pub fn clone_results_into_output(&self, answer: String) -> Output {
Output {
answer,
evidence: self.all_evidence.clone(),
metrics: super::config::Metrics {
llm_calls: self.total_llm_calls,
nodes_visited: self
.sub_results
.iter()
.map(|r| r.metrics.nodes_visited)
.sum(),
plan_generated: self.sub_results.iter().any(|r| r.metrics.plan_generated),
check_count: self.sub_results.iter().map(|r| r.metrics.check_count).sum(),
evidence_chars: self
.sub_results
.iter()
.map(|r| r.metrics.evidence_chars)
.sum(),
..Default::default()
},
confidence: 0.0,
}
}
pub fn into_output(self, answer: String) -> Output {
Output {
answer,
evidence: self.all_evidence,
metrics: super::config::Metrics {
llm_calls: self.total_llm_calls,
nodes_visited: self
.sub_results
.iter()
.map(|r| r.metrics.nodes_visited)
.sum(),
plan_generated: self.sub_results.iter().any(|r| r.metrics.plan_generated),
check_count: self.sub_results.iter().map(|r| r.metrics.check_count).sum(),
evidence_chars: self
.sub_results
.iter()
.map(|r| r.metrics.evidence_chars)
.sum(),
..Default::default()
},
confidence: 0.0,
}
}
}
impl Default for OrchestratorState {
fn default() -> Self {
Self::new()
}
}