pub(crate) use crate::agent::driver::StreamEvent;
use crate::agent::manifest::AgentManifest;
use crate::agent::phase::LoopPhase;
use crate::agent::result::{StopReason, TokenUsage};
fn truncate_str(s: &str, max_len: usize) -> String {
if s.len() <= max_len {
s.to_owned()
} else {
format!("{}...", &s[..max_len.saturating_sub(3)])
}
}
#[derive(Debug, Clone)]
pub struct AgentDashboardState {
pub agent_name: String,
pub phase: LoopPhase,
pub iteration: u32,
pub max_iterations: u32,
pub usage: TokenUsage,
pub token_budget: Option<u64>,
pub tool_calls: u32,
pub max_tool_calls: u32,
pub recent_text: Vec<String>,
pub tool_log: Vec<ToolLogEntry>,
pub cost_usd: f64,
pub max_cost_usd: f64,
pub running: bool,
pub stop_reason: Option<StopReason>,
}
#[derive(Debug, Clone)]
pub struct ToolLogEntry {
pub name: String,
pub input_summary: String,
pub result_summary: String,
}
impl AgentDashboardState {
pub fn from_manifest(manifest: &AgentManifest) -> Self {
Self {
agent_name: manifest.name.clone(),
phase: LoopPhase::Perceive,
iteration: 0,
max_iterations: manifest.resources.max_iterations,
usage: TokenUsage { input_tokens: 0, output_tokens: 0 },
token_budget: manifest.resources.max_tokens_budget,
tool_calls: 0,
max_tool_calls: manifest.resources.max_tool_calls,
recent_text: Vec::new(),
tool_log: Vec::new(),
cost_usd: 0.0,
max_cost_usd: manifest.resources.max_cost_usd,
running: true,
stop_reason: None,
}
}
pub fn apply_event(&mut self, event: &StreamEvent) {
match event {
StreamEvent::PhaseChange { phase } => {
self.phase = phase.clone();
}
StreamEvent::TextDelta { text } => {
self.push_text(text);
}
StreamEvent::ToolUseStart { name, .. } => {
self.push_tool_start(name);
}
StreamEvent::ToolUseEnd { name, result, .. } => {
self.complete_tool(name, result);
}
StreamEvent::ContentComplete { stop_reason, usage } => {
self.usage = usage.clone();
self.stop_reason = Some(stop_reason.clone());
self.running = false;
}
}
}
fn push_text(&mut self, text: &str) {
self.recent_text.push(text.to_owned());
if self.recent_text.len() > 20 {
self.recent_text.remove(0);
}
}
fn push_tool_start(&mut self, name: &str) {
self.tool_calls += 1;
self.tool_log.push(ToolLogEntry {
name: name.to_owned(),
input_summary: String::new(),
result_summary: "running...".into(),
});
if self.tool_log.len() > 10 {
self.tool_log.remove(0);
}
}
fn complete_tool(&mut self, name: &str, result: &str) {
let Some(entry) = self.tool_log.iter_mut().rev().find(|e| e.name == name) else {
return;
};
entry.result_summary = truncate_str(result, 60);
}
pub fn iteration_pct(&self) -> u32 {
if self.max_iterations == 0 {
return 0;
}
(self.iteration * 100) / self.max_iterations
}
pub fn token_budget_pct(&self) -> u32 {
let Some(budget) = self.token_budget else {
return 0;
};
if budget == 0 {
return 0;
}
let total = self.usage.input_tokens + self.usage.output_tokens;
((total * 100) / budget) as u32
}
}
#[cfg(feature = "presentar-terminal")]
#[path = "tui_render.rs"]
mod tui_render;
#[cfg(feature = "presentar-terminal")]
pub use tui_render::AgentDashboard;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dashboard_state_from_manifest() {
let manifest = AgentManifest::default();
let state = AgentDashboardState::from_manifest(&manifest);
assert!(state.running);
assert_eq!(state.iteration, 0);
assert_eq!(state.max_iterations, manifest.resources.max_iterations,);
}
#[test]
fn test_apply_text_delta() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
state.apply_event(&StreamEvent::TextDelta { text: "hello".into() });
assert_eq!(state.recent_text.len(), 1);
assert_eq!(state.recent_text[0], "hello");
}
#[test]
fn test_apply_content_complete() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
state.apply_event(&StreamEvent::ContentComplete {
stop_reason: StopReason::EndTurn,
usage: TokenUsage { input_tokens: 100, output_tokens: 50 },
});
assert!(!state.running);
assert_eq!(state.usage.input_tokens, 100);
assert!(matches!(state.stop_reason, Some(StopReason::EndTurn)));
}
#[test]
fn test_apply_tool_use_events() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
state.apply_event(&StreamEvent::ToolUseStart { id: "1".into(), name: "rag".into() });
assert_eq!(state.tool_calls, 1);
assert_eq!(state.tool_log.len(), 1);
assert_eq!(state.tool_log[0].name, "rag");
state.apply_event(&StreamEvent::ToolUseEnd {
id: "1".into(),
name: "rag".into(),
result: "found 3 results".into(),
});
assert_eq!(state.tool_log[0].result_summary, "found 3 results",);
}
#[test]
fn test_apply_phase_change() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
state.apply_event(&StreamEvent::PhaseChange {
phase: LoopPhase::Act { tool_name: "rag".into() },
});
assert!(matches!(state.phase, LoopPhase::Act { .. }));
}
#[test]
fn test_iteration_pct() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
state.max_iterations = 10;
state.iteration = 3;
assert_eq!(state.iteration_pct(), 30);
}
#[test]
fn test_iteration_pct_zero_max() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
state.max_iterations = 0;
assert_eq!(state.iteration_pct(), 0);
}
#[test]
fn test_token_budget_pct() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
state.token_budget = Some(1000);
state.usage = TokenUsage { input_tokens: 400, output_tokens: 100 };
assert_eq!(state.token_budget_pct(), 50);
}
#[test]
fn test_token_budget_pct_unlimited() {
let state = AgentDashboardState::from_manifest(&AgentManifest::default());
assert_eq!(state.token_budget_pct(), 0);
}
#[test]
fn test_recent_text_capped_at_20() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
for i in 0..25 {
state.apply_event(&StreamEvent::TextDelta { text: format!("t{i}") });
}
assert_eq!(state.recent_text.len(), 20);
assert_eq!(state.recent_text[0], "t5");
}
#[test]
fn test_tool_log_capped_at_10() {
let mut state = AgentDashboardState::from_manifest(&AgentManifest::default());
for i in 0..12 {
state.apply_event(&StreamEvent::ToolUseStart {
id: format!("{i}"),
name: format!("tool_{i}"),
});
}
assert_eq!(state.tool_log.len(), 10);
assert_eq!(state.tool_log[0].name, "tool_2");
}
}