1use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum ReActStep {
9 Thought(String),
11 Action { tool: String, input: String },
13 Observation(String),
15 FinalAnswer(String),
17}
18
19impl ReActStep {
20 pub fn kind(&self) -> &'static str {
22 match self {
23 ReActStep::Thought(_) => "Thought",
24 ReActStep::Action { .. } => "Action",
25 ReActStep::Observation(_) => "Observation",
26 ReActStep::FinalAnswer(_) => "FinalAnswer",
27 }
28 }
29
30 pub fn is_final(&self) -> bool {
32 matches!(self, ReActStep::FinalAnswer(_))
33 }
34
35 pub fn content(&self) -> &str {
37 match self {
38 ReActStep::Thought(s) | ReActStep::Observation(s) | ReActStep::FinalAnswer(s) => s,
39 ReActStep::Action { input, .. } => input,
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub enum Role {
47 System,
49 User,
51 Assistant,
53 Tool,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Message {
60 pub role: Role,
62 pub content: String,
64 pub token_estimate: usize,
66}
67
68impl Message {
69 pub fn new(role: Role, content: impl Into<String>) -> Self {
71 let content = content.into();
72 let token_estimate = content.len() / 4;
73 Self { role, content, token_estimate }
74 }
75
76 pub fn system(content: impl Into<String>) -> Self { Self::new(Role::System, content) }
78 pub fn user(content: impl Into<String>) -> Self { Self::new(Role::User, content) }
80 pub fn assistant(content: impl Into<String>) -> Self { Self::new(Role::Assistant, content) }
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AgentConfig {
87 pub max_iterations: u32,
89 pub context_token_limit: usize,
91 pub timeout_ms: Option<u64>,
93 pub model: String,
95}
96
97impl Default for AgentConfig {
98 fn default() -> Self {
99 Self {
100 max_iterations: 10,
101 context_token_limit: 8192,
102 timeout_ms: Some(30_000),
103 model: "claude-haiku-4-5-20251001".into(),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ToolResult {
111 pub tool_name: String,
113 pub output: String,
115 pub success: bool,
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn test_react_step_thought_kind() {
125 let s = ReActStep::Thought("think".into());
126 assert_eq!(s.kind(), "Thought");
127 assert!(!s.is_final());
128 }
129
130 #[test]
131 fn test_react_step_final_answer_is_final() {
132 let s = ReActStep::FinalAnswer("done".into());
133 assert!(s.is_final());
134 }
135
136 #[test]
137 fn test_react_step_action_kind() {
138 let s = ReActStep::Action { tool: "search".into(), input: "query".into() };
139 assert_eq!(s.kind(), "Action");
140 }
141
142 #[test]
143 fn test_react_step_observation_kind() {
144 let s = ReActStep::Observation("result".into());
145 assert_eq!(s.kind(), "Observation");
146 assert!(!s.is_final());
147 }
148
149 #[test]
150 fn test_react_step_content_returns_text() {
151 let s = ReActStep::Thought("my thought".into());
152 assert_eq!(s.content(), "my thought");
153 }
154
155 #[test]
156 fn test_react_step_action_content_is_input() {
157 let s = ReActStep::Action { tool: "t".into(), input: "i".into() };
158 assert_eq!(s.content(), "i");
159 }
160
161 #[test]
162 fn test_message_token_estimate_nonzero_for_nonempty() {
163 let m = Message::user("hello world this is a test");
164 assert!(m.token_estimate > 0);
165 }
166
167 #[test]
168 fn test_message_empty_content_zero_tokens() {
169 let m = Message::user("");
170 assert_eq!(m.token_estimate, 0);
171 }
172
173 #[test]
174 fn test_agent_config_default_has_reasonable_limits() {
175 let c = AgentConfig::default();
176 assert!(c.max_iterations > 0);
177 assert!(c.context_token_limit > 0);
178 }
179
180 #[test]
181 fn test_message_system_has_system_role() {
182 let m = Message::system("sys");
183 assert_eq!(m.role, Role::System);
184 }
185
186 #[test]
187 fn test_message_assistant_has_assistant_role() {
188 let m = Message::assistant("asst");
189 assert_eq!(m.role, Role::Assistant);
190 }
191}