Skip to main content

ctc_agents/
agent.rs

1use crate::config::AgentConfig;
2use crate::error::{AgentError, AgentResult};
3use crate::probe::{CorrectionVector, ProbeFinding, ProbeKind};
4use ctc_dag::{Epoch, SpacetimeAddr};
5use ctc_ledger::{OmniversalLedger, UniverseId, UniverseStatus};
6use ctc_signal::{ExpectedFootprint, PayloadCell, SignalDaemon};
7use serde::{Deserialize, Serialize};
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct AgentId(pub u64);
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
12pub enum AgentRole {
13    /// Scans for paradox / residual divergence.
14    ParadoxWarden,
15    /// Audits convergence quality and weight mass.
16    ConvergenceAuditor,
17    /// Speculatively probes future failure modes.
18    FutureScout,
19}
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
22pub struct AgentReport {
23    pub agent: u64,
24    pub role: AgentRole,
25    pub universe: u64,
26    pub findings: Vec<ProbeFinding>,
27    pub corrections: Vec<CorrectionVector>,
28    pub injected: usize,
29}
30
31/// Autonomous chronal agent bound to a home universe, free to traverse others.
32#[derive(Clone)]
33pub struct ChronalAgent {
34    pub id: AgentId,
35    pub role: AgentRole,
36    pub home: UniverseId,
37    pub config: AgentConfig,
38    pub active: bool,
39    /// Non-deterministic seed for exploration jitter.
40    pub seed: u64,
41}
42
43impl ChronalAgent {
44    pub fn new(id: AgentId, role: AgentRole, home: UniverseId, config: AgentConfig, seed: u64) -> Self {
45        Self {
46            id,
47            role,
48            home,
49            config,
50            active: true,
51            seed,
52        }
53    }
54
55    /// Explore a universe branch and optionally inject corrections via signal.
56    pub fn explore(
57        &self,
58        ledger: &OmniversalLedger,
59        target: UniverseId,
60        signal: Option<&SignalDaemon>,
61    ) -> AgentResult<AgentReport> {
62        if !self.active {
63            return Err(AgentError::Decommissioned(self.id.0));
64        }
65        let status = ledger
66            .status(target)
67            .ok_or(AgentError::UnnavigableUniverse(target.0))?;
68        if !matches!(status, UniverseStatus::Active | UniverseStatus::Condemned) {
69            return Err(AgentError::UnnavigableUniverse(target.0));
70        }
71
72        let mut findings = Vec::new();
73        let mut corrections = Vec::new();
74
75        let residual = ledger.residual(target).unwrap_or(0.0);
76        let weight = ledger.weight(target).unwrap_or(0.0);
77        let fp = ledger.fixed_point(target).unwrap_or_default();
78
79        match self.role {
80            AgentRole::ParadoxWarden => {
81                if residual > self.config.paradox_residual {
82                    findings.push(ProbeFinding {
83                        kind: ProbeKind::ParadoxScan,
84                        universe: target.0,
85                        severity: residual,
86                        message: format!("residual {residual:.3e} exceeds paradox gate"),
87                        needs_correction: true,
88                    });
89                } else {
90                    findings.push(ProbeFinding {
91                        kind: ProbeKind::ParadoxScan,
92                        universe: target.0,
93                        severity: residual,
94                        message: "paradox scan clear".into(),
95                        needs_correction: false,
96                    });
97                }
98            }
99            AgentRole::ConvergenceAuditor => {
100                let suboptimal = weight < self.config.suboptimal_weight;
101                findings.push(ProbeFinding {
102                    kind: ProbeKind::ConvergenceAudit,
103                    universe: target.0,
104                    severity: if suboptimal { 1.0 - weight } else { 0.0 },
105                    message: format!("weight={weight:.4} residual={residual:.3e}"),
106                    needs_correction: suboptimal,
107                });
108                if suboptimal {
109                    // Nudge primary coordinate toward higher-weight basin centroid (0.5).
110                    if let Some(v0) = fp.first() {
111                        let delta = (0.5 - v0) * self.config.correction_scale;
112                        corrections.push(CorrectionVector {
113                            universe: target,
114                            target_tau: 0,
115                            address: 0,
116                            delta,
117                            reason: "suboptimal weight — micro-optimize toward basin center".into(),
118                        });
119                    }
120                }
121            }
122            AgentRole::FutureScout => {
123                // Non-deterministic future-failure probe using seed jitter.
124                let jitter = ((self.seed.wrapping_mul(target.0.wrapping_add(1))) % 1000) as f64 / 1000.0;
125                let risk = residual * 0.1 + jitter * 0.05;
126                let failure = risk > 0.04;
127                findings.push(ProbeFinding {
128                    kind: ProbeKind::FutureFailureProbe,
129                    universe: target.0,
130                    severity: risk,
131                    message: if failure {
132                        "projected future deadlock / divergence risk".into()
133                    } else {
134                        "future failure probe nominal".into()
135                    },
136                    needs_correction: failure,
137                });
138                if failure {
139                    if let Some(v0) = fp.first() {
140                        corrections.push(CorrectionVector {
141                            universe: target,
142                            target_tau: 0,
143                            address: 0,
144                            delta: -v0.signum() * self.config.correction_scale * risk,
145                            reason: "preemptive damping against projected failure".into(),
146                        });
147                    }
148                }
149            }
150        }
151
152        let mut injected = 0usize;
153        if let Some(daemon) = signal {
154            for c in &corrections {
155                if self.inject_correction(ledger, daemon, c)? {
156                    injected += 1;
157                }
158            }
159        }
160
161        Ok(AgentReport {
162            agent: self.id.0,
163            role: self.role,
164            universe: target.0,
165            findings,
166            corrections,
167            injected,
168        })
169    }
170
171    fn inject_correction(
172        &self,
173        ledger: &OmniversalLedger,
174        daemon: &SignalDaemon,
175        corr: &CorrectionVector,
176    ) -> AgentResult<bool> {
177        let binding = ledger
178            .with_universe(corr.universe, |u| daemon.bind(&u.dag))
179            .map_err(|e| AgentError::Ledger(e.to_string()))?;
180
181        let addr = SpacetimeAddr::new(corr.address, corr.target_tau);
182        let current = ledger
183            .with_universe(corr.universe, |u| {
184                u.dag
185                    .lookup(addr)
186                    .map(|n| n.state.value.first().copied().unwrap_or(0.0))
187                    .unwrap_or(0.0)
188            })
189            .map_err(|e| AgentError::Ledger(e.to_string()))?;
190
191        let new_val = current + corr.delta;
192        let cells = vec![PayloadCell {
193            addr,
194            values: vec![new_val],
195            blob: vec![],
196        }];
197        let fp = ExpectedFootprint::from_cells(Epoch(corr.target_tau), &cells, binding);
198        daemon.register_footprint(fp);
199
200        let packet = daemon
201            .package_scalars(
202                Epoch(corr.target_tau + 1),
203                Epoch(corr.target_tau),
204                binding,
205                &[(addr, new_val)],
206            )
207            .map_err(|e| AgentError::Signal(e.to_string()))?;
208
209        ledger
210            .with_universe_mut(corr.universe, |u| {
211                daemon
212                    .transmit(&mut u.dag, &packet, None)
213                    .map_err(|e| AgentError::Signal(e.to_string()))
214            })
215            .map_err(|e| AgentError::Ledger(e.to_string()))??;
216
217        // Update stored fixed point coordinate if present.
218        let _ = ledger.with_universe_mut(corr.universe, |u| {
219            if let Some(slot) = u.fixed_point.get_mut(corr.address as usize) {
220                *slot = new_val;
221            }
222        });
223
224        Ok(true)
225    }
226}