Skip to main content

deepstrike_core/context/
dashboard.rs

1use crate::types::message::Message;
2
3#[derive(Debug, Clone, Default)]
4pub struct EventSurface {
5    pub pending_events: Vec<serde_json::Value>,
6    pub active_risks: Vec<serde_json::Value>,
7    pub recent_event_decisions: Vec<serde_json::Value>,
8}
9
10#[derive(Debug, Clone, Default)]
11pub struct KnowledgeSurface {
12    pub active_questions: Vec<String>,
13    pub evidence_packs: Vec<serde_json::Value>,
14    pub citations: Vec<String>,
15}
16
17/// Legacy agent-state surface, superseded by [`crate::context::task_state::TaskState`].
18///
19/// `TaskState` is now the single source of truth for goal/plan/progress/scratchpad
20/// and is rendered into the State turn. `Dashboard` is retained only for the
21/// `render_dashboard` opt-in path (off by default) and for `WorkingMemory`.
22/// Prefer `TaskState` for new code; the `goal_progress` / `plan` / `scratchpad`
23/// fields here mirror data that `TaskState` owns canonically.
24#[derive(Debug, Clone)]
25pub struct Dashboard {
26    pub rho: f64,
27    pub token_budget: u32,
28    pub goal_progress: String,
29    pub error_count: u32,
30    pub depth: u32,
31    pub interrupt_requested: bool,
32    pub plan: Vec<String>,
33    pub event_surface: EventSurface,
34    pub knowledge_surface: KnowledgeSurface,
35    pub scratchpad: String,
36}
37
38impl Default for Dashboard {
39    fn default() -> Self {
40        Self {
41            rho: 0.0,
42            token_budget: 0,
43            goal_progress: String::new(),
44            error_count: 0,
45            depth: 0,
46            interrupt_requested: false,
47            plan: Vec::new(),
48            event_surface: EventSurface::default(),
49            knowledge_surface: KnowledgeSurface::default(),
50            scratchpad: String::new(),
51        }
52    }
53}
54
55impl Dashboard {
56    /// Compact single-block representation for embedding in system_text.
57    /// Returns an empty string when all fields are at their default/empty values
58    /// so the renderer can skip it entirely on fresh agents.
59    pub fn format_compact(&self) -> String {
60        let has_progress = !self.goal_progress.is_empty();
61        let has_plan = !self.plan.is_empty();
62        let has_scratchpad = !self.scratchpad.is_empty();
63        let has_questions = !self.knowledge_surface.active_questions.is_empty();
64        let has_activity = self.error_count > 0 || self.depth > 0 || self.interrupt_requested;
65
66        if !has_progress && !has_plan && !has_scratchpad && !has_questions && !has_activity {
67            return String::new();
68        }
69
70        let mut parts: Vec<String> = Vec::new();
71        parts.push(format!(
72            "[AGENT STATE] rho={:.3} turn={} errors={} interrupt={}",
73            self.rho, self.depth, self.error_count, self.interrupt_requested
74        ));
75        if has_progress {
76            parts.push(format!("goal_progress: {}", self.goal_progress));
77        }
78        if has_plan {
79            let plan = self
80                .plan
81                .iter()
82                .enumerate()
83                .map(|(i, s)| format!("  {}. {}", i + 1, s))
84                .collect::<Vec<_>>()
85                .join("\n");
86            parts.push(format!("plan:\n{plan}"));
87        }
88        if has_questions {
89            parts.push(format!(
90                "active_questions: {}",
91                self.knowledge_surface.active_questions.join(", ")
92            ));
93        }
94        if has_scratchpad {
95            parts.push(format!("scratchpad: {}", self.scratchpad));
96        }
97        parts.join("\n")
98    }
99
100    pub fn format_message(&self) -> Message {
101        let plan_str = if self.plan.is_empty() {
102            "(none)".to_string()
103        } else {
104            self.plan
105                .iter()
106                .enumerate()
107                .map(|(i, s)| format!("  {}. {}", i + 1, s))
108                .collect::<Vec<_>>()
109                .join("\n")
110        };
111
112        let questions = if self.knowledge_surface.active_questions.is_empty() {
113            "(none)".to_string()
114        } else {
115            self.knowledge_surface.active_questions.join(", ")
116        };
117
118        let content = format!(
119            "[DASHBOARD]\nrho={:.3} budget={} errors={} depth={} interrupt={}\ngoal_progress: {}\nplan:\n{}\nactive_questions: {}\nscratchpad: {}",
120            self.rho,
121            self.token_budget,
122            self.error_count,
123            self.depth,
124            self.interrupt_requested,
125            self.goal_progress,
126            plan_str,
127            questions,
128            self.scratchpad,
129        );
130
131        Message::system(content)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138    use crate::types::message::Role;
139
140    #[test]
141    fn format_message_produces_system_message() {
142        let d = Dashboard::default();
143        let msg = d.format_message();
144        assert_eq!(msg.role, Role::System);
145        if let crate::types::message::Content::Text(ref t) = msg.content {
146            assert!(t.contains("[DASHBOARD]"));
147        }
148    }
149}