use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum ReActStep {
Thought(String),
Action { tool: String, input: String },
Observation(String),
FinalAnswer(String),
}
impl ReActStep {
pub fn kind(&self) -> &'static str {
match self {
ReActStep::Thought(_) => "Thought",
ReActStep::Action { .. } => "Action",
ReActStep::Observation(_) => "Observation",
ReActStep::FinalAnswer(_) => "FinalAnswer",
}
}
pub fn is_final(&self) -> bool {
matches!(self, ReActStep::FinalAnswer(_))
}
pub fn content(&self) -> &str {
match self {
ReActStep::Thought(s) | ReActStep::Observation(s) | ReActStep::FinalAnswer(s) => s,
ReActStep::Action { input, .. } => input,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Role {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub role: Role,
pub content: String,
pub token_estimate: usize,
}
impl Message {
pub fn new(role: Role, content: impl Into<String>) -> Self {
let content = content.into();
let token_estimate = content.len() / 4;
Self { role, content, token_estimate }
}
pub fn system(content: impl Into<String>) -> Self { Self::new(Role::System, content) }
pub fn user(content: impl Into<String>) -> Self { Self::new(Role::User, content) }
pub fn assistant(content: impl Into<String>) -> Self { Self::new(Role::Assistant, content) }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentConfig {
pub max_iterations: u32,
pub context_token_limit: usize,
pub timeout_ms: Option<u64>,
pub model: String,
}
impl Default for AgentConfig {
fn default() -> Self {
Self {
max_iterations: 10,
context_token_limit: 8192,
timeout_ms: Some(30_000),
model: "claude-haiku-4-5-20251001".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolResult {
pub tool_name: String,
pub output: String,
pub success: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_react_step_thought_kind() {
let s = ReActStep::Thought("think".into());
assert_eq!(s.kind(), "Thought");
assert!(!s.is_final());
}
#[test]
fn test_react_step_final_answer_is_final() {
let s = ReActStep::FinalAnswer("done".into());
assert!(s.is_final());
}
#[test]
fn test_react_step_action_kind() {
let s = ReActStep::Action { tool: "search".into(), input: "query".into() };
assert_eq!(s.kind(), "Action");
}
#[test]
fn test_react_step_observation_kind() {
let s = ReActStep::Observation("result".into());
assert_eq!(s.kind(), "Observation");
assert!(!s.is_final());
}
#[test]
fn test_react_step_content_returns_text() {
let s = ReActStep::Thought("my thought".into());
assert_eq!(s.content(), "my thought");
}
#[test]
fn test_react_step_action_content_is_input() {
let s = ReActStep::Action { tool: "t".into(), input: "i".into() };
assert_eq!(s.content(), "i");
}
#[test]
fn test_message_token_estimate_nonzero_for_nonempty() {
let m = Message::user("hello world this is a test");
assert!(m.token_estimate > 0);
}
#[test]
fn test_message_empty_content_zero_tokens() {
let m = Message::user("");
assert_eq!(m.token_estimate, 0);
}
#[test]
fn test_agent_config_default_has_reasonable_limits() {
let c = AgentConfig::default();
assert!(c.max_iterations > 0);
assert!(c.context_token_limit > 0);
}
#[test]
fn test_message_system_has_system_role() {
let m = Message::system("sys");
assert_eq!(m.role, Role::System);
}
#[test]
fn test_message_assistant_has_assistant_role() {
let m = Message::assistant("asst");
assert_eq!(m.role, Role::Assistant);
}
}