Skip to main content

keel_ttl/
lib.rs

1//! # Keel Core
2//!
3//! First-person self-termination types for agent fleets.
4//!
5//! Every entity carries its own death from its own frame.
6//! Death is default. Survival must be actively earned.
7//! No central scheduler. No garbage collector. No heartbeat.
8//!
9//! ## Architecture
10//!
11//! Five types, one pattern: `{ keel_date, ttl, ... }` + a status method.
12//!
13//! The unified equation: `lifespan(E) = f(use(E), load(E), time(E))`
14//! Termination when: `lifespan(E) < time(E)`
15//!
16//! ## The Mandelbrot Constraint
17//!
18//! Same types, same methods, at every scale.
19//! This library compiles on Arduino targets and A100 datacenters.
20//! Only the anchor density changes with scale.
21
22use chrono::{DateTime, Duration, Utc};
23
24// ─── Risk Enum (shared by BearingTtl) ──────────────────────────────────────────────
25
26/// The risk level of a bearing-based collision assessment.
27///
28/// Models the navigator's rule: "If the bearing isn't changing and the scope overlaps,
29/// you're on a collision course. No message needed. The field communicates."
30#[derive(Debug, Clone, PartialEq)]
31pub enum Risk {
32    /// Course is clear. Bearing is changing or scopes do not overlap.
33    Stable,
34    /// Potential collision detected. Requires attention.
35    Warning,
36    /// Certain collision course or bearing expired (position unknown).
37    /// "Stale bearings ARE collision warnings."
38    Critical,
39}
40
41// ─── TileTtl: Self-Expiring Memory ─────────────────────────────────────────────────
42
43/// Memory that knows its own death.
44///
45/// A tile is born with a timestamp and a lifespan. Readers filter at read time.
46/// Dead tiles don't need removal — they're invisible. Compaction is optional.
47///
48/// ```
49/// use keel_core::*;
50/// use chrono::{Utc, Duration};
51///
52/// let tile = TileTtl::new("hello", Duration::hours(1));
53/// assert!(tile.is_alive());
54/// ```
55#[derive(Debug, Clone)]
56pub struct TileTtl {
57    keel_date: DateTime<Utc>,
58    ttl: Duration,
59    data: String,
60}
61
62impl TileTtl {
63    /// Create a new tile. The keel_date is set to now.
64    /// The tile's death is encoded at birth — no external scheduler needed.
65    pub fn new(data: impl Into<String>, ttl: Duration) -> Self {
66        Self { keel_date: Utc::now(), ttl, data: data.into() }
67    }
68
69    /// Is this tile still alive from its own frame?
70    /// No garbage collector asked. The tile knows.
71    pub fn is_alive(&self) -> bool {
72        Utc::now() < self.keel_date + self.ttl
73    }
74
75    /// How much of the tile's lifespan remains, as a fraction 0.0–1.0.
76    pub fn freshness(&self) -> f64 {
77        let elapsed = Utc::now() - self.keel_date;
78        if elapsed >= self.ttl { return 0.0 }
79        let remaining = self.ttl - elapsed;
80        remaining.num_milliseconds() as f64 / self.ttl.num_milliseconds() as f64
81    }
82
83    /// Reference the data. Returns None if the tile is dead.
84    pub fn data(&self) -> Option<&str> {
85        if self.is_alive() { Some(&self.data) } else { None }
86    }
87
88    /// Filter a slice to only alive tiles. Read-time filtering.
89    /// No sweep pass required. Death is invisible.
90    pub fn filter_active(tiles: &[Self]) -> Vec<&Self> {
91        tiles.iter().filter(|t| t.is_alive()).collect()
92    }
93
94    /// Partition into (alive, dead) — for when compaction is wanted.
95    pub fn partition(tiles: Vec<Self>) -> (Vec<Self>, Vec<Self>) {
96        tiles.into_iter().partition(|t| t.is_alive())
97    }
98}
99
100// ─── TaskTtl: Self-Expiring Work ──────────────────────────────────────────────────
101
102/// Work that knows when to stop.
103///
104/// A task carries its own expiry from birth. Workers check staleness mid-loop.
105/// If stale, the task silently drops — no cancellation protocol, no re-enqueue.
106#[derive(Debug, Clone)]
107pub struct TaskTtl {
108    created: DateTime<Utc>,
109    ttl: Duration,
110    steps: Vec<String>,
111    completed: usize,
112}
113
114impl TaskTtl {
115    pub fn new(steps: Vec<String>, ttl: Duration) -> Self {
116        Self { created: Utc::now(), ttl, steps, completed: 0 }
117    }
118
119    /// Has this task's time expired?
120    pub fn is_stale(&self) -> bool {
121        Utc::now() >= self.created + self.ttl
122    }
123
124    /// Execute steps until stale. Returns the number of steps completed.
125    /// No one cancels this task. It cancels itself.
126    pub fn execute_until_stale(&mut self) -> usize {
127        while self.completed < self.steps.len() {
128            if self.is_stale() {
129                break; // Self-termination. No kill signal needed.
130            }
131            // In a real system, execute the step here
132            self.completed += 1;
133        }
134        self.completed
135    }
136
137    /// Fraction of steps completed.
138    pub fn progress(&self) -> f64 {
139        if self.steps.is_empty() { return 1.0 }
140        self.completed as f64 / self.steps.len() as f64
141    }
142
143    /// Filter tasks that are not stale (still actionable).
144    pub fn filter_fresh(tasks: &[Self]) -> Vec<&Self> {
145        tasks.iter().filter(|t| !t.is_stale()).collect()
146    }
147}
148
149// ─── AgentTtl: Self-Expiring Presence ─────────────────────────────────────────────
150
151/// Presence that knows when to fade.
152///
153/// An agent declares a lifespan at birth. Output IS the heartbeat.
154/// No health-check endpoint. No keepalive packet.
155/// An agent that stops producing stops existing.
156#[derive(Debug, Clone)]
157pub struct AgentTtl {
158    keel_date: DateTime<Utc>,
159    ttl: Duration,
160    last_output: DateTime<Utc>,
161    heading: String,
162}
163
164impl AgentTtl {
165    pub fn new(heading: impl Into<String>, ttl: Duration) -> Self {
166        Self {
167            keel_date: Utc::now(),
168            ttl,
169            last_output: Utc::now(),
170            heading: heading.into(),
171        }
172    }
173
174    /// Is the agent present? Must be within lifespan AND have produced output recently.
175    /// "Output IS the heartbeat. Silence IS death."
176    pub fn is_present(&self) -> bool {
177        let now = Utc::now();
178        now < self.keel_date + self.ttl
179            && now - self.last_output < self.ttl / 4
180    }
181
182    /// Record a heartbeat (output event).
183    pub fn heartbeat(&mut self) {
184        self.last_output = Utc::now();
185    }
186
187    /// How many missed beats since last output.
188    pub fn missed_beats(&self) -> i64 {
189        (Utc::now() - self.last_output).num_seconds() / (self.ttl.num_seconds() / 4).max(1)
190    }
191
192    /// The agent's heading (what it's working on).
193    pub fn heading(&self) -> &str { &self.heading }
194
195    /// Change heading (a refit).
196    pub fn change_heading(&mut self, heading: impl Into<String>) {
197        self.heading = heading.into();
198    }
199
200    /// Filter to only present agents. No heartbeat protocol. No health checks.
201    pub fn filter_present(agents: &[Self]) -> Vec<&Self> {
202        agents.iter().filter(|a| a.is_present()).collect()
203    }
204}
205
206// ─── BearingTtl: Self-Expiring Relationships ───────────────────────────────────────
207
208/// A bearing observation between two agents.
209///
210/// Set by the observer based on distance. Close agents: short TTL.
211/// Distant agents: long TTL. Expired bearings mean unknown position.
212/// "Stale bearings ARE collision warnings."
213#[derive(Debug, Clone)]
214pub struct BearingTtl {
215    target: String,
216    angle: f64,     // radians, angle between heading vectors
217    rate: f64,      // first derivative of angle (radians per second)
218    observed: DateTime<Utc>,
219    ttl: Duration,
220}
221
222impl BearingTtl {
223    pub fn new(target: impl Into<String>, angle: f64, rate: f64, ttl: Duration) -> Self {
224        Self { target: target.into(), angle, rate, observed: Utc::now(), ttl }
225    }
226
227    /// Assess collision risk.
228    /// "If the bearing isn't changing, you're on a collision course."
229    /// Expired bearing = unknown position = Critical.
230    pub fn collision_risk(&self) -> Risk {
231        let now = Utc::now();
232        if now > self.observed + self.ttl {
233            return Risk::Critical; // Position unknown. Highest alert.
234        }
235        if self.rate.abs() < 0.001 && self.angle.abs() < 1.0 {
236            return Risk::Warning; // Constant bearing, overlapping heading.
237        }
238        Risk::Stable
239    }
240
241    /// Is this bearing still current?
242    pub fn is_current(&self) -> bool {
243        Utc::now() <= self.observed + self.ttl
244    }
245}
246
247// ─── TrustTtl: Self-Expiring Assertions ────────────────────────────────────────────
248
249/// A trust assertion that decays.
250///
251/// Trust carries confidence and a lifetime. It decays linearly with age
252/// and exponentially with provenance depth. No certificate revocation list.
253/// No central authority. Every agent builds its own trust workspace.
254#[derive(Debug, Clone)]
255pub struct TrustTtl {
256    assertion: String,
257    confidence: f64,
258    provenance_depth: u8,
259    proven: DateTime<Utc>,
260    ttl: Duration,
261}
262
263impl TrustTtl {
264    pub fn new(assertion: impl Into<String>, confidence: f64, depth: u8, ttl: Duration) -> Self {
265        Self {
266            assertion: assertion.into(),
267            confidence: confidence.clamp(0.0, 1.0),
268            provenance_depth: depth,
269            proven: Utc::now(),
270            ttl,
271        }
272    }
273
274    /// Effective confidence after time decay AND provenance decay.
275    /// Trust decays linearly: at TTL/2, confidence drops to 75% of original.
276    /// Provenance: each hop reduces weight by 50%.
277    pub fn effective_confidence(&self) -> f64 {
278        let age = Utc::now() - self.proven;
279        let age_frac = (age.num_milliseconds() as f64 / self.ttl.num_milliseconds() as f64).min(1.0);
280        let time_decay = 1.0 - age_frac * 0.5; // Linear: 50% decay at full TTL
281        let hop_decay = 0.5_f64.powi(self.provenance_depth as i32); // 50% per hop
282        self.confidence * time_decay * hop_decay
283    }
284
285    /// Is this assertion above the "accept without verification" threshold (0.7)?
286    pub fn is_trusted(&self) -> bool {
287        self.effective_confidence() >= 0.7
288    }
289
290    /// Is this assertion in the "verify before processing" zone (0.3–0.7)?
291    pub fn needs_verification(&self) -> bool {
292        let c = self.effective_confidence();
293        c >= 0.3 && c < 0.7
294    }
295
296    /// Has this assertion decayed below the "re-request" threshold (0.3)?
297    pub fn needs_renewal(&self) -> bool {
298        self.effective_confidence() < 0.3
299    }
300
301    /// Renew an assertion — reset the clock and optionally adjust confidence.
302    pub fn renew(&self, new_confidence: f64) -> Self {
303        Self::new(
304            self.assertion.clone(),
305            new_confidence,
306            self.provenance_depth,
307            self.ttl,
308        )
309    }
310}
311
312// ─── Tests ──────────────────────────────────────────────────────────────────────────
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use std::thread;
318    use chrono::Duration;
319
320    #[test]
321    fn tile_ttl_is_alive_after_creation() {
322        let tile = TileTtl::new("test", Duration::hours(1));
323        assert!(tile.is_alive());
324    }
325
326    #[test]
327    fn tile_ttl_dies_after_ttl() {
328        let tile = TileTtl::new("test", Duration::milliseconds(1));
329        thread::sleep(std::time::Duration::from_millis(5));
330        assert!(!tile.is_alive());
331    }
332
333    #[test]
334    fn tile_ttl_data_returns_none_when_dead() {
335        let tile = TileTtl::new("test", Duration::milliseconds(1));
336        thread::sleep(std::time::Duration::from_millis(5));
337        assert!(tile.data().is_none());
338    }
339
340    #[test]
341    fn tile_ttl_filter_active() {
342        let tiles = vec![
343            TileTtl::new("fresh", Duration::hours(1)),
344            TileTtl::new("stale", Duration::milliseconds(1)),
345        ];
346        thread::sleep(std::time::Duration::from_millis(5));
347        let alive = TileTtl::filter_active(&tiles);
348        assert_eq!(alive.len(), 1);
349        assert_eq!(alive[0].data(), Some("fresh"));
350    }
351
352    #[test]
353    fn tile_ttl_freshness_decays() {
354        let tile = TileTtl::new("test", Duration::hours(1));
355        let f = tile.freshness();
356        assert!(f > 0.99 && f <= 1.0);
357    }
358
359    #[test]
360    fn task_ttl_stale_after_ttl() {
361        let mut task = TaskTtl::new(
362            vec!["step1".into(), "step2".into()],
363            Duration::milliseconds(1),
364        );
365        thread::sleep(std::time::Duration::from_millis(5));
366        assert!(task.is_stale());
367    }
368
369    #[test]
370    fn task_ttl_execute_until_stale() {
371        let mut task = TaskTtl::new(
372            vec!["step1".into(), "step2".into(), "step3".into()],
373            Duration::hours(1),
374        );
375        let done = task.execute_until_stale();
376        assert_eq!(done, 3);
377    }
378
379    #[test]
380    fn agent_ttl_present_after_creation() {
381        let agent = AgentTtl::new("research", Duration::hours(1));
382        assert!(agent.is_present());
383    }
384
385    #[test]
386    fn agent_ttl_fades_without_output() {
387        let agent = AgentTtl::new("research", Duration::milliseconds(10));
388        thread::sleep(std::time::Duration::from_millis(15));
389        assert!(!agent.is_present());
390    }
391
392    #[test]
393    fn agent_ttl_heartbeat_resets_fade() {
394        let mut agent = AgentTtl::new("research", Duration::milliseconds(50));
395        thread::sleep(std::time::Duration::from_millis(10));
396        agent.heartbeat();
397        thread::sleep(std::time::Duration::from_millis(10));
398        agent.heartbeat();
399        assert!(agent.is_present());
400    }
401
402    #[test]
403    fn bearing_ttl_stable_when_changing() {
404        let bearing = BearingTtl::new("target", 0.5, 0.1, Duration::hours(1));
405        assert_eq!(bearing.collision_risk(), Risk::Stable);
406    }
407
408    #[test]
409    fn bearing_ttl_warning_when_constant() {
410        let bearing = BearingTtl::new("target", 0.1, 0.0001, Duration::hours(1));
411        assert_eq!(bearing.collision_risk(), Risk::Warning);
412    }
413
414    #[test]
415    fn bearing_ttl_critical_when_expired() {
416        let bearing = BearingTtl::new("target", 0.5, 0.1, Duration::milliseconds(1));
417        thread::sleep(std::time::Duration::from_millis(5));
418        assert_eq!(bearing.collision_risk(), Risk::Critical);
419    }
420
421    #[test]
422    fn trust_ttl_confidence_decays() {
423        let trust = TrustTtl::new("verified proof", 0.95, 0, Duration::hours(1));
424        let c = trust.effective_confidence();
425        assert!(c > 0.9 && c <= 1.0);
426    }
427
428    #[test]
429    fn trust_ttl_provenance_halves_weight() {
430        let direct = TrustTtl::new("seen myself", 1.0, 0, Duration::hours(1));
431        let hop1 = TrustTtl::new("heard from bob", 1.0, 1, Duration::hours(1));
432        let hop2 = TrustTtl::new("bob heard from alice", 1.0, 2, Duration::hours(1));
433        assert!(direct.effective_confidence() > hop1.effective_confidence());
434        assert!(hop1.effective_confidence() > hop2.effective_confidence());
435    }
436
437    #[test]
438    fn trust_ttl_gray_zones() {
439        let trusted = TrustTtl::new("high trust", 0.9, 0, Duration::hours(1));
440        assert!(trusted.is_trusted());
441
442        let borderline = TrustTtl::new("medium trust", 0.7, 1, Duration::hours(1));
443        // After decay this should be in verification zone
444        assert!(borderline.is_trusted() || borderline.needs_verification());
445
446        let expired = TrustTtl::new("old trust", 0.5, 0, Duration::milliseconds(1));
447        thread::sleep(std::time::Duration::from_millis(5));
448        assert!(expired.needs_renewal() || expired.needs_verification());
449    }
450}