hh_cli/core/agent/
state.rs1use 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}