1use crate::{generate_with, AgentContext, AgentGenerator, AgentResult};
8use car_inference::{GenerateParams, GenerateRequest};
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum Pattern {
15 Solo,
17 Pipeline,
19 Swarm,
21 Supervisor,
23}
24
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
27pub struct CoordinationPlan {
28 pub pattern: Pattern,
29 pub agents: Vec<String>,
30 pub reasoning: String,
31}
32
33#[derive(Debug, Clone)]
35pub struct CoordinatorConfig {
36 pub max_tokens: usize,
37 pub temperature: f64,
38 pub model: Option<String>,
39}
40
41impl Default for CoordinatorConfig {
42 fn default() -> Self {
43 Self {
44 max_tokens: 1024,
45 temperature: 0.2,
46 model: None,
47 }
48 }
49}
50
51pub struct Coordinator {
53 ctx: AgentContext,
54 config: CoordinatorConfig,
55 generator: Option<Arc<AgentGenerator>>,
56}
57
58impl Coordinator {
59 pub fn new(ctx: AgentContext) -> Self {
60 Self {
61 ctx,
62 config: CoordinatorConfig::default(),
63 generator: None,
64 }
65 }
66
67 pub fn with_config(ctx: AgentContext, config: CoordinatorConfig) -> Self {
68 Self {
69 ctx,
70 config,
71 generator: None,
72 }
73 }
74
75 pub fn with_generator(
77 ctx: AgentContext,
78 config: CoordinatorConfig,
79 generator: Arc<AgentGenerator>,
80 ) -> Self {
81 Self {
82 ctx,
83 config,
84 generator: Some(generator),
85 }
86 }
87
88 pub async fn coordinate(&self, goal: &str) -> (CoordinationPlan, AgentResult) {
90 let prompt = format!(
91 "You are a coordination agent. Given a goal, decide which agents to use and how to coordinate them.\n\n\
92 Available agents:\n\
93 - researcher: Searches the codebase and gathers concrete information, files, and evidence.\n\
94 - 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\
95 - 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\
96 - verifier: Checks that the previous agent's output actually addresses the goal.\n\n\
97 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\
98 Available patterns:\n\
99 - solo: One agent handles it alone.\n\
100 - pipeline: Sequential chain (A → B → C). The LAST agent's output is what the user sees.\n\
101 - swarm: Multiple agents in parallel, pick best result.\n\
102 - supervisor: Agent works, supervisor reviews, iterate.\n\n\
103 Default choice for analytical goals: pipeline [researcher, summarizer, verifier].\n\
104 Default choice for change-making goals: pipeline [researcher, planner, verifier].\n\n\
105 Goal: {goal}\n\n\
106 Respond with EXACTLY this JSON format:\n\
107 {{\"pattern\": \"pipeline\", \"agents\": [\"researcher\", \"summarizer\", \"verifier\"], \"reasoning\": \"why this pattern\"}}"
108 );
109
110 let start = std::time::Instant::now();
111 let req = GenerateRequest {
112 prompt,
113 model: self.config.model.clone(),
114 params: GenerateParams {
115 temperature: self.config.temperature,
116 max_tokens: self.config.max_tokens,
117 ..Default::default()
118 },
119 context: None,
120 context_stable_prefix: None,
121 tools: None,
122 images: None,
123 messages: None,
124 cache_control: false,
125 response_format: None,
126 intent: None,
127 client_ref: None,
128 expected_row_digest: None,
129 expected_catalog_revision: None,
130 caller: None,
131 };
132
133 match generate_with(&self.ctx, self.generator.as_ref(), req).await {
134 Ok(result) => {
135 let plan = parse_plan(&result.text).unwrap_or_else(|| {
137 CoordinationPlan {
141 pattern: Pattern::Pipeline,
142 agents: vec!["researcher".into(), "summarizer".into(), "verifier".into()],
143 reasoning: "default pipeline (failed to parse model response)".into(),
144 }
145 });
146
147 let agent_result = AgentResult {
148 agent: "coordinator".into(),
149 output: result.text,
150 confidence: if plan.reasoning.contains("default") {
151 0.5
152 } else {
153 0.8
154 },
155 model_used: result.model_used,
156 latency_ms: start.elapsed().as_millis() as u64,
157 };
158
159 (plan, agent_result)
160 }
161 Err(e) => {
162 let plan = CoordinationPlan {
163 pattern: Pattern::Pipeline,
164 agents: vec!["researcher".into(), "summarizer".into()],
165 reasoning: format!("fallback (coordination failed: {})", e),
166 };
167 let agent_result = AgentResult {
168 agent: "coordinator".into(),
169 output: format!("Coordination failed: {}", e),
170 confidence: 0.3,
171 model_used: String::new(),
172 latency_ms: start.elapsed().as_millis() as u64,
173 };
174 (plan, agent_result)
175 }
176 }
177 }
178}
179
180fn parse_plan(text: &str) -> Option<CoordinationPlan> {
182 let start = text.find('{')?;
184 let end = text.rfind('}')? + 1;
185 let json_str = &text[start..end];
186 serde_json::from_str(json_str).ok()
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[test]
194 fn parse_plan_from_json() {
195 let text = r#"Here's the plan: {"pattern": "pipeline", "agents": ["researcher", "verifier"], "reasoning": "research then verify"}"#;
196 let plan = parse_plan(text).unwrap();
197 assert_eq!(plan.pattern, Pattern::Pipeline);
198 assert_eq!(plan.agents, vec!["researcher", "verifier"]);
199 }
200
201 #[test]
202 fn parse_plan_from_clean_json() {
203 let text = r#"{"pattern": "swarm", "agents": ["researcher", "researcher"], "reasoning": "parallel research"}"#;
204 let plan = parse_plan(text).unwrap();
205 assert_eq!(plan.pattern, Pattern::Swarm);
206 }
207
208 #[test]
209 fn parse_plan_fails_gracefully() {
210 assert!(parse_plan("not json").is_none());
211 }
212}