lellm_agent/runtime/
typed_state.rs1use lellm_core::{ChatResponse, Message};
14use lellm_graph::WorkflowState;
15use serde::{Deserialize, Serialize};
16
17use super::context::estimate_tokens;
18use super::event::StopReason;
19
20#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
27pub struct AgentState {
28 pub messages: Vec<Message>,
30 pub iterations: usize,
32 pub total_tool_calls: usize,
34 pub output_tokens: usize,
36 pub reasoning_tokens: usize,
38 pub compact_count: usize,
40 pub stop_reason: Option<StopReason>,
42 pub last_response: Option<ChatResponse>,
44}
45
46impl AgentState {
47 pub fn from_messages(messages: Vec<Message>) -> Self {
49 Self {
50 messages,
51 iterations: 0,
52 total_tool_calls: 0,
53 output_tokens: 0,
54 reasoning_tokens: 0,
55 compact_count: 0,
56 stop_reason: None,
57 last_response: None,
58 }
59 }
60
61 pub fn estimated_context_tokens(&self) -> usize {
64 estimate_tokens(&self.messages)
65 }
66
67 pub fn reached_max(&self, max_iterations: usize) -> bool {
69 self.iterations >= max_iterations
70 }
71
72 pub fn exceeded_output(&self, max: Option<u32>) -> bool {
74 match max {
75 Some(limit) => self.output_tokens >= limit as usize,
76 None => false,
77 }
78 }
79
80 pub fn exceeded_reasoning(&self, max: Option<u32>) -> bool {
82 match max {
83 Some(limit) => self.reasoning_tokens >= limit as usize,
84 None => false,
85 }
86 }
87
88 pub fn exceeded_output_with_extra(&self, max: Option<u32>, extra: usize) -> bool {
92 match max {
93 Some(limit) => self.output_tokens + extra >= limit as usize,
94 None => false,
95 }
96 }
97
98 pub fn exceeded_reasoning_with_extra(&self, max: Option<u32>, extra: usize) -> bool {
100 match max {
101 Some(limit) => self.reasoning_tokens + extra >= limit as usize,
102 None => false,
103 }
104 }
105
106 pub fn is_terminal(&self) -> bool {
108 self.stop_reason.is_some()
109 }
110
111 pub fn messages_ref(&self) -> &[Message] {
113 &self.messages
114 }
115}
116
117#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
124pub enum AgentMutation {
125 AppendMessage(Message),
127 AppendMessages(Vec<Message>),
129 IncrementIteration,
131 AddToolCalls(usize),
133 AddOutputTokens(usize),
135 AddReasoningTokens(usize),
137 IncrementCompactCount,
139 ReplaceMessages(Vec<Message>),
141 SetStopReason(StopReason),
143 SetLastResponse(ChatResponse),
145}
146
147impl lellm_graph::state::workflow_state::StateMutation<AgentState> for AgentMutation {
148 fn apply(self, state: &mut AgentState) {
149 match self {
150 AgentMutation::AppendMessage(msg) => {
151 state.messages.push(msg);
152 }
153 AgentMutation::AppendMessages(msgs) => {
154 state.messages.extend(msgs);
155 }
156 AgentMutation::IncrementIteration => {
157 state.iterations += 1;
158 }
159 AgentMutation::AddToolCalls(n) => {
160 state.total_tool_calls += n;
161 }
162 AgentMutation::AddOutputTokens(n) => {
163 state.output_tokens += n;
164 }
165 AgentMutation::AddReasoningTokens(n) => {
166 state.reasoning_tokens += n;
167 }
168 AgentMutation::IncrementCompactCount => {
169 state.compact_count += 1;
170 }
171 AgentMutation::ReplaceMessages(msgs) => {
172 state.messages = msgs;
173 }
174 AgentMutation::SetStopReason(reason) => {
175 state.stop_reason = Some(reason);
176 }
177 AgentMutation::SetLastResponse(response) => {
178 state.last_response = Some(response);
179 }
180 }
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct AgentCheckpoint {
193 pub messages: Vec<Message>,
195 pub iterations: usize,
197 pub total_tool_calls: usize,
199 pub output_tokens: usize,
201 pub reasoning_tokens: usize,
203 pub compact_count: usize,
205 pub stop_reason: Option<StopReason>,
207 }
209
210impl WorkflowState for AgentState {
213 type Checkpoint = AgentCheckpoint;
214 type Mutation = AgentMutation;
215
216 fn snapshot(&self) -> AgentCheckpoint {
217 AgentCheckpoint {
218 messages: self.messages.clone(),
219 iterations: self.iterations,
220 total_tool_calls: self.total_tool_calls,
221 output_tokens: self.output_tokens,
222 reasoning_tokens: self.reasoning_tokens,
223 compact_count: self.compact_count,
224 stop_reason: self.stop_reason.clone(),
225 }
226 }
227
228 fn restore(checkpoint: AgentCheckpoint) -> Self {
229 AgentState {
230 messages: checkpoint.messages,
231 iterations: checkpoint.iterations,
232 total_tool_calls: checkpoint.total_tool_calls,
233 output_tokens: checkpoint.output_tokens,
234 reasoning_tokens: checkpoint.reasoning_tokens,
235 compact_count: checkpoint.compact_count,
236 stop_reason: checkpoint.stop_reason,
237 last_response: None, }
239 }
240}
241
242#[derive(Clone)]
253pub struct AgentStateMerge;
254
255impl lellm_graph::MergeStrategy<AgentState> for AgentStateMerge {
256 fn merge(branches: Vec<AgentState>) -> Result<AgentState, lellm_graph::WorkflowError> {
257 let mut iter = branches.into_iter();
258 let mut merged = iter.next().ok_or_else(|| {
259 lellm_graph::WorkflowError::MergeConflict("no branches to merge".into())
260 })?;
261
262 for branch in iter {
263 merged.messages.extend(branch.messages);
264 merged.iterations = merged.iterations.max(branch.iterations);
265 merged.total_tool_calls = merged.total_tool_calls.max(branch.total_tool_calls);
266 merged.output_tokens += branch.output_tokens;
267 merged.reasoning_tokens += branch.reasoning_tokens;
268 merged.compact_count += branch.compact_count;
269 if merged.stop_reason.is_none() {
270 merged.stop_reason = branch.stop_reason;
271 }
272 if merged.last_response.is_none() {
273 merged.last_response = branch.last_response;
274 }
275 }
276
277 Ok(merged)
278 }
279
280 fn default_instance() -> Self {
281 AgentStateMerge
282 }
283}
284
285