Skip to main content

a3s_code_core/
retention.rs

1//! In-memory retention limits for long-running sessions.
2//!
3//! The framework's in-memory stores
4//! ([`InMemoryRunStore`](crate::run::InMemoryRunStore),
5//! [`InMemoryTraceSink`](crate::trace::InMemoryTraceSink),
6//! [`InMemorySubagentTaskTracker`](crate::subagent_task_tracker::InMemorySubagentTaskTracker))
7//! use conservative finite defaults so sessions that live for hours or days
8//! cannot grow these collections without bound.
9//!
10//! `SessionRetentionLimits` lets the host cap each store with a FIFO
11//! policy. `None` for any field keeps that collection unbounded. Hosts that
12//! deliberately need the legacy behavior can use
13//! [`SessionRetentionLimits::unbounded`].
14//!
15//! All caps are **soft**: when a store hits its cap, the oldest entry
16//! is dropped on insert. The framework never returns errors from cap
17//! enforcement.
18
19/// Per-session in-memory retention caps. Built via
20/// [`SessionOptions::with_retention_limits`](crate::agent_api::SessionOptions::with_retention_limits)
21/// or by constructing the struct directly.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct SessionRetentionLimits {
24    /// Maximum number of runs retained in
25    /// [`InMemoryRunStore`](crate::run::InMemoryRunStore).
26    ///
27    /// When a new run is created past this cap, the **oldest** run
28    /// (by insertion order) is dropped along with its events.
29    /// `None` keeps all runs.
30    pub max_runs_retained: Option<usize>,
31
32    /// Maximum number of event records retained per run in
33    /// [`InMemoryRunStore`](crate::run::InMemoryRunStore).
34    ///
35    /// When a run accumulates more events than this, the oldest
36    /// events are FIFO-dropped. The run snapshot's `event_count`
37    /// is **not** decremented — it remains the total ever recorded.
38    /// `None` keeps all events.
39    pub max_events_per_run: Option<usize>,
40
41    /// Maximum serialized bytes retained for event records in each run.
42    ///
43    /// This cap is enforced together with [`Self::max_events_per_run`].
44    /// Oldest records are FIFO-dropped until both limits are satisfied. A
45    /// single record larger than the cap is therefore not retained, while
46    /// the run's cumulative `event_count` and state transition still advance.
47    /// `None` does not impose a byte limit.
48    pub max_event_bytes_per_run: Option<usize>,
49
50    /// Maximum number of events retained in
51    /// [`InMemoryTraceSink`](crate::trace::InMemoryTraceSink).
52    ///
53    /// When the sink reaches this cap, the oldest event is dropped
54    /// on each new write. `None` keeps all events.
55    pub max_trace_events: Option<usize>,
56
57    /// Maximum number of **terminal** (Completed / Failed / Cancelled)
58    /// subagent task snapshots retained in
59    /// [`InMemorySubagentTaskTracker`](crate::subagent_task_tracker::InMemorySubagentTaskTracker).
60    /// Running tasks are never dropped.
61    ///
62    /// When the count of terminal entries exceeds this cap, the
63    /// oldest terminal entry (by completion time) is dropped.
64    /// `None` keeps all terminal entries.
65    pub max_terminal_subagent_tasks: Option<usize>,
66}
67
68impl SessionRetentionLimits {
69    /// Convenience builder.
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Explicitly opt into unbounded in-memory retention for every store.
75    pub const fn unbounded() -> Self {
76        Self {
77            max_runs_retained: None,
78            max_events_per_run: None,
79            max_event_bytes_per_run: None,
80            max_trace_events: None,
81            max_terminal_subagent_tasks: None,
82        }
83    }
84
85    pub fn with_max_runs(mut self, n: usize) -> Self {
86        self.max_runs_retained = Some(n);
87        self
88    }
89
90    pub fn with_max_events_per_run(mut self, n: usize) -> Self {
91        self.max_events_per_run = Some(n);
92        self
93    }
94
95    pub fn with_max_event_bytes_per_run(mut self, n: usize) -> Self {
96        self.max_event_bytes_per_run = Some(n);
97        self
98    }
99
100    pub fn with_max_trace_events(mut self, n: usize) -> Self {
101        self.max_trace_events = Some(n);
102        self
103    }
104
105    pub fn with_max_terminal_subagent_tasks(mut self, n: usize) -> Self {
106        self.max_terminal_subagent_tasks = Some(n);
107        self
108    }
109}
110
111impl Default for SessionRetentionLimits {
112    fn default() -> Self {
113        Self {
114            max_runs_retained: Some(64),
115            max_events_per_run: Some(2_048),
116            max_event_bytes_per_run: Some(8 * 1024 * 1024),
117            max_trace_events: Some(8_192),
118            max_terminal_subagent_tasks: Some(512),
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn defaults_are_finite_and_unbounded_is_explicit() {
129        let defaults = SessionRetentionLimits::default();
130        assert!(defaults.max_runs_retained.is_some());
131        assert!(defaults.max_events_per_run.is_some());
132        assert!(defaults.max_event_bytes_per_run.is_some());
133        assert!(defaults.max_trace_events.is_some());
134        assert!(defaults.max_terminal_subagent_tasks.is_some());
135        assert_eq!(
136            SessionRetentionLimits::unbounded(),
137            SessionRetentionLimits {
138                max_runs_retained: None,
139                max_events_per_run: None,
140                max_event_bytes_per_run: None,
141                max_trace_events: None,
142                max_terminal_subagent_tasks: None,
143            }
144        );
145    }
146}