Skip to main content

agent_engine/session/
view.rs

1//! `RuntimeView` + `RuntimeRead` (PLAN-phase2 §2.8).
2//!
3//! The TUI's synchronous getters (`runtime.model()` …) compile against
4//! either a live `Runtime` (today) or a published `RuntimeView` snapshot
5//! (day 2, over a transport). Trait signatures are copied EXACTLY from
6//! `Runtime` so call sites do not change.
7
8use agent_core::reasoning::ReasoningLevel;
9use serde::{Deserialize, Serialize};
10
11use super::types::reasoning_level_serde;
12
13/// Snapshot of the read-mostly Runtime getters the clients render from.
14/// Refreshed by the actor after every Set, after prompt reload, at turn start.
15#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
16pub struct RuntimeView {
17    pub model: String,
18    pub thinking_level: String,
19    #[serde(with = "reasoning_level_serde")]
20    pub reasoning_level: ReasoningLevel,
21    pub is_reasoning_explicit: bool,
22    pub thinking_budget: u32,
23    pub context_window: u64,
24    pub system_prompt: Option<String>,
25    pub compaction_model: String,
26    pub api_retries: u32,
27    pub subagent_timeout: u64,
28    pub max_tool_output: usize,
29    pub bash_timeout: u64,
30    pub bash_max_timeout: u64,
31    pub prompt_generation: u64,
32    pub hook_handler_count: usize,
33    /// `Runtime::prompt_inspection_json()` cached at snapshot time
34    /// (read once per `/prompt`).
35    pub prompt_inspection: Option<String>,
36}
37
38impl RuntimeView {
39    /// Synchronous snapshot. `hook_handler_count` is supplied by the caller
40    /// because `HookBus::handler_count` is async.
41    pub fn snapshot(runtime: &crate::Runtime, hook_handler_count: usize) -> Self {
42        Self {
43            model: runtime.model().to_string(),
44            thinking_level: runtime.thinking_level().to_string(),
45            reasoning_level: runtime.reasoning_level(),
46            is_reasoning_explicit: runtime.is_reasoning_explicit(),
47            thinking_budget: runtime.thinking_budget(),
48            context_window: runtime.context_window(),
49            system_prompt: runtime.system_prompt().map(str::to_string),
50            compaction_model: runtime.compaction_model().to_string(),
51            api_retries: runtime.api_retries(),
52            subagent_timeout: runtime.subagent_timeout(),
53            max_tool_output: runtime.max_tool_output(),
54            bash_timeout: runtime.bash_timeout(),
55            bash_max_timeout: runtime.bash_max_timeout(),
56            prompt_generation: runtime.prompt_generation(),
57            hook_handler_count,
58            prompt_inspection: runtime.prompt_inspection_json(),
59        }
60    }
61
62    /// Snapshot including the live hook handler count.
63    pub async fn from_runtime(runtime: &crate::Runtime) -> Self {
64        let n = runtime.hook_bus().handler_count().await;
65        Self::snapshot(runtime, n)
66    }
67}
68
69/// The 14 getters, with the EXACT signatures `Runtime` has today, so TUI
70/// call sites compile against either. Implemented for `Runtime` by
71/// delegation and for `RuntimeView` by field access.
72pub trait RuntimeRead {
73    fn model(&self) -> &str;
74    fn thinking_level(&self) -> &str;
75    fn reasoning_level(&self) -> ReasoningLevel;
76    fn is_reasoning_explicit(&self) -> bool;
77    fn thinking_budget(&self) -> u32;
78    fn context_window(&self) -> u64;
79    fn system_prompt(&self) -> Option<&str>;
80    fn compaction_model(&self) -> &str;
81    fn api_retries(&self) -> u32;
82    fn subagent_timeout(&self) -> u64;
83    fn max_tool_output(&self) -> usize;
84    fn bash_timeout(&self) -> u64;
85    fn bash_max_timeout(&self) -> u64;
86    /// `RuntimeView`: cached at snapshot time.
87    fn prompt_inspection_json(&self) -> Option<String>;
88}
89
90impl RuntimeRead for crate::Runtime {
91    fn model(&self) -> &str {
92        crate::Runtime::model(self)
93    }
94    fn thinking_level(&self) -> &str {
95        crate::Runtime::thinking_level(self)
96    }
97    fn reasoning_level(&self) -> ReasoningLevel {
98        crate::Runtime::reasoning_level(self)
99    }
100    fn is_reasoning_explicit(&self) -> bool {
101        crate::Runtime::is_reasoning_explicit(self)
102    }
103    fn thinking_budget(&self) -> u32 {
104        crate::Runtime::thinking_budget(self)
105    }
106    fn context_window(&self) -> u64 {
107        crate::Runtime::context_window(self)
108    }
109    fn system_prompt(&self) -> Option<&str> {
110        crate::Runtime::system_prompt(self)
111    }
112    fn compaction_model(&self) -> &str {
113        crate::Runtime::compaction_model(self)
114    }
115    fn api_retries(&self) -> u32 {
116        crate::Runtime::api_retries(self)
117    }
118    fn subagent_timeout(&self) -> u64 {
119        crate::Runtime::subagent_timeout(self)
120    }
121    fn max_tool_output(&self) -> usize {
122        crate::Runtime::max_tool_output(self)
123    }
124    fn bash_timeout(&self) -> u64 {
125        crate::Runtime::bash_timeout(self)
126    }
127    fn bash_max_timeout(&self) -> u64 {
128        crate::Runtime::bash_max_timeout(self)
129    }
130    fn prompt_inspection_json(&self) -> Option<String> {
131        crate::Runtime::prompt_inspection_json(self)
132    }
133}
134
135impl RuntimeRead for RuntimeView {
136    fn model(&self) -> &str {
137        &self.model
138    }
139    fn thinking_level(&self) -> &str {
140        &self.thinking_level
141    }
142    fn reasoning_level(&self) -> ReasoningLevel {
143        self.reasoning_level
144    }
145    fn is_reasoning_explicit(&self) -> bool {
146        self.is_reasoning_explicit
147    }
148    fn thinking_budget(&self) -> u32 {
149        self.thinking_budget
150    }
151    fn context_window(&self) -> u64 {
152        self.context_window
153    }
154    fn system_prompt(&self) -> Option<&str> {
155        self.system_prompt.as_deref()
156    }
157    fn compaction_model(&self) -> &str {
158        &self.compaction_model
159    }
160    fn api_retries(&self) -> u32 {
161        self.api_retries
162    }
163    fn subagent_timeout(&self) -> u64 {
164        self.subagent_timeout
165    }
166    fn max_tool_output(&self) -> usize {
167        self.max_tool_output
168    }
169    fn bash_timeout(&self) -> u64 {
170        self.bash_timeout
171    }
172    fn bash_max_timeout(&self) -> u64 {
173        self.bash_max_timeout
174    }
175    fn prompt_inspection_json(&self) -> Option<String> {
176        self.prompt_inspection.clone()
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    fn view() -> RuntimeView {
185        RuntimeView {
186            model: "m".into(),
187            thinking_level: "high".into(),
188            reasoning_level: ReasoningLevel::High,
189            is_reasoning_explicit: false,
190            thinking_budget: 1024,
191            context_window: 200_000,
192            system_prompt: Some("sys".into()),
193            compaction_model: "c".into(),
194            api_retries: 3,
195            subagent_timeout: 60,
196            max_tool_output: 4096,
197            bash_timeout: 30,
198            bash_max_timeout: 300,
199            prompt_generation: 1,
200            hook_handler_count: 0,
201            prompt_inspection: None,
202        }
203    }
204
205    #[test]
206    fn view_json_round_trip() {
207        let v = view();
208        let json = serde_json::to_string(&v).unwrap();
209        let back: RuntimeView = serde_json::from_str(&json).unwrap();
210        assert_eq!(v, back);
211        assert!(json.contains(r#""reasoning_level":"high""#));
212    }
213
214    #[test]
215    fn view_reads_through_trait() {
216        fn read(r: &impl RuntimeRead) -> (String, u64) {
217            (r.model().to_string(), r.context_window())
218        }
219        assert_eq!(read(&view()), ("m".to_string(), 200_000));
220    }
221
222    #[test]
223    fn headless_runtime_snapshot_matches_getters() {
224        let rt = crate::Runtime::new_headless();
225        let v = RuntimeView::snapshot(&rt, 0);
226        assert_eq!(v.model, crate::Runtime::model(&rt));
227        assert_eq!(v.thinking_level, crate::Runtime::thinking_level(&rt));
228        assert_eq!(v.context_window, crate::Runtime::context_window(&rt));
229        assert_eq!(v.bash_timeout, crate::Runtime::bash_timeout(&rt));
230        assert_eq!(RuntimeRead::model(&v), RuntimeRead::model(&rt));
231    }
232}