Skip to main content

hh_cli/core/agent/
state.rs

1use crate::core::{Message, Role, TodoItem};
2use crate::tool::ToolResult;
3use serde::Deserialize;
4
5#[derive(Debug, Default)]
6pub struct AgentState {
7    pub messages: Vec<Message>,
8    pub todo_items: Vec<TodoItem>,
9    pub step: usize,
10}
11
12impl AgentState {
13    pub fn push(&mut self, msg: Message) {
14        self.messages.push(msg);
15    }
16
17    pub fn apply_tool_result(&mut self, tool_name: &str, result: &ToolResult) -> bool {
18        if result.is_error || tool_name != "todo_write" {
19            return false;
20        }
21
22        let Some(items) = parse_todos(result) else {
23            return false;
24        };
25
26        if self.todo_items == items {
27            return false;
28        }
29
30        self.todo_items = items;
31        true
32    }
33
34    pub fn state_for_llm(&self) -> Option<Message> {
35        if self.todo_items.is_empty() {
36            return None;
37        }
38
39        let mut lines = Vec::new();
40        lines.push("Runtime TODO state: use this as the canonical plan snapshot.".to_string());
41
42        let total = self.todo_items.len();
43        let pending = self
44            .todo_items
45            .iter()
46            .filter(|item| {
47                matches!(
48                    item.status,
49                    crate::core::TodoStatus::Pending | crate::core::TodoStatus::InProgress
50                )
51            })
52            .count();
53        lines.push(format!("{pending} pending out of {total} total tasks."));
54
55        for item in &self.todo_items {
56            let status = match item.status {
57                crate::core::TodoStatus::Pending => "pending",
58                crate::core::TodoStatus::InProgress => "in_progress",
59                crate::core::TodoStatus::Completed => "completed",
60                crate::core::TodoStatus::Cancelled => "cancelled",
61            };
62            lines.push(format!("- [{status}] {}", item.content));
63        }
64
65        Some(Message {
66            role: Role::System,
67            content: lines.join("\n"),
68            attachments: Vec::new(),
69            tool_call_id: None,
70        })
71    }
72}
73
74#[derive(Debug, Deserialize)]
75struct TodoWriteOutput {
76    todos: Vec<TodoItem>,
77}
78
79fn parse_todos(result: &ToolResult) -> Option<Vec<TodoItem>> {
80    if let Ok(parsed) = serde_json::from_value::<TodoWriteOutput>(result.payload.clone()) {
81        return Some(parsed.todos);
82    }
83
84    serde_json::from_str::<TodoWriteOutput>(&result.output)
85        .ok()
86        .map(|parsed| parsed.todos)
87}