Skip to main content

wasm_agent/
types.rs

1// SPDX-License-Identifier: MIT
2//! Core types for the ReAct agent loop.
3
4use serde::{Deserialize, Serialize};
5
6/// A single step in the ReAct loop.
7#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8pub enum ReActStep {
9    /// The agent's internal reasoning trace.
10    Thought(String),
11    /// A tool invocation with its name and JSON input.
12    Action { tool: String, input: String },
13    /// The result of a tool invocation.
14    Observation(String),
15    /// The agent's final answer, terminating the loop.
16    FinalAnswer(String),
17}
18
19impl ReActStep {
20    /// Returns a short label for the step kind.
21    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    /// Returns `true` if this step terminates the loop.
31    pub fn is_final(&self) -> bool {
32        matches!(self, ReActStep::FinalAnswer(_))
33    }
34
35    /// Returns the primary text content of the step.
36    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/// Role of a participant in the conversation.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
46pub enum Role {
47    /// The system prompt that defines the agent's persona and instructions.
48    System,
49    /// A human user turn.
50    User,
51    /// An assistant (model) turn.
52    Assistant,
53    /// A tool result injected into the conversation.
54    Tool,
55}
56
57/// A single message in the conversation history.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct Message {
60    /// Role of the message author.
61    pub role: Role,
62    /// Text content of the message.
63    pub content: String,
64    /// Rough token count estimate (4 chars per token heuristic).
65    pub token_estimate: usize,
66}
67
68impl Message {
69    /// Creates a new message, estimating its token count.
70    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    /// Convenience constructor for system messages.
77    pub fn system(content: impl Into<String>) -> Self { Self::new(Role::System, content) }
78    /// Convenience constructor for user messages.
79    pub fn user(content: impl Into<String>) -> Self { Self::new(Role::User, content) }
80    /// Convenience constructor for assistant messages.
81    pub fn assistant(content: impl Into<String>) -> Self { Self::new(Role::Assistant, content) }
82}
83
84/// Agent configuration.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct AgentConfig {
87    /// Maximum number of ReAct iterations before giving up.
88    pub max_iterations: u32,
89    /// Maximum total tokens allowed in the conversation history.
90    pub context_token_limit: usize,
91    /// Optional wall-clock timeout in milliseconds.
92    pub timeout_ms: Option<u64>,
93    /// Model identifier string.
94    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/// The result of executing a tool.
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct ToolResult {
111    /// Name of the tool that produced this result.
112    pub tool_name: String,
113    /// The tool's textual output.
114    pub output: String,
115    /// Whether execution succeeded.
116    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}