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}