Skip to main content

a3s_code_core/
host_env.rs

1//! Host-environment plumbing: ID generation and time.
2//!
3//! The framework relies on two ambient capabilities — fresh IDs and the
4//! current time — at many call sites (`session_id`, `run_id`, event
5//! timestamps, retry backoff). Defaulting both to `uuid::Uuid::new_v4()`
6//! / `SystemTime::now()` is fine for production but blocks two
7//! cluster-grade features:
8//!
9//! - **Deterministic replay** of a run on another node for failure
10//!   investigation. With injectable [`IdGenerator`] / [`Clock`] impls a
11//!   host can record the seed and replay it bit-identical elsewhere.
12//! - **Time-bending tests** without monkey-patching `std::time`.
13//!
14//! Hosts plug a custom impl via
15//! [`SessionOptions::with_host_env`](crate::agent_api::SessionOptions::with_host_env);
16//! the framework uses [`SystemHostEnv`] (the wall-clock + random-UUID
17//! default) when none is supplied — observably identical to pre-P2
18//! behaviour.
19
20use std::sync::Arc;
21use std::time::{SystemTime, UNIX_EPOCH};
22
23/// Generator for unique identifiers used by the framework
24/// (session_id, run_id, subagent task_id, …).
25///
26/// The contract is intentionally loose: implementations may produce
27/// random, monotonic, or deterministic-by-seed IDs. The framework
28/// treats output as opaque and only requires uniqueness within the
29/// hosting process.
30pub trait IdGenerator: Send + Sync + std::fmt::Debug {
31    /// Return a fresh ID. May be called concurrently from many tasks.
32    fn next_id(&self) -> String;
33}
34
35/// Source of the current time in Unix-epoch milliseconds.
36///
37/// Same uniqueness contract as [`IdGenerator`]: the framework treats
38/// the value as opaque. Monotonicity is not required (NTP corrections
39/// happen) but typical impls are at least non-decreasing.
40pub trait Clock: Send + Sync + std::fmt::Debug {
41    /// Current time, milliseconds since Unix epoch.
42    fn now_ms(&self) -> u64;
43}
44
45/// Bundle of host-environment capabilities. Used as the single
46/// `Option<Arc<HostEnv>>` slot on [`AgentConfig`](crate::agent::AgentConfig)
47/// and [`SessionOptions`](crate::agent_api::SessionOptions) — avoids
48/// growing two parallel `Arc<dyn …>` fields.
49#[derive(Debug, Clone)]
50pub struct HostEnv {
51    pub id_generator: Arc<dyn IdGenerator>,
52    pub clock: Arc<dyn Clock>,
53}
54
55impl HostEnv {
56    /// Construct a host env from concrete components.
57    pub fn new(id_generator: Arc<dyn IdGenerator>, clock: Arc<dyn Clock>) -> Self {
58        Self {
59            id_generator,
60            clock,
61        }
62    }
63
64    /// Default system-backed host env: random UUIDs + wall clock.
65    /// Equivalent to pre-P2 behaviour.
66    pub fn system() -> Self {
67        Self {
68            id_generator: Arc::new(SystemIdGenerator),
69            clock: Arc::new(SystemClock),
70        }
71    }
72
73    /// Shortcut for `self.id_generator.next_id()`.
74    pub fn next_id(&self) -> String {
75        self.id_generator.next_id()
76    }
77
78    /// Shortcut for `self.clock.now_ms()`.
79    pub fn now_ms(&self) -> u64 {
80        self.clock.now_ms()
81    }
82}
83
84impl Default for HostEnv {
85    fn default() -> Self {
86        Self::system()
87    }
88}
89
90// ============================================================================
91// Default impls
92// ============================================================================
93
94/// UUID-v4 based ID generator — the framework default.
95#[derive(Debug, Default, Clone, Copy)]
96pub struct SystemIdGenerator;
97
98impl IdGenerator for SystemIdGenerator {
99    fn next_id(&self) -> String {
100        uuid::Uuid::new_v4().to_string()
101    }
102}
103
104/// Wall-clock time source — the framework default.
105#[derive(Debug, Default, Clone, Copy)]
106pub struct SystemClock;
107
108impl Clock for SystemClock {
109    fn now_ms(&self) -> u64 {
110        SystemTime::now()
111            .duration_since(UNIX_EPOCH)
112            .map(|d| d.as_millis() as u64)
113            .unwrap_or(0)
114    }
115}
116
117// ============================================================================
118// Deterministic helpers (cfg(test) + replay)
119// ============================================================================
120
121/// Deterministic ID generator that yields a configured prefix followed
122/// by a monotonic counter (`<prefix>-0`, `<prefix>-1`, …).
123///
124/// Public so external host crates (e.g. 书安OS replay tooling) can use it
125/// without re-implementing the pattern.
126#[derive(Debug, Default)]
127pub struct SequentialIdGenerator {
128    prefix: String,
129    counter: std::sync::atomic::AtomicU64,
130}
131
132impl SequentialIdGenerator {
133    pub fn new(prefix: impl Into<String>) -> Self {
134        Self {
135            prefix: prefix.into(),
136            counter: std::sync::atomic::AtomicU64::new(0),
137        }
138    }
139}
140
141impl IdGenerator for SequentialIdGenerator {
142    fn next_id(&self) -> String {
143        let n = self
144            .counter
145            .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
146        if self.prefix.is_empty() {
147            n.to_string()
148        } else {
149            format!("{}-{}", self.prefix, n)
150        }
151    }
152}
153
154/// Clock that returns a configured, atomically-updatable timestamp.
155/// Useful for replay (advance to recorded value) and for tests that
156/// need stable timestamps.
157#[derive(Debug)]
158pub struct FixedClock {
159    now_ms: std::sync::atomic::AtomicU64,
160}
161
162impl FixedClock {
163    pub fn new(now_ms: u64) -> Self {
164        Self {
165            now_ms: std::sync::atomic::AtomicU64::new(now_ms),
166        }
167    }
168
169    /// Atomically set the clock to a new value. Returns the previous value.
170    pub fn set(&self, now_ms: u64) -> u64 {
171        self.now_ms
172            .swap(now_ms, std::sync::atomic::Ordering::SeqCst)
173    }
174
175    /// Advance the clock by `delta_ms`.
176    pub fn advance(&self, delta_ms: u64) {
177        self.now_ms
178            .fetch_add(delta_ms, std::sync::atomic::Ordering::SeqCst);
179    }
180}
181
182impl Clock for FixedClock {
183    fn now_ms(&self) -> u64 {
184        self.now_ms.load(std::sync::atomic::Ordering::SeqCst)
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn system_host_env_produces_nonempty_ids_and_increasing_time() {
194        let env = HostEnv::system();
195        let a = env.next_id();
196        let b = env.next_id();
197        assert!(!a.is_empty());
198        assert!(!b.is_empty());
199        assert_ne!(a, b);
200        let t1 = env.now_ms();
201        std::thread::sleep(std::time::Duration::from_millis(2));
202        let t2 = env.now_ms();
203        assert!(t2 >= t1);
204    }
205
206    #[test]
207    fn sequential_id_generator_is_deterministic() {
208        let gen = SequentialIdGenerator::new("run");
209        assert_eq!(gen.next_id(), "run-0");
210        assert_eq!(gen.next_id(), "run-1");
211        assert_eq!(gen.next_id(), "run-2");
212    }
213
214    #[test]
215    fn fixed_clock_is_controllable() {
216        let clock = FixedClock::new(1000);
217        assert_eq!(clock.now_ms(), 1000);
218        clock.advance(500);
219        assert_eq!(clock.now_ms(), 1500);
220        assert_eq!(clock.set(0), 1500);
221        assert_eq!(clock.now_ms(), 0);
222    }
223}