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}