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