oxicode_sdk/
agent_group.rs1use crate::error::SdkResult;
6use anyhow::Result;
7use oxicode_agent::Agent;
8use std::sync::Arc;
9
10#[derive(Debug, Clone)]
12pub enum GroupStrategy {
13 Pipeline,
15
16 Parallel {
18 max_concurrency: usize,
20 },
21}
22
23impl Default for GroupStrategy {
24 fn default() -> Self {
25 GroupStrategy::Parallel { max_concurrency: 4 }
26 }
27}
28
29#[derive(Debug, Clone)]
31pub struct AgentGroupOutput {
32 pub name: String,
34 pub content: String,
36 pub success: bool,
38 pub error: Option<String>,
40}
41
42#[derive(Debug)]
44pub struct GroupResult {
45 pub results: Vec<AgentGroupOutput>,
47 pub total_duration_ms: u64,
49}
50
51impl GroupResult {
52 pub fn all_succeeded(&self) -> bool {
54 self.results.iter().all(|r| r.success)
55 }
56
57 pub fn has_failures(&self) -> bool {
62 self.results.iter().any(|r| !r.success)
63 }
64
65 pub fn success_count(&self) -> usize {
67 self.results.iter().filter(|r| r.success).count()
68 }
69
70 pub fn combined_content(&self) -> String {
72 self.results
73 .iter()
74 .map(|r| r.content.as_str())
75 .collect::<Vec<_>>()
76 .join("\n\n")
77 }
78}
79
80pub struct AgentGroup {
82 agents: Vec<Arc<Agent>>,
83 strategy: GroupStrategy,
84}
85
86impl AgentGroup {
87 pub fn new(strategy: GroupStrategy) -> Self {
89 Self {
90 agents: Vec::new(),
91 strategy,
92 }
93 }
94
95 pub fn agent(mut self, agent: Arc<Agent>) -> Self {
97 self.agents.push(agent);
98 self
99 }
100
101 pub fn len(&self) -> usize {
103 self.agents.len()
104 }
105
106 pub fn is_empty(&self) -> bool {
108 self.agents.is_empty()
109 }
110
111 pub async fn run(&self, prompt: String) -> SdkResult<GroupResult> {
113 if self.agents.is_empty() {
114 return Ok(GroupResult {
115 results: Vec::new(),
116 total_duration_ms: 0,
117 });
118 }
119
120 let start = std::time::Instant::now();
121 let results = match &self.strategy {
122 GroupStrategy::Pipeline => self.run_pipeline(prompt).await?,
123 GroupStrategy::Parallel { max_concurrency } => {
124 self.run_parallel(prompt, *max_concurrency).await?
125 }
126 };
127
128 Ok(GroupResult {
129 total_duration_ms: start.elapsed().as_millis() as u64,
130 results,
131 })
132 }
133
134 async fn run_pipeline(&self, prompt: String) -> Result<Vec<AgentGroupOutput>> {
139 let mut results = Vec::with_capacity(self.agents.len());
140 let mut current_input = prompt;
141
142 for agent in &self.agents {
143 match agent.run(current_input.clone()).await {
144 Ok((response, _events)) => {
145 results.push(AgentGroupOutput {
146 name: agent.model_id(),
147 content: response.content.clone(),
148 success: true,
149 error: None,
150 });
151 current_input = response.content;
152 }
153 Err(e) => {
154 results.push(AgentGroupOutput {
155 name: agent.model_id(),
156 content: String::new(),
157 success: false,
158 error: Some(e.to_string()),
159 });
160 tracing::warn!(
163 agent = agent.model_id(),
164 index = results.len() - 1,
165 "Pipeline agent failed, stopping sequential execution"
166 );
167 break;
168 }
169 }
170 }
171
172 Ok(results)
173 }
174
175 async fn run_parallel(
179 &self,
180 prompt: String,
181 max_concurrency: usize,
182 ) -> Result<Vec<AgentGroupOutput>> {
183 let semaphore = Arc::new(tokio::sync::Semaphore::new(max_concurrency));
184 let mut handles = Vec::with_capacity(self.agents.len());
185
186 for agent in self.agents.iter() {
187 let agent = Arc::clone(agent);
188 let prompt = prompt.clone();
189 let sem = Arc::clone(&semaphore);
190
191 handles.push(tokio::task::spawn_blocking(move || {
192 #[allow(clippy::expect_used)]
195 let rt = tokio::runtime::Builder::new_current_thread()
196 .enable_all()
197 .build()
198 .expect("Failed to create runtime");
199 rt.block_on(async move {
200 #[allow(clippy::expect_used)]
203 let _permit = sem.acquire().await.expect("semaphore closed");
204 match agent.run(prompt).await {
205 Ok((response, _events)) => AgentGroupOutput {
206 name: agent.model_id(),
207 content: response.content,
208 success: true,
209 error: None,
210 },
211 Err(e) => AgentGroupOutput {
212 name: agent.model_id(),
213 content: String::new(),
214 success: false,
215 error: Some(e.to_string()),
216 },
217 }
218 })
219 }));
220 }
221
222 let mut results = Vec::with_capacity(handles.len());
223 for handle in handles {
224 match handle.await {
225 Ok(output) => results.push(output),
226 Err(e) => results.push(AgentGroupOutput {
227 name: String::new(),
228 content: String::new(),
229 success: false,
230 error: Some(format!("Join error: {}", e)),
231 }),
232 }
233 }
234 Ok(results)
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn test_group_strategy_default() {
244 let strategy = GroupStrategy::default();
245 matches!(strategy, GroupStrategy::Parallel { max_concurrency: 4 });
246 }
247
248 #[test]
249 fn test_empty_group() {
250 let group = AgentGroup::new(GroupStrategy::Pipeline);
251 assert!(group.is_empty());
252 assert_eq!(group.len(), 0);
253 }
254
255 #[test]
256 fn test_group_result_all_succeeded() {
257 let result = GroupResult {
258 results: vec![
259 AgentGroupOutput {
260 name: "a".into(),
261 content: "ok".into(),
262 success: true,
263 error: None,
264 },
265 AgentGroupOutput {
266 name: "b".into(),
267 content: "ok".into(),
268 success: true,
269 error: None,
270 },
271 ],
272 total_duration_ms: 100,
273 };
274 assert!(result.all_succeeded());
275 }
276
277 #[test]
278 fn test_group_result_combined_content() {
279 let result = GroupResult {
280 results: vec![
281 AgentGroupOutput {
282 name: "a".into(),
283 content: "first".into(),
284 success: true,
285 error: None,
286 },
287 AgentGroupOutput {
288 name: "b".into(),
289 content: "second".into(),
290 success: true,
291 error: None,
292 },
293 ],
294 total_duration_ms: 100,
295 };
296 assert_eq!(result.combined_content(), "first\n\nsecond");
297 }
298}