Skip to main content

tiy_core/agent/
state.rs

1//! Agent state management.
2
3use crate::agent::{AgentMessage, AgentTool};
4use crate::thinking::ThinkingLevel;
5use crate::types::Model;
6use parking_lot::RwLock;
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
10
11/// Agent state — pure runtime state without configuration.
12///
13/// Configuration values (model, thinking_level) live in [`AgentConfig`]
14/// (the single source of truth). This struct only holds conversational
15/// and streaming runtime state.
16#[derive(Debug)]
17pub struct AgentState {
18    /// System prompt.
19    pub system_prompt: RwLock<String>,
20    /// Available tools.
21    pub tools: RwLock<Vec<AgentTool>>,
22    /// Conversation messages.
23    pub messages: RwLock<Vec<AgentMessage>>,
24    /// Whether currently streaming.
25    pub is_streaming: AtomicBool,
26    /// Current streaming message.
27    pub stream_message: RwLock<Option<AgentMessage>>,
28    /// Pending tool call IDs.
29    pub pending_tool_calls: RwLock<HashSet<String>>,
30    /// Last error.
31    pub error: RwLock<Option<String>>,
32    /// Maximum number of messages in conversation history.
33    /// 0 = unlimited. When exceeded, oldest messages are drained.
34    pub max_messages: AtomicUsize,
35}
36
37impl AgentState {
38    /// Create a new agent state with default values.
39    pub fn new() -> Self {
40        Self {
41            system_prompt: RwLock::new(String::new()),
42            tools: RwLock::new(Vec::new()),
43            messages: RwLock::new(Vec::new()),
44            is_streaming: AtomicBool::new(false),
45            stream_message: RwLock::new(None),
46            pending_tool_calls: RwLock::new(HashSet::new()),
47            error: RwLock::new(None),
48            max_messages: AtomicUsize::new(0), // 0 = unlimited
49        }
50    }
51
52    /// Set the system prompt.
53    pub fn set_system_prompt(&self, prompt: impl Into<String>) {
54        *self.system_prompt.write() = prompt.into();
55    }
56
57    /// Set the tools.
58    pub fn set_tools(&self, tools: Vec<AgentTool>) {
59        *self.tools.write() = tools;
60    }
61
62    /// Add a message, enforcing the max_messages limit.
63    /// When the limit is exceeded, oldest messages are drained (FIFO).
64    pub fn add_message(&self, message: AgentMessage) {
65        let mut msgs = self.messages.write();
66        msgs.push(message);
67        let max = self.max_messages.load(Ordering::SeqCst);
68        if max > 0 && msgs.len() > max {
69            let excess = msgs.len() - max;
70            msgs.drain(..excess);
71        }
72    }
73
74    /// Set the maximum number of messages in conversation history.
75    /// 0 = unlimited.
76    pub fn set_max_messages(&self, max: usize) {
77        self.max_messages.store(max, Ordering::SeqCst);
78        // Immediately enforce if there are already too many messages
79        if max > 0 {
80            let mut msgs = self.messages.write();
81            if msgs.len() > max {
82                let excess = msgs.len() - max;
83                msgs.drain(..excess);
84            }
85        }
86    }
87
88    /// Get the current max_messages limit (0 = unlimited).
89    pub fn get_max_messages(&self) -> usize {
90        self.max_messages.load(Ordering::SeqCst)
91    }
92
93    /// Replace all messages.
94    pub fn replace_messages(&self, messages: Vec<AgentMessage>) {
95        *self.messages.write() = messages;
96    }
97
98    /// Clear all messages.
99    pub fn clear_messages(&self) {
100        self.messages.write().clear();
101    }
102
103    /// Reset the runtime state (messages, streaming, errors).
104    ///
105    /// Does NOT reset model or thinking_level (those live in `AgentConfig`).
106    pub fn reset(&self) {
107        *self.system_prompt.write() = String::new();
108        *self.tools.write() = Vec::new();
109        self.messages.write().clear();
110        self.is_streaming.store(false, Ordering::SeqCst);
111        *self.stream_message.write() = None;
112        self.pending_tool_calls.write().clear();
113        *self.error.write() = None;
114    }
115
116    /// Check if currently streaming.
117    pub fn is_streaming(&self) -> bool {
118        self.is_streaming.load(Ordering::SeqCst)
119    }
120
121    /// Set streaming state.
122    pub fn set_streaming(&self, value: bool) {
123        self.is_streaming.store(value, Ordering::SeqCst);
124    }
125
126    /// Get message count.
127    pub fn message_count(&self) -> usize {
128        self.messages.read().len()
129    }
130}
131
132impl Default for AgentState {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138/// NOTE: This `Clone` implementation acquires each lock independently,
139/// so the resulting clone is NOT a single atomic snapshot.
140/// For a consistent point-in-time snapshot, use [`Agent::snapshot()`].
141impl Clone for AgentState {
142    fn clone(&self) -> Self {
143        Self {
144            system_prompt: RwLock::new(self.system_prompt.read().clone()),
145            tools: RwLock::new(self.tools.read().clone()),
146            messages: RwLock::new(self.messages.read().clone()),
147            is_streaming: AtomicBool::new(self.is_streaming.load(Ordering::SeqCst)),
148            stream_message: RwLock::new(self.stream_message.read().clone()),
149            pending_tool_calls: RwLock::new(self.pending_tool_calls.read().clone()),
150            error: RwLock::new(self.error.read().clone()),
151            max_messages: AtomicUsize::new(self.max_messages.load(Ordering::SeqCst)),
152        }
153    }
154}
155
156// ============================================================================
157// AgentStateSnapshot — consistent point-in-time view
158// ============================================================================
159
160/// A consistent, lock-free snapshot of the agent's full state.
161///
162/// Includes both runtime state (from [`AgentState`]) and configuration
163/// (model, thinking_level from [`AgentConfig`]).
164///
165/// Obtain via [`Agent::snapshot()`].
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct AgentStateSnapshot {
168    /// System prompt.
169    pub system_prompt: String,
170    /// Current model (from AgentConfig).
171    pub model: Model,
172    /// Thinking level (from AgentConfig).
173    pub thinking_level: ThinkingLevel,
174    /// Conversation messages.
175    pub messages: Vec<AgentMessage>,
176    /// Whether currently streaming.
177    pub is_streaming: bool,
178    /// Current streaming message.
179    pub stream_message: Option<AgentMessage>,
180    /// Pending tool call IDs.
181    pub pending_tool_calls: HashSet<String>,
182    /// Last error.
183    pub error: Option<String>,
184    /// Message count.
185    pub message_count: usize,
186    /// Max messages limit (0 = unlimited).
187    pub max_messages: usize,
188}