1pub(crate) use crate::agent::driver::StreamEvent;
9use crate::agent::manifest::AgentManifest;
10use crate::agent::phase::LoopPhase;
11use crate::agent::result::{StopReason, TokenUsage};
12
13fn truncate_str(s: &str, max_len: usize) -> String {
15 if s.len() <= max_len {
16 s.to_owned()
17 } else {
18 format!("{}...", &s[..max_len.saturating_sub(3)])
19 }
20}
21
22#[derive(Debug, Clone)]
24pub struct AgentDashboardState {
25 pub agent_name: String,
27 pub phase: LoopPhase,
29 pub iteration: u32,
31 pub max_iterations: u32,
33 pub usage: TokenUsage,
35 pub token_budget: Option<u64>,
37 pub tool_calls: u32,
39 pub max_tool_calls: u32,
41 pub recent_text: Vec<String>,
43 pub tool_log: Vec<ToolLogEntry>,
45 pub cost_usd: f64,
47 pub max_cost_usd: f64,
49 pub running: bool,
51 pub stop_reason: Option<StopReason>,
53}
54
55#[derive(Debug, Clone)]
57pub struct ToolLogEntry {
58 pub name: String,
60 pub input_summary: String,
62 pub result_summary: String,
64}
65
66impl AgentDashboardState {
67 pub fn from_manifest(manifest: &AgentManifest) -> Self {
69 Self {
70 agent_name: manifest.name.clone(),
71 phase: LoopPhase::Perceive,
72 iteration: 0,
73 max_iterations: manifest.resources.max_iterations,
74 usage: TokenUsage { input_tokens: 0, output_tokens: 0 },
75 token_budget: manifest.resources.max_tokens_budget,
76 tool_calls: 0,
77 max_tool_calls: manifest.resources.max_tool_calls,
78 recent_text: Vec::new(),
79 tool_log: Vec::new(),
80 cost_usd: 0.0,
81 max_cost_usd: manifest.resources.max_cost_usd,
82 running: true,
83 stop_reason: None,
84 }
85 }
86
87 pub fn apply_event(&mut self, event: &StreamEvent) {
89 match event {
90 StreamEvent::PhaseChange { phase } => {
91 self.phase = phase.clone();
92 }
93 StreamEvent::TextDelta { text } => {
94 self.push_text(text);
95 }
96 StreamEvent::ToolUseStart { name, .. } => {
97 self.push_tool_start(name);
98 }
99 StreamEvent::ToolUseEnd { name, result, .. } => {
100 self.complete_tool(name, result);
101 }
102 StreamEvent::ContentComplete { stop_reason, usage } => {
103 self.usage = usage.clone();
104 self.stop_reason = Some(stop_reason.clone());
105 self.running = false;
106 }
107 }
108 }
109
110 fn push_text(&mut self, text: &str) {
111 self.recent_text.push(text.to_owned());
112 if self.recent_text.len() > 20 {
113 self.recent_text.remove(0);
114 }
115 }
116
117 fn push_tool_start(&mut self, name: &str) {
118 self.tool_calls += 1;
119 self.tool_log.push(ToolLogEntry {
120 name: name.to_owned(),
121 input_summary: String::new(),
122 result_summary: "running...".into(),
123 });
124 if self.tool_log.len() > 10 {
125 self.tool_log.remove(0);
126 }
127 }
128
129 fn complete_tool(&mut self, name: &str, result: &str) {
130 let Some(entry) = self.tool_log.iter_mut().rev().find(|e| e.name == name) else {
131 return;
132 };
133 entry.result_summary = truncate_str(result, 60);
134 }
135
136 pub fn iteration_pct(&self) -> u32 {
138 if self.max_iterations == 0 {
139 return 0;
140 }
141 (self.iteration * 100) / self.max_iterations
142 }
143
144 pub fn token_budget_pct(&self) -> u32 {
146 let Some(budget) = self.token_budget else {
147 return 0;
148 };
149 if budget == 0 {
150 return 0;
151 }
152 let total = self.usage.input_tokens + self.usage.output_tokens;
153 ((total * 100) / budget) as u32
154 }
155}
156
157#[cfg(feature = "presentar-terminal")]
164#[path = "tui_render.rs"]
165mod tui_render;
166
167#[cfg(feature = "presentar-terminal")]
168pub use tui_render::AgentDashboard;
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173
174 #[test]
175 fn test_dashboard_state_from_manifest() {
176 let manifest = AgentManifest::default();
177 let state = AgentDashboardState::from_manifest(&manifest);
178 assert!(state.running);
179 assert_eq!(state.iteration, 0);
180 assert_eq!(state.max_iterations, manifest.resources.max_iterations,);
181 }
182
183 #[test]
184 fn test_apply_text_delta() {
185 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
186 state.apply_event(&StreamEvent::TextDelta { text: "hello".into() });
187 assert_eq!(state.recent_text.len(), 1);
188 assert_eq!(state.recent_text[0], "hello");
189 }
190
191 #[test]
192 fn test_apply_content_complete() {
193 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
194 state.apply_event(&StreamEvent::ContentComplete {
195 stop_reason: StopReason::EndTurn,
196 usage: TokenUsage { input_tokens: 100, output_tokens: 50 },
197 });
198 assert!(!state.running);
199 assert_eq!(state.usage.input_tokens, 100);
200 assert!(matches!(state.stop_reason, Some(StopReason::EndTurn)));
201 }
202
203 #[test]
204 fn test_apply_tool_use_events() {
205 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
206 state.apply_event(&StreamEvent::ToolUseStart { id: "1".into(), name: "rag".into() });
207 assert_eq!(state.tool_calls, 1);
208 assert_eq!(state.tool_log.len(), 1);
209 assert_eq!(state.tool_log[0].name, "rag");
210
211 state.apply_event(&StreamEvent::ToolUseEnd {
212 id: "1".into(),
213 name: "rag".into(),
214 result: "found 3 results".into(),
215 });
216 assert_eq!(state.tool_log[0].result_summary, "found 3 results",);
217 }
218
219 #[test]
220 fn test_apply_phase_change() {
221 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
222 state.apply_event(&StreamEvent::PhaseChange {
223 phase: LoopPhase::Act { tool_name: "rag".into() },
224 });
225 assert!(matches!(state.phase, LoopPhase::Act { .. }));
226 }
227
228 #[test]
229 fn test_iteration_pct() {
230 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
231 state.max_iterations = 10;
232 state.iteration = 3;
233 assert_eq!(state.iteration_pct(), 30);
234 }
235
236 #[test]
237 fn test_iteration_pct_zero_max() {
238 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
239 state.max_iterations = 0;
240 assert_eq!(state.iteration_pct(), 0);
241 }
242
243 #[test]
244 fn test_token_budget_pct() {
245 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
246 state.token_budget = Some(1000);
247 state.usage = TokenUsage { input_tokens: 400, output_tokens: 100 };
248 assert_eq!(state.token_budget_pct(), 50);
249 }
250
251 #[test]
252 fn test_token_budget_pct_unlimited() {
253 let state = AgentDashboardState::from_manifest(&AgentManifest::default());
254 assert_eq!(state.token_budget_pct(), 0);
255 }
256
257 #[test]
258 fn test_recent_text_capped_at_20() {
259 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
260 for i in 0..25 {
261 state.apply_event(&StreamEvent::TextDelta { text: format!("t{i}") });
262 }
263 assert_eq!(state.recent_text.len(), 20);
264 assert_eq!(state.recent_text[0], "t5");
265 }
266
267 #[test]
268 fn test_tool_log_capped_at_10() {
269 let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
270 for i in 0..12 {
271 state.apply_event(&StreamEvent::ToolUseStart {
272 id: format!("{i}"),
273 name: format!("tool_{i}"),
274 });
275 }
276 assert_eq!(state.tool_log.len(), 10);
277 assert_eq!(state.tool_log[0].name, "tool_2");
278 }
279}