1use crate::{AgentContext, AgentResult};
8use car_inference::{GenerateParams, GenerateRequest};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum Pattern {
14 Solo,
16 Pipeline,
18 Swarm,
20 Supervisor,
22}
23
24#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
26pub struct CoordinationPlan {
27 pub pattern: Pattern,
28 pub agents: Vec<String>,
29 pub reasoning: String,
30}
31
32#[derive(Debug, Clone)]
34pub struct CoordinatorConfig {
35 pub max_tokens: usize,
36 pub temperature: f64,
37 pub model: Option<String>,
38}
39
40impl Default for CoordinatorConfig {
41 fn default() -> Self {
42 Self {
43 max_tokens: 1024,
44 temperature: 0.2,
45 model: None,
46 }
47 }
48}
49
50pub struct Coordinator {
52 ctx: AgentContext,
53 config: CoordinatorConfig,
54}
55
56impl Coordinator {
57 pub fn new(ctx: AgentContext) -> Self {
58 Self {
59 ctx,
60 config: CoordinatorConfig::default(),
61 }
62 }
63
64 pub fn with_config(ctx: AgentContext, config: CoordinatorConfig) -> Self {
65 Self { ctx, config }
66 }
67
68 pub async fn coordinate(&self, goal: &str) -> (CoordinationPlan, AgentResult) {
70 let prompt = format!(
71 "You are a coordination agent. Given a goal, decide which agents to use and how to coordinate them.\n\n\
72 Available agents:\n\
73 - researcher: Searches the codebase and gathers concrete information, files, and evidence.\n\
74 - summarizer: Synthesizes the researcher's findings into a direct, polished ANSWER to the user's question. Use this for analytical goals (review, describe, explain, audit, identify, summarize, what/how/why questions).\n\
75 - planner: Breaks a goal into ordered action STEPS the user or another agent will execute. Only use this when the goal is to MAKE A CHANGE (implement, fix, add, refactor, build, migrate) and the user genuinely wants a step-by-step workflow back.\n\
76 - verifier: Checks that the previous agent's output actually addresses the goal.\n\n\
77 CRITICAL: If the user is asking you to describe/review/explain/identify/audit something, the pipeline must END with summarizer, NOT planner. The summarizer produces the final answer text. The planner turns answers into checklists — that is the wrong output shape for analytical goals.\n\n\
78 Available patterns:\n\
79 - solo: One agent handles it alone.\n\
80 - pipeline: Sequential chain (A → B → C). The LAST agent's output is what the user sees.\n\
81 - swarm: Multiple agents in parallel, pick best result.\n\
82 - supervisor: Agent works, supervisor reviews, iterate.\n\n\
83 Default choice for analytical goals: pipeline [researcher, summarizer, verifier].\n\
84 Default choice for change-making goals: pipeline [researcher, planner, verifier].\n\n\
85 Goal: {goal}\n\n\
86 Respond with EXACTLY this JSON format:\n\
87 {{\"pattern\": \"pipeline\", \"agents\": [\"researcher\", \"summarizer\", \"verifier\"], \"reasoning\": \"why this pattern\"}}"
88 );
89
90 let start = std::time::Instant::now();
91 let req = GenerateRequest {
92 prompt,
93 model: self.config.model.clone(),
94 params: GenerateParams {
95 temperature: self.config.temperature,
96 max_tokens: self.config.max_tokens,
97 ..Default::default()
98 },
99 context: None,
100 tools: None,
101 images: None,
102 messages: None,
103 cache_control: false,
104 response_format: None,
105 intent: None,
106 };
107
108 match self.ctx.inference.generate_tracked(req).await {
109 Ok(result) => {
110 let plan = parse_plan(&result.text).unwrap_or_else(|| {
112 CoordinationPlan {
116 pattern: Pattern::Pipeline,
117 agents: vec!["researcher".into(), "summarizer".into(), "verifier".into()],
118 reasoning: "default pipeline (failed to parse model response)".into(),
119 }
120 });
121
122 let agent_result = AgentResult {
123 agent: "coordinator".into(),
124 output: result.text,
125 confidence: if plan.reasoning.contains("default") {
126 0.5
127 } else {
128 0.8
129 },
130 model_used: result.model_used,
131 latency_ms: start.elapsed().as_millis() as u64,
132 };
133
134 (plan, agent_result)
135 }
136 Err(e) => {
137 let plan = CoordinationPlan {
138 pattern: Pattern::Pipeline,
139 agents: vec!["researcher".into(), "summarizer".into()],
140 reasoning: format!("fallback (coordination failed: {})", e),
141 };
142 let agent_result = AgentResult {
143 agent: "coordinator".into(),
144 output: format!("Coordination failed: {}", e),
145 confidence: 0.3,
146 model_used: String::new(),
147 latency_ms: start.elapsed().as_millis() as u64,
148 };
149 (plan, agent_result)
150 }
151 }
152 }
153}
154
155fn parse_plan(text: &str) -> Option<CoordinationPlan> {
157 let start = text.find('{')?;
159 let end = text.rfind('}')? + 1;
160 let json_str = &text[start..end];
161 serde_json::from_str(json_str).ok()
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn parse_plan_from_json() {
170 let text = r#"Here's the plan: {"pattern": "pipeline", "agents": ["researcher", "verifier"], "reasoning": "research then verify"}"#;
171 let plan = parse_plan(text).unwrap();
172 assert_eq!(plan.pattern, Pattern::Pipeline);
173 assert_eq!(plan.agents, vec!["researcher", "verifier"]);
174 }
175
176 #[test]
177 fn parse_plan_from_clean_json() {
178 let text = r#"{"pattern": "swarm", "agents": ["researcher", "researcher"], "reasoning": "parallel research"}"#;
179 let plan = parse_plan(text).unwrap();
180 assert_eq!(plan.pattern, Pattern::Swarm);
181 }
182
183 #[test]
184 fn parse_plan_fails_gracefully() {
185 assert!(parse_plan("not json").is_none());
186 }
187}