Skip to main content

idlewarden_plugin_api/
observation.rs

1// SPDX-License-Identifier: Apache-2.0
2//! What the Core perceives, and how sure it is about it.
3//!
4//! Vision is probabilistic. Carrying that uncertainty all the way to the agent
5//! is what stops it from clicking on ghosts (ADR-0002), so every signal has a
6//! [`Confidence`] and every observation has an age.
7
8use crate::{manifest::SignalId, value::Value};
9use serde::{Deserialize, Serialize};
10
11/// A perception confidence in `0.0..=1.0`.
12#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
13pub struct Confidence(f64);
14
15impl Confidence {
16    pub const CERTAIN: Confidence = Confidence(1.0);
17
18    pub fn new(v: f64) -> Self {
19        Confidence(v.clamp(0.0, 1.0))
20    }
21
22    pub fn get(self) -> f64 {
23        self.0
24    }
25}
26
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct Signal {
29    pub id: SignalId,
30    pub value: Value,
31    pub confidence: Confidence,
32}
33
34/// One perception pass over one captured frame.
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36pub struct Observation {
37    /// Monotonic id of the frame this was derived from.
38    pub frame_id: u64,
39    /// Milliseconds since the session started, when the frame was captured.
40    pub captured_at_ms: u64,
41    pub signals: Vec<Signal>,
42}
43
44impl Observation {
45    pub fn get(&self, id: &str) -> Option<&Signal> {
46        self.signals.iter().find(|s| s.id.as_str() == id)
47    }
48
49    /// The lowest confidence across all signals, the Governor pauses the agent
50    /// when this drops below the configured floor.
51    pub fn weakest_confidence(&self) -> Confidence {
52        self.signals
53            .iter()
54            .map(|s| s.confidence)
55            .fold(Confidence::CERTAIN, |a, b| if b < a { b } else { a })
56    }
57
58    /// How stale this observation is, given the current session clock.
59    pub fn age_ms(&self, now_ms: u64) -> u64 {
60        now_ms.saturating_sub(self.captured_at_ms)
61    }
62}