agentron 0.1.0

Agentron (Ag): a unit of agentic work volume — Ag = hours × Mtokens.
Documentation
//! **Agentron (Ag)** — a unit of *agentic work volume*.
//!
//! ```text
//! Ag = hours × Mtokens          (Mtokens = tokens / 1_000_000)
//! ```
//!
//! It fuses the two things that actually cost during an agent run — wall-clock
//! **time** and **tokens** — into one comparable number (think kWh, not km/h).
//!
//! ```
//! use agentron::{agentron, Time, format_ag};
//!
//! assert_eq!(agentron(1_500_000.0, Time::Hours(2.0)), 3.0); // 2 h over 1.5M tok
//! assert_eq!(format_ag(0.0845), "0.08 Ag");
//! ```
//!
//! See <https://github.com/gravitymir/agentron>.

/// Symbol printed in place of the unit name.
pub const SYMBOL: &str = "Ag";

/// Wall-clock time for a run, expressed in one of several units.
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Time {
    Hours(f64),
    Seconds(f64),
    Millis(f64),
}

impl Time {
    /// This duration in hours.
    #[inline]
    pub fn hours(self) -> f64 {
        match self {
            Time::Hours(h) => h,
            Time::Seconds(s) => s / 3600.0,
            Time::Millis(ms) => ms / 3_600_000.0,
        }
    }
}

/// Agentrons for a single run: `hours × tokens / 1e6`.
///
/// ```
/// use agentron::{agentron, Time};
/// assert_eq!(agentron(1_000_000.0, Time::Seconds(3600.0)), 1.0);
/// ```
#[inline]
pub fn agentron(tokens: f64, time: Time) -> f64 {
    time.hours() * (tokens / 1_000_000.0)
}

/// Multi-agent aggregate: `(Σ hours) × (Σ Mtokens)`.
///
/// `runs` is a slice of `(tokens, hours)` pairs. This is deliberately **not**
/// `Σ(hoursᵢ × Mtokensᵢ)`: parallel agents share one real-time window, so their
/// combined volume multiplies.
///
/// ```
/// use agentron::agentron_total;
/// // (1+1) h × (1+1) Mtok = 4, not 2
/// assert_eq!(agentron_total(&[(1_000_000.0, 1.0), (1_000_000.0, 1.0)]), 4.0);
/// ```
pub fn agentron_total(runs: &[(f64, f64)]) -> f64 {
    let mut tokens = 0.0;
    let mut hours = 0.0;
    for &(t, h) in runs {
        tokens += t;
        hours += h;
    }
    hours * (tokens / 1_000_000.0)
}

/// Drop-in twin of agent_web's `computeAgentron(durationMs, tokens)`.
#[inline]
pub fn compute_agentron(duration_ms: f64, tokens: f64) -> f64 {
    agentron(tokens, Time::Millis(duration_ms))
}

/// Human string, e.g. `"3.0 Ag"`; values below 1 keep two decimals (`"0.08 Ag"`).
pub fn format_ag(value: f64) -> String {
    let s = if value >= 1.0 {
        format!("{value:.1}")
    } else {
        format!("{value:.2}")
    };
    format!("{s} {SYMBOL}")
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn basic() {
        // 2 h × 1.5M tok = 3.0 Ag
        assert_eq!(agentron(1_500_000.0, Time::Hours(2.0)), 3.0);
    }

    #[test]
    fn time_units_equivalent() {
        assert_eq!(agentron(1_000_000.0, Time::Seconds(3600.0)), 1.0);
        assert_eq!(agentron(1_000_000.0, Time::Millis(3_600_000.0)), 1.0);
        assert_eq!(compute_agentron(3_600_000.0, 1_000_000.0), 1.0);
    }

    #[test]
    fn multi_agent_multiplies() {
        assert_eq!(agentron_total(&[(1_000_000.0, 1.0), (1_000_000.0, 1.0)]), 4.0);
    }

    #[test]
    fn formatting() {
        assert_eq!(format_ag(3.0), "3.0 Ag");
        assert_eq!(format_ag(0.0845), "0.08 Ag");
        assert_eq!(SYMBOL, "Ag");
    }
}