Skip to main content

ctc_agents/
fleet.rs

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
20/// Fleet manager for autonomous chronal agents.
21pub 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    /// Deploy the default triad (warden, auditor, scout) onto each child universe.
54    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    /// Run one exploration tick across all active agents on their home universes.
65    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}