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 ParadoxWarden,
15 ConvergenceAuditor,
17 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#[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 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 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 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 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 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}