1use crate::agent::{AgentId, AgentReport, AgentRole, ChronalAgent};
2use crate::config::AgentConfig;
3use crate::error::{AgentError, AgentResult};
4use ctc_ledger::{OmniversalLedger, UniverseId};
5use ctc_signal::SignalDaemon;
6use parking_lot::RwLock;
7use rustc_hash::{FxHashMap, FxHashSet};
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10
11#[derive(Clone, Debug, Default, Serialize, Deserialize)]
12pub struct FleetReport {
13 pub agents_deployed: usize,
14 pub universes_probed: usize,
15 pub findings: usize,
16 pub corrections_injected: usize,
17 pub reports: Vec<AgentReport>,
18}
19
20pub struct AgentFleet {
22 pub config: AgentConfig,
23 next_id: RwLock<u64>,
24 agents: RwLock<FxHashMap<u64, ChronalAgent>>,
25 signal: Option<Arc<SignalDaemon>>,
26}
27
28impl AgentFleet {
29 pub fn new(config: AgentConfig) -> Self {
30 Self {
31 config,
32 next_id: RwLock::new(1),
33 agents: RwLock::new(FxHashMap::default()),
34 signal: None,
35 }
36 }
37
38 pub fn with_signal(mut self, signal: Arc<SignalDaemon>) -> Self {
39 self.signal = Some(signal);
40 self
41 }
42
43 pub fn spawn(&self, role: AgentRole, home: UniverseId) -> AgentId {
44 let mut n = self.next_id.write();
45 let id = AgentId(*n);
46 let seed = (*n).wrapping_mul(2654435761);
47 *n += 1;
48 let agent = ChronalAgent::new(id, role, home, self.config.clone(), seed);
49 self.agents.write().insert(id.0, agent);
50 id
51 }
52
53 pub fn deploy_triad(&self, homes: &[UniverseId]) -> Vec<AgentId> {
55 let mut ids = Vec::new();
56 for home in homes {
57 ids.push(self.spawn(AgentRole::ParadoxWarden, *home));
58 ids.push(self.spawn(AgentRole::ConvergenceAuditor, *home));
59 ids.push(self.spawn(AgentRole::FutureScout, *home));
60 }
61 ids
62 }
63
64 pub fn explore_all(&self, ledger: &OmniversalLedger) -> AgentResult<FleetReport> {
66 let agents: Vec<ChronalAgent> = self
67 .agents
68 .read()
69 .values()
70 .filter(|a| a.active)
71 .cloned()
72 .collect();
73
74 let mut report = FleetReport {
75 agents_deployed: agents.len(),
76 ..FleetReport::default()
77 };
78 let mut probed = FxHashSet::default();
79 let signal = self.signal.as_deref();
80
81 for agent in &agents {
82 let r = agent.explore(ledger, agent.home, signal)?;
83 probed.insert(r.universe);
84 report.findings += r.findings.len();
85 report.corrections_injected += r.injected;
86 report.reports.push(r);
87 }
88 report.universes_probed = probed.len();
89 Ok(report)
90 }
91
92 pub fn decommission(&self, id: AgentId) -> AgentResult<()> {
93 let mut agents = self.agents.write();
94 let a = agents
95 .get_mut(&id.0)
96 .ok_or(AgentError::UnknownAgent(id.0))?;
97 a.active = false;
98 Ok(())
99 }
100
101 pub fn len(&self) -> usize {
102 self.agents.read().len()
103 }
104
105 pub fn is_empty(&self) -> bool {
106 self.agents.read().is_empty()
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113 use ctc_dag::WorldlineDag;
114 use ctc_kernel::{ConvergenceClass, FixedPointSolution, SolverStats};
115 use ctc_ledger::{ForkCause, OmniversalLedger};
116
117 #[test]
118 fn triad_probes_bifurcated_universes() {
119 let ledger = OmniversalLedger::default();
120 let root = ledger.bootstrap("prime", WorldlineDag::new());
121 let sol = FixedPointSolution {
122 class: ConvergenceClass::MultiWeighted,
123 states: vec![vec![0.1], vec![0.9]],
124 weights: vec![0.8, 0.2],
125 stats: SolverStats {
126 iterations: 4,
127 final_residual: 1e-12,
128 restarts_used: 2,
129 fixed_points_found: 2,
130 },
131 };
132 let event = ledger
133 .bifurcate_from_solution(root, &sol, ForkCause::MultiFixedPoint)
134 .unwrap();
135
136 let fleet = AgentFleet::new(AgentConfig::default());
137 fleet.deploy_triad(&event.children);
138 let report = fleet.explore_all(&ledger).unwrap();
139 assert_eq!(report.agents_deployed, 6);
140 assert!(report.findings >= 6);
141 }
142}