oxicode_sdk/coordination/
group_ext.rs1use std::sync::Arc;
4
5use crate::coordination::consensus::Consensus;
6use crate::coordination::shared_memory::{MemoryKey, SharedMemory};
7use crate::coordination::work_queue::{WorkQueue, WorkResult};
8use crate::lifecycle::AgentHandle;
9
10pub struct CoordinatedGroupBuilder {
12 handles: Vec<AgentHandle>,
13 work_queue: Option<Arc<WorkQueue>>,
14 shared_memory: Option<Arc<SharedMemory>>,
15 consensus: Option<Arc<Consensus>>,
16}
17
18impl CoordinatedGroupBuilder {
19 fn new() -> Self {
20 Self {
21 handles: Vec::new(),
22 work_queue: None,
23 shared_memory: None,
24 consensus: None,
25 }
26 }
27
28 pub fn handle(mut self, handle: AgentHandle) -> Self {
30 self.handles.push(handle);
31 self
32 }
33
34 pub fn work_queue(mut self, queue: Arc<WorkQueue>) -> Self {
36 self.work_queue = Some(queue);
37 self
38 }
39
40 pub fn shared_memory(mut self, memory: Arc<SharedMemory>) -> Self {
42 self.shared_memory = Some(memory);
43 self
44 }
45
46 pub fn consensus(mut self, consensus: Arc<Consensus>) -> Self {
48 self.consensus = Some(consensus);
49 self
50 }
51
52 pub fn build(self) -> CoordinatedGroup {
54 CoordinatedGroup {
55 handles: self.handles,
56 work_queue: self.work_queue,
57 shared_memory: self.shared_memory,
58 consensus: self.consensus,
59 }
60 }
61}
62
63pub struct CoordinatedGroup {
67 handles: Vec<AgentHandle>,
68 work_queue: Option<Arc<WorkQueue>>,
69 shared_memory: Option<Arc<SharedMemory>>,
70 consensus: Option<Arc<Consensus>>,
71}
72
73impl CoordinatedGroup {
74 pub fn builder() -> CoordinatedGroupBuilder {
76 CoordinatedGroupBuilder::new()
77 }
78
79 pub fn len(&self) -> usize {
81 self.handles.len()
82 }
83
84 pub fn is_empty(&self) -> bool {
86 self.handles.is_empty()
87 }
88
89 pub async fn fan_out(
93 &self,
94 work_type: &str,
95 payloads: Vec<serde_json::Value>,
96 ) -> Vec<WorkResult> {
97 let queue = match &self.work_queue {
98 Some(q) => q,
99 None => return Vec::new(),
100 };
101
102 for payload in &payloads {
104 queue.enqueue(work_type, payload.clone(), 0);
105 }
106
107 let mut join_handles = Vec::new();
109 for handle in &self.handles {
110 let queue = Arc::clone(queue);
111 let handle = handle.clone();
112
113 join_handles.push(tokio::spawn(async move {
114 let item = queue.claim(handle.agent_id(), None);
115 if let Some(item) = item {
116 queue.start(&item.id).ok();
117 let prompt = format!(
118 "Complete this task:\n{}",
119 serde_json::to_string_pretty(&item.payload).unwrap_or_default()
120 );
121 let start = std::time::Instant::now();
122 match handle.run(prompt).await {
123 Ok((response, _)) => {
124 let result = WorkResult {
125 success: true,
126 content: response.content,
127 error: None,
128 duration_ms: start.elapsed().as_millis() as u64,
129 tokens_used: None,
130 };
131 queue.complete(&item.id, result.clone()).ok();
132 Some(result)
133 }
134 Err(e) => {
135 let result = WorkResult {
136 success: false,
137 content: String::new(),
138 error: Some(e.to_string()),
139 duration_ms: start.elapsed().as_millis() as u64,
140 tokens_used: None,
141 };
142 queue.complete(&item.id, result.clone()).ok();
143 Some(result)
144 }
145 }
146 } else {
147 None
148 }
149 }));
150 }
151
152 let mut results = Vec::new();
154 for jh in join_handles {
155 if let Ok(Some(result)) = jh.await {
156 results.push(result);
157 }
158 }
159 results
160 }
161
162 pub async fn vote(
166 &self,
167 question: &str,
168 options: &[&str],
169 ) -> Option<crate::coordination::consensus::VoteResult> {
170 let consensus = self.consensus.as_ref()?;
171
172 let vote_id = format!("vote-{}", uuid::Uuid::new_v4());
173 let voter_ids: Vec<String> = self
174 .handles
175 .iter()
176 .map(|h| h.agent_id().to_string())
177 .collect();
178 consensus.start(&vote_id, voter_ids, 0.5);
179
180 for handle in &self.handles {
182 let options_str = options.join(", ");
183 let prompt = format!(
184 "Choose one option for this question. Reply with ONLY the option name, nothing else.\n\nQuestion: {}\nOptions: {}",
185 question, options_str
186 );
187
188 if let Ok((response, _)) = handle.run(prompt).await {
190 let choice = response.content.trim().to_string();
191 let matched = options
193 .iter()
194 .find(|o| choice.contains(**o))
195 .map(|o| o.to_string())
196 .unwrap_or(choice);
197 consensus.vote(&vote_id, handle.agent_id(), matched).ok();
198 }
199 }
200
201 consensus.status(&vote_id)
202 }
203
204 pub async fn map_reduce(
206 &self,
207 work_type: &str,
208 payloads: Vec<serde_json::Value>,
209 reduce_key: &MemoryKey,
210 ) -> anyhow::Result<Vec<WorkResult>> {
211 let memory = self.shared_memory.as_ref();
212 let results = self.fan_out(work_type, payloads).await;
213
214 if let Some(memory) = memory {
216 let results_json = serde_json::to_string(&results)?;
217 memory.write(
218 reduce_key,
219 serde_json::json!(results_json),
220 "coordinator",
221 None,
222 )?;
223 }
224
225 Ok(results)
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn builder_creates_group() {
235 let group = CoordinatedGroup::builder().build();
236 assert!(group.is_empty());
237 assert_eq!(group.len(), 0);
238 }
239}