1use std::collections::VecDeque;
7use std::fs::{File, OpenOptions};
8use std::io::{BufRead, BufReader, BufWriter, Write};
9use std::path::{Path, PathBuf};
10use std::sync::mpsc::{self, Receiver, Sender};
11
12use serde::{Deserialize, Serialize};
13
14pub mod adapters;
15pub mod agents;
16pub mod collaboration;
17pub mod contract;
18pub mod details;
19pub mod history;
20pub mod launcher;
21pub mod persistence;
22pub mod policy;
23pub mod relay;
24pub mod resources;
25pub mod settings;
26pub mod trace;
27pub use adapters::*;
28pub use relay::{Relay, RelayDecision};
29
30pub type RosterSlot = usize;
32
33#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
34pub struct Mode {
35 pub id: String,
36 pub label: String,
37}
38
39#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
40pub struct AgentCapabilities {
41 pub supports_cancel: bool,
42 pub supports_modes: bool,
43 pub supports_permissions: bool,
44 pub supports_terminals: bool,
45 pub supports_session_load: bool,
46 #[serde(default)]
47 pub supports_models: bool,
48}
49
50#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51pub struct ToolUpdate {
52 pub id: String,
53 pub title: String,
54 pub status: ToolStatus,
55 pub detail: Option<String>,
56}
57
58#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
63pub struct AgentCommand {
64 pub name: String,
65}
66
67#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
69pub struct UsageUpdate {
70 pub used: u64,
71 pub size: u64,
72}
73
74#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
75pub enum ToolStatus {
76 Pending,
77 Running,
78 Completed,
79 Failed,
80}
81
82#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
83pub struct PermissionRequest {
84 pub id: String,
85 pub title: String,
86 pub options: Vec<String>,
87 #[serde(default)]
90 pub option_ids: Vec<String>,
91}
92
93#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
96pub enum PermissionAnswer {
97 Selected { option_id: String },
98 Cancelled,
99}
100
101#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
102pub enum TerminalEvent {
103 Created { id: String, command: String },
104 Output { id: String, text: String },
105 Exited { id: String, code: i32 },
106 Released { id: String },
107}
108
109#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
110pub enum RosterUpdate {
111 Added {
112 slot: RosterSlot,
113 name: String,
114 identity: String,
115 },
116 Reloaded {
117 slot: RosterSlot,
118 },
119 Dropped {
120 slot: RosterSlot,
121 },
122 Swapped {
123 first: RosterSlot,
124 second: RosterSlot,
125 },
126 Rejected {
127 action: String,
128 detail: String,
129 },
130}
131
132#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
133pub enum AgentEvent {
134 RosterUpdated {
135 update: RosterUpdate,
136 },
137 Ready {
138 slot: RosterSlot,
139 capabilities: AgentCapabilities,
140 },
141 TurnStarted {
142 slot: RosterSlot,
143 },
144 ModesReplaced {
145 slot: RosterSlot,
146 modes: Vec<Mode>,
147 current_mode: Option<String>,
148 },
149 ModeUpdated {
150 slot: RosterSlot,
151 current_mode: String,
152 },
153 ModelsReplaced {
154 slot: RosterSlot,
155 config_id: String,
156 models: Vec<Mode>,
157 current_model: Option<String>,
158 },
159 ModelUpdated {
160 slot: RosterSlot,
161 current_model: String,
162 },
163 UserText {
164 slot: RosterSlot,
165 text: String,
166 },
167 CommandsReplaced {
168 slot: RosterSlot,
169 commands: Vec<AgentCommand>,
170 },
171 UsageUpdated {
172 slot: RosterSlot,
173 usage: UsageUpdate,
174 },
175 Text {
176 slot: RosterSlot,
177 text: String,
178 },
179 Thought {
180 slot: RosterSlot,
181 text: String,
182 },
183 Tool {
184 slot: RosterSlot,
185 update: ToolUpdate,
186 },
187 Permission {
188 slot: RosterSlot,
189 request: PermissionRequest,
190 },
191 Terminal {
192 slot: RosterSlot,
193 event: TerminalEvent,
194 },
195 TurnComplete {
196 slot: RosterSlot,
197 },
198 Failed {
199 slot: RosterSlot,
200 started: bool,
201 detail: String,
202 },
203}
204
205#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
206pub enum Effect {
207 Render,
208 DispatchPrompt { slot: RosterSlot, prompt: String },
209 OfferReload { slot: RosterSlot, crashed: bool },
210}
211
212#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
213pub struct AgentSlot {
214 pub active: bool,
215 pub capabilities: AgentCapabilities,
216 pub modes: Vec<Mode>,
217 pub current_mode: Option<String>,
218 #[serde(default)]
219 pub models: Vec<Mode>,
220 #[serde(default)]
221 pub current_model: Option<String>,
222 #[serde(default)]
223 pub commands: Vec<AgentCommand>,
224 #[serde(default)]
225 pub usage: Option<UsageUpdate>,
226}
227
228impl Default for AgentSlot {
229 fn default() -> Self {
230 Self {
231 active: true,
232 capabilities: AgentCapabilities::default(),
233 modes: Vec::new(),
234 current_mode: None,
235 models: Vec::new(),
236 current_model: None,
237 commands: Vec::new(),
238 usage: None,
239 }
240 }
241}
242
243#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
244pub struct SessionState {
245 pub slots: Vec<AgentSlot>,
246 pub active_slot: Option<RosterSlot>,
247 pub queued_prompts: VecDeque<(RosterSlot, String)>,
248 pub public_text: Vec<(RosterSlot, String)>,
249}
250
251impl SessionState {
252 pub fn new(roster_size: usize) -> Self {
253 Self {
254 slots: (0..roster_size).map(|_| AgentSlot::default()).collect(),
255 active_slot: None,
256 queued_prompts: VecDeque::new(),
257 public_text: Vec::new(),
258 }
259 }
260}
261
262pub fn reduce(state: &mut SessionState, event: AgentEvent) -> Vec<Effect> {
265 match event {
266 AgentEvent::RosterUpdated { .. } => vec![Effect::Render],
267 AgentEvent::Ready { slot, capabilities } => {
268 if let Some(agent) = state.slots.get_mut(slot) {
269 agent.capabilities = capabilities;
270 }
271 vec![Effect::Render]
272 }
273 AgentEvent::TurnStarted { slot } => {
274 state.active_slot = Some(slot);
275 vec![Effect::Render]
276 }
277 AgentEvent::ModesReplaced {
278 slot,
279 modes,
280 current_mode,
281 } => {
282 if let Some(agent) = state.slots.get_mut(slot) {
283 agent.modes = modes;
284 agent.current_mode =
285 current_mode.filter(|id| agent.modes.iter().any(|mode| mode.id == *id));
286 }
287 vec![Effect::Render]
288 }
289 AgentEvent::CommandsReplaced { slot, commands } => {
290 if let Some(agent) = state.slots.get_mut(slot) {
291 agent.commands = commands;
292 }
293 vec![Effect::Render]
294 }
295 AgentEvent::ModeUpdated { slot, current_mode } => {
296 if let Some(agent) = state.slots.get_mut(slot) {
297 agent.current_mode = Some(current_mode);
298 }
299 vec![Effect::Render]
300 }
301 AgentEvent::ModelsReplaced {
302 slot,
303 models,
304 current_model,
305 ..
306 } => {
307 if let Some(agent) = state.slots.get_mut(slot) {
308 agent.models = models;
309 agent.current_model =
310 current_model.filter(|id| agent.models.iter().any(|model| model.id == *id));
311 }
312 vec![Effect::Render]
313 }
314 AgentEvent::ModelUpdated {
315 slot,
316 current_model,
317 } => {
318 if let Some(agent) = state.slots.get_mut(slot) {
319 agent.current_model = Some(current_model);
320 }
321 vec![Effect::Render]
322 }
323 AgentEvent::UsageUpdated { slot, usage } => {
324 if let Some(agent) = state.slots.get_mut(slot) {
325 agent.usage = Some(usage);
326 }
327 vec![Effect::Render]
328 }
329 AgentEvent::Text { slot, text } => {
330 state.active_slot = Some(slot);
331 state.public_text.push((slot, text));
332 vec![Effect::Render]
333 }
334 AgentEvent::UserText { slot, .. } => {
335 state.active_slot = Some(slot);
336 vec![Effect::Render]
337 }
338 AgentEvent::Thought { slot, .. }
339 | AgentEvent::Tool { slot, .. }
340 | AgentEvent::Permission { slot, .. }
341 | AgentEvent::Terminal { slot, .. } => {
342 state.active_slot = Some(slot);
343 vec![Effect::Render]
344 }
345 AgentEvent::TurnComplete { .. } => {
346 state.active_slot = None;
347 let next =
348 state
349 .queued_prompts
350 .pop_front()
351 .map(|(target, prompt)| Effect::DispatchPrompt {
352 slot: target,
353 prompt,
354 });
355 let mut effects = vec![Effect::Render];
356 if let Some(effect) = next {
357 effects.push(effect);
358 }
359 effects
360 }
361 AgentEvent::Failed {
362 slot,
363 started,
364 detail: _,
365 } => {
366 if let Some(agent) = state.slots.get_mut(slot) {
367 agent.active = false;
368 }
369 if state.active_slot == Some(slot) {
370 state.active_slot = None;
371 }
372 vec![
373 Effect::Render,
374 Effect::OfferReload {
375 slot,
376 crashed: started,
377 },
378 ]
379 }
380 }
381}
382
383#[derive(Clone, Debug)]
388pub struct EventLog {
389 path: PathBuf,
390}
391
392impl EventLog {
393 pub fn open(path: impl Into<PathBuf>) -> Self {
394 Self { path: path.into() }
395 }
396
397 pub fn path(&self) -> &Path {
398 &self.path
399 }
400
401 pub fn append(&self, event: &AgentEvent) -> std::io::Result<()> {
402 let encoded = serde_json::to_string(event)
403 .map_err(|error| std::io::Error::other(error.to_string()))?;
404 let mut file = OpenOptions::new()
405 .create(true)
406 .append(true)
407 .open(&self.path)?;
408 file.write_all(encoded.as_bytes())?;
409 file.write_all(b"\n")
410 }
411
412 pub fn append_durable(&self, event: &AgentEvent) -> std::io::Result<()> {
418 let encoded = serde_json::to_string(event)
419 .map_err(|error| std::io::Error::other(error.to_string()))?;
420 let mut file = OpenOptions::new()
421 .create(true)
422 .append(true)
423 .open(&self.path)?;
424 file.write_all(encoded.as_bytes())?;
425 file.write_all(b"\n")?;
426 file.sync_data()
427 }
428
429 pub fn sync(&self) -> std::io::Result<()> {
433 let file = OpenOptions::new().read(true).open(&self.path)?;
434 file.sync_data()
435 }
436
437 pub fn buffered(&self) -> std::io::Result<BufferedEventLog> {
442 BufferedEventLog::open(self.path.clone())
443 }
444
445 pub fn read(&self) -> std::io::Result<Vec<AgentEvent>> {
446 let file = match File::open(&self.path) {
447 Ok(file) => file,
448 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
449 Err(error) => return Err(error),
450 };
451 BufReader::new(file)
452 .lines()
453 .enumerate()
454 .filter_map(|(line_number, result)| match result {
455 Ok(line) if line.trim().is_empty() => None,
456 Ok(line) => Some(serde_json::from_str(&line).map_err(|error| {
457 std::io::Error::new(
458 std::io::ErrorKind::InvalidData,
459 format!("event log line {}: {error}", line_number + 1),
460 )
461 })),
462 Err(error) => Some(Err(error)),
463 })
464 .collect()
465 }
466
467 pub fn replay(&self, roster_size: usize) -> std::io::Result<SessionState> {
468 let mut state = SessionState::new(roster_size);
469 for event in self.read()? {
470 reduce(&mut state, event);
471 }
472 Ok(state)
473 }
474}
475
476enum BufferedLogCommand {
477 Append(String),
478 Flush(Sender<std::io::Result<()>>),
479 Shutdown(Sender<std::io::Result<()>>),
480}
481
482pub struct BufferedEventLog {
488 sender: Sender<BufferedLogCommand>,
489 worker: Option<std::thread::JoinHandle<std::io::Result<()>>>,
490}
491
492impl std::fmt::Debug for BufferedEventLog {
493 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
494 formatter
495 .debug_struct("BufferedEventLog")
496 .field("worker_running", &self.worker.is_some())
497 .finish_non_exhaustive()
498 }
499}
500
501impl BufferedEventLog {
502 fn open(path: PathBuf) -> std::io::Result<Self> {
503 let (sender, receiver) = mpsc::channel();
504 let worker = std::thread::Builder::new()
505 .name("codeswarm-event-log".into())
506 .spawn(move || buffered_log_worker(path, receiver))?;
507 Ok(Self {
508 sender,
509 worker: Some(worker),
510 })
511 }
512
513 pub fn append(&self, event: &AgentEvent) -> std::io::Result<()> {
516 let encoded = serde_json::to_string(event)
517 .map_err(|error| std::io::Error::other(error.to_string()))?;
518 self.sender
519 .send(BufferedLogCommand::Append(format!("{encoded}\n")))
520 .map_err(|_| {
521 std::io::Error::new(
522 std::io::ErrorKind::BrokenPipe,
523 "event log background writer stopped",
524 )
525 })
526 }
527
528 pub fn flush(&self) -> std::io::Result<()> {
530 let (reply, result) = mpsc::channel();
531 self.sender
532 .send(BufferedLogCommand::Flush(reply))
533 .map_err(|_| {
534 std::io::Error::new(
535 std::io::ErrorKind::BrokenPipe,
536 "event log background writer stopped",
537 )
538 })?;
539 result.recv().map_err(|_| {
540 std::io::Error::new(
541 std::io::ErrorKind::BrokenPipe,
542 "event log background writer stopped",
543 )
544 })?
545 }
546}
547
548impl Drop for BufferedEventLog {
549 fn drop(&mut self) {
550 let (reply, result) = mpsc::channel();
551 if self
552 .sender
553 .send(BufferedLogCommand::Shutdown(reply))
554 .is_ok()
555 {
556 let _ = result.recv();
557 }
558 if let Some(worker) = self.worker.take() {
559 let _ = worker.join();
560 }
561 }
562}
563
564fn buffered_log_worker(
565 path: PathBuf,
566 receiver: Receiver<BufferedLogCommand>,
567) -> std::io::Result<()> {
568 let file = OpenOptions::new().create(true).append(true).open(path)?;
569 let mut writer = BufWriter::new(file);
570 while let Ok(command) = receiver.recv() {
571 match command {
572 BufferedLogCommand::Append(line) => writer.write_all(line.as_bytes())?,
573 BufferedLogCommand::Flush(reply) => {
574 let result = writer.flush().and_then(|()| writer.get_ref().sync_data());
575 let _ = reply.send(result);
576 }
577 BufferedLogCommand::Shutdown(reply) => {
578 let result = writer.flush().and_then(|()| writer.get_ref().sync_data());
579 let worker_result = match result {
580 Ok(()) => {
581 let _ = reply.send(Ok(()));
582 Ok(())
583 }
584 Err(error) => {
585 let kind = error.kind();
586 let detail = error.to_string();
587 let _ = reply.send(Err(std::io::Error::new(kind, detail.clone())));
588 Err(std::io::Error::new(kind, detail))
589 }
590 };
591 return worker_result;
592 }
593 }
594 }
595 writer.flush()
596}
597
598#[cfg(test)]
599mod tests {
600 use std::time::{SystemTime, UNIX_EPOCH};
601
602 use super::{AgentCapabilities, AgentEvent, Effect, Mode, SessionState, reduce};
603
604 #[test]
605 fn replacement_catalog_invalidates_stale_mode() {
606 let mut state = SessionState::new(1);
607 reduce(
608 &mut state,
609 AgentEvent::ModesReplaced {
610 slot: 0,
611 modes: vec![Mode {
612 id: "read".into(),
613 label: "Read only".into(),
614 }],
615 current_mode: Some("write".into()),
616 },
617 );
618 assert_eq!(state.slots[0].current_mode, None);
619 }
620
621 #[test]
622 fn crash_tombstones_slot_and_uses_crash_copy() {
623 let mut state = SessionState::new(2);
624 reduce(
625 &mut state,
626 AgentEvent::Ready {
627 slot: 1,
628 capabilities: AgentCapabilities::default(),
629 },
630 );
631 let effects = reduce(
632 &mut state,
633 AgentEvent::Failed {
634 slot: 1,
635 started: true,
636 detail: "process exited".into(),
637 },
638 );
639 assert!(!state.slots[1].active);
640 assert!(effects.contains(&Effect::OfferReload {
641 slot: 1,
642 crashed: true,
643 }));
644 }
645
646 #[test]
647 fn event_log_replays_into_the_same_state() {
648 let unique = SystemTime::now()
649 .duration_since(UNIX_EPOCH)
650 .expect("clock")
651 .as_nanos();
652 let path = std::env::temp_dir().join(format!("codeswarm-core-{unique}.jsonl"));
653 let log = super::EventLog::open(&path);
654 let events = [
655 AgentEvent::Text {
656 slot: 0,
657 text: "first".into(),
658 },
659 AgentEvent::Failed {
660 slot: 1,
661 started: true,
662 detail: "crashed".into(),
663 },
664 ];
665 for event in &events {
666 log.append(event).expect("append");
667 }
668 let replayed = log.replay(2).expect("replay");
669 let mut expected = SessionState::new(2);
670 for event in events {
671 reduce(&mut expected, event);
672 }
673 assert_eq!(replayed, expected);
674 std::fs::remove_file(path).expect("cleanup");
675 }
676
677 #[test]
678 fn event_log_can_checkpoint_batched_appends() {
679 let unique = SystemTime::now()
680 .duration_since(UNIX_EPOCH)
681 .expect("clock")
682 .as_nanos();
683 let path = std::env::temp_dir().join(format!("codeswarm-core-checkpoint-{unique}.jsonl"));
684 let log = super::EventLog::open(&path);
685 log.append(&AgentEvent::Text {
686 slot: 0,
687 text: "batched".into(),
688 })
689 .expect("append");
690 log.sync().expect("checkpoint");
693 assert_eq!(log.read().expect("read").len(), 1);
694 std::fs::remove_file(path).expect("cleanup");
695 }
696
697 #[test]
698 fn event_log_durable_append_is_replayable() {
699 let unique = SystemTime::now()
700 .duration_since(UNIX_EPOCH)
701 .expect("clock")
702 .as_nanos();
703 let path = std::env::temp_dir().join(format!("codeswarm-core-durable-{unique}.jsonl"));
704 let log = super::EventLog::open(&path);
705 log.append_durable(&AgentEvent::Text {
706 slot: 1,
707 text: "durable".into(),
708 })
709 .expect("durable append");
710 assert_eq!(
711 log.read().expect("read")[0].clone(),
712 AgentEvent::Text {
713 slot: 1,
714 text: "durable".into(),
715 }
716 );
717 std::fs::remove_file(path).expect("cleanup");
718 }
719
720 #[test]
721 fn buffered_event_log_drains_and_checkpoints_in_order() {
722 let unique = SystemTime::now()
723 .duration_since(UNIX_EPOCH)
724 .expect("clock")
725 .as_nanos();
726 let path = std::env::temp_dir().join(format!("codeswarm-core-buffered-{unique}.jsonl"));
727 let log = super::EventLog::open(&path);
728 let buffered = log.buffered().expect("background writer");
729 for text in ["one", "two", "three"] {
730 buffered
731 .append(&AgentEvent::Text {
732 slot: 0,
733 text: text.into(),
734 })
735 .expect("queue event");
736 }
737 buffered.flush().expect("checkpoint");
738 assert_eq!(
739 log.read().expect("read").into_iter().collect::<Vec<_>>(),
740 [
741 AgentEvent::Text {
742 slot: 0,
743 text: "one".into()
744 },
745 AgentEvent::Text {
746 slot: 0,
747 text: "two".into()
748 },
749 AgentEvent::Text {
750 slot: 0,
751 text: "three".into()
752 }
753 ]
754 );
755 drop(buffered);
756 std::fs::remove_file(path).expect("cleanup");
757 }
758
759 #[test]
760 fn activity_marks_turn_active_until_completion() {
761 let mut state = SessionState::new(1);
762 reduce(
763 &mut state,
764 AgentEvent::Text {
765 slot: 0,
766 text: "stream".into(),
767 },
768 );
769 assert_eq!(state.active_slot, Some(0));
770 reduce(&mut state, AgentEvent::TurnComplete { slot: 0 });
771 assert_eq!(state.active_slot, None);
772 }
773}