swarm_engine_core/analysis/
analyzer.rs1use std::collections::{HashMap, HashSet};
17
18use crate::actions::ActionsConfig;
19use crate::orchestrator::SwarmConfig;
20use crate::state::SwarmState;
21use crate::types::{SwarmTask, WorkerId};
22
23use crate::context::{TaskContext, WorkerSummary};
24
25pub trait Analyzer: Send + Sync {
57 fn analyze(&self, state: &SwarmState) -> TaskContext;
59
60 fn name(&self) -> &str {
62 "Analyzer"
63 }
64}
65
66#[derive(Debug, Clone, Default)]
75pub struct DefaultAnalyzer {
76 name: String,
78}
79
80impl DefaultAnalyzer {
81 pub fn new() -> Self {
82 Self {
83 name: "DefaultAnalyzer".to_string(),
84 }
85 }
86
87 pub fn with_name(mut self, name: impl Into<String>) -> Self {
89 self.name = name.into();
90 self
91 }
92
93 fn calculate_success_rate(state: &SwarmState) -> f64 {
95 state.shared.stats.success_rate()
96 }
97
98 fn calculate_progress(state: &SwarmState, max_ticks: u64) -> f64 {
100 if max_ticks == 0 {
101 return Self::calculate_success_rate(state);
103 }
104
105 (state.shared.tick as f64 / max_ticks as f64).min(1.0)
106 }
107}
108
109impl Analyzer for DefaultAnalyzer {
110 fn analyze(&self, state: &SwarmState) -> TaskContext {
111 let tick = state.shared.tick;
112 let mut workers = HashMap::new();
113 let mut escalations = Vec::new();
114
115 for (idx, ctx) in state.workers.iter().enumerate() {
117 let worker_id = WorkerId(idx);
118 let has_escalation = ctx.escalation.is_some();
119
120 if let Some(esc) = &ctx.escalation {
122 escalations.push((worker_id, esc.clone()));
123 }
124
125 let last_entry = ctx.history.latest();
127 let (last_action, last_success) = last_entry
128 .map(|e| (Some(e.action_name.clone()), Some(e.success)))
129 .unwrap_or((None, None));
130
131 let summary = WorkerSummary {
133 id: worker_id,
134 consecutive_failures: ctx.consecutive_failures,
135 last_action,
136 last_success,
137 last_output: ctx.last_output.clone(),
138 history_len: ctx.history.len(),
139 has_escalation,
140 };
141
142 workers.insert(worker_id, summary);
143 }
144
145 let success_rate = Self::calculate_success_rate(state);
147 let max_ticks = state
148 .shared
149 .extensions
150 .get::<SwarmConfig>()
151 .map(|c| c.max_ticks)
152 .unwrap_or(0);
153 let progress = Self::calculate_progress(state, max_ticks);
154
155 let available_actions = state.shared.extensions.get::<ActionsConfig>().cloned();
157
158 let mut metadata = HashMap::new();
160
161 if let Some(task) = state.shared.extensions.get::<SwarmTask>() {
163 metadata.insert(
164 "task".to_string(),
165 serde_json::Value::String(task.goal.clone()),
166 );
167
168 if let Some(obj) = task.context.as_object() {
170 for (key, value) in obj {
171 metadata.insert(format!("task_{}", key), value.clone());
172 }
173 }
174 }
175
176 TaskContext {
177 tick,
178 workers,
179 success_rate,
180 progress,
181 escalations,
182 available_actions,
183 v2_guidances: None, excluded_actions: Vec::new(), previous_guidances: HashMap::new(), done_workers: HashSet::new(), metadata,
188 }
189 }
190
191 fn name(&self) -> &str {
192 &self.name
193 }
194}
195
196impl Analyzer for Box<dyn Analyzer> {
201 fn analyze(&self, state: &SwarmState) -> TaskContext {
202 (**self).analyze(state)
203 }
204
205 fn name(&self) -> &str {
206 (**self).name()
207 }
208}
209
210#[cfg(test)]
215mod tests {
216 use super::*;
217
218 #[test]
219 fn test_default_analyzer_empty_state() {
220 let analyzer = DefaultAnalyzer::new();
221 let state = SwarmState::new(3);
222
223 let ctx = analyzer.analyze(&state);
224
225 assert_eq!(ctx.tick, 0);
226 assert_eq!(ctx.workers.len(), 3);
227 assert_eq!(ctx.success_rate, 1.0);
229 assert!(!ctx.has_escalations());
230 }
231
232 #[test]
233 fn test_default_analyzer_with_name() {
234 let analyzer = DefaultAnalyzer::new().with_name("TestAnalyzer");
235 assert_eq!(analyzer.name(), "TestAnalyzer");
236 }
237}