codeswarm_adapters/
collaboration.rs1#[derive(Clone, Debug, Eq, PartialEq)]
4pub struct PublicEvent {
5 pub speaker: String,
6 pub text: String,
7}
8
9#[derive(Clone, Debug, Eq, PartialEq)]
10pub struct CollaborationContext {
11 shared_task: Option<String>,
12 events: Vec<PublicEvent>,
13 seen: Vec<usize>,
14 truncated: Vec<bool>,
15}
16
17impl CollaborationContext {
18 pub fn new(agent_count: usize) -> Self {
19 Self {
20 shared_task: None,
21 events: Vec::new(),
22 seen: vec![0; agent_count],
23 truncated: vec![false; agent_count],
24 }
25 }
26
27 pub fn set_shared_task(&mut self, task: impl Into<String>) {
28 self.shared_task = Some(task.into());
29 }
30
31 pub fn shared_task(&self) -> Option<&str> {
32 self.shared_task.as_deref()
33 }
34
35 pub fn add_agent(&mut self) {
36 self.seen.push(0);
37 self.truncated.push(false);
38 }
39
40 pub fn record(&mut self, speaker: impl Into<String>, text: impl Into<String>, active: &[bool]) {
41 self.prune(active);
42 self.events.push(PublicEvent {
43 speaker: speaker.into(),
44 text: compact(text.into()),
45 });
46 }
47
48 pub fn mark_seen(&mut self, slot: usize) {
49 if let Some(seen) = self.seen.get_mut(slot) {
50 *seen = self.events.len();
51 }
52 }
53
54 pub fn rewind(&mut self, slot: usize) {
57 if let Some(seen) = self.seen.get_mut(slot) {
58 *seen = 0;
59 }
60 }
61
62 pub fn swap_agents(&mut self, first: usize, second: usize) {
66 if first < self.seen.len() && second < self.seen.len() {
67 self.seen.swap(first, second);
68 self.truncated.swap(first, second);
69 }
70 }
71
72 pub fn unseen(&mut self, slot: usize) -> String {
73 let start = self.seen.get(slot).copied().unwrap_or(self.events.len());
74 let mut updates = self.events[start..]
75 .iter()
76 .filter(|event| !event.text.is_empty())
77 .map(|event| format!("{}:\n{}", event.speaker, event.text))
78 .collect::<Vec<_>>();
79 if self.truncated.get(slot).copied().unwrap_or(false) {
80 updates.insert(
81 0,
82 "[CodeSwarm omitted older unseen updates to protect context.]".into(),
83 );
84 self.truncated[slot] = false;
85 }
86 limit(updates, 24_000)
87 }
88
89 fn prune(&mut self, active: &[bool]) {
90 let consumed = active
91 .iter()
92 .enumerate()
93 .filter_map(|(slot, enabled)| {
94 enabled.then_some(self.seen.get(slot).copied().unwrap_or(0))
95 })
96 .min()
97 .unwrap_or(0);
98 if consumed > 0 {
99 self.events.drain(..consumed);
100 for seen in &mut self.seen {
101 *seen = seen.saturating_sub(consumed);
102 }
103 }
104 while self.events.len() >= 200
105 || self
106 .events
107 .iter()
108 .map(|event| event.text.len())
109 .sum::<usize>()
110 >= 48_000
111 {
112 self.events.remove(0);
113 for (slot, seen) in self.seen.iter_mut().enumerate() {
114 if *seen > 0 {
115 *seen -= 1;
116 } else if active.get(slot).copied().unwrap_or(false) {
117 self.truncated[slot] = true;
118 }
119 }
120 }
121 }
122}
123
124fn compact(text: String) -> String {
125 const LIMIT: usize = 12_000;
126 if text.len() <= LIMIT {
127 return text;
128 }
129 let head = floor_char_boundary(&text, LIMIT / 2);
134 let tail_budget = LIMIT - head;
135 let tail_start = ceil_char_boundary(&text, text.len().saturating_sub(tail_budget));
136 format!(
137 "{}\n\n[CodeSwarm omitted the middle of this response to protect context.]\n\n{}",
138 &text[..head],
139 &text[tail_start..],
140 )
141}
142
143fn floor_char_boundary(text: &str, index: usize) -> usize {
144 let mut index = index.min(text.len());
145 while index > 0 && !text.is_char_boundary(index) {
146 index -= 1;
147 }
148 index
149}
150
151fn ceil_char_boundary(text: &str, index: usize) -> usize {
152 let mut index = index.min(text.len());
153 while index < text.len() && !text.is_char_boundary(index) {
154 index += 1;
155 }
156 index
157}
158
159fn limit(mut updates: Vec<String>, limit: usize) -> String {
160 let rendered = updates.join("\n\n");
161 if rendered.len() <= limit {
162 return rendered;
163 }
164 let marker = "[CodeSwarm omitted older unseen updates to protect context.]";
165 let mut selected = Vec::new();
166 let mut used = 0;
167 while let Some(update) = updates.pop() {
168 let added = update.len() + 2;
169 if used + added > limit - marker.len() - 2 {
170 break;
171 }
172 used += added;
173 selected.push(update);
174 }
175 selected.reverse();
176 format!("{marker}\n\n{}", selected.join("\n\n"))
177}
178
179#[cfg(test)]
180mod tests {
181 use super::CollaborationContext;
182
183 #[test]
184 fn only_unseen_public_text_is_sent_to_each_agent() {
185 let mut context = CollaborationContext::new(2);
186 context.record("Human", "task", &[true, true]);
187 context.mark_seen(0);
188 assert_eq!(context.unseen(0), "");
189 assert_eq!(context.unseen(1), "Human:\ntask");
190 }
191
192 #[test]
193 fn rewind_replays_retained_context_to_a_reloaded_agent() {
194 let mut context = CollaborationContext::new(1);
195 context.record("Agent", "answer", &[true]);
196 context.mark_seen(0);
197 assert_eq!(context.unseen(0), "");
198 context.rewind(0);
199 assert_eq!(context.unseen(0), "Agent:\nanswer");
200 }
201
202 #[test]
203 fn long_history_is_bounded_without_losing_recent_updates() {
204 let mut context = CollaborationContext::new(1);
205 for index in 0..250 {
206 context.record("Agent", format!("reply {index}"), &[true]);
207 }
208 let unseen = context.unseen(0);
209 assert!(unseen.contains("reply 249"));
210 assert!(unseen.len() <= 24_000);
211 }
212
213 #[test]
214 fn long_unicode_response_is_compacted_without_slicing_panic() {
215 let mut context = CollaborationContext::new(1);
216 context.record("Agent", "🚀漢å—".repeat(5_000), &[true]);
217 let unseen = context.unseen(0);
218 assert!(unseen.contains("omitted the middle"));
219 assert!(unseen.contains("🚀"));
220 assert!(unseen.is_char_boundary(0));
221 }
222}