use std::collections::HashMap;
use lellm_core::{ChatResponse, Message};
use serde_json::Value;
use super::ToolUseResult;
use super::context::{estimate_reasoning_block, estimate_text, estimate_tokens};
use super::event::StopReason;
pub const SK_ITERATIONS: &str = "iterations";
pub const SK_TOOL_CALLS: &str = "tool_calls";
pub const SK_STOP_REASON: &str = "stop_reason";
pub const SK_OUTPUT_TOKENS: &str = "output_tokens";
pub const SK_REASONING_TOKENS: &str = "reasoning_tokens";
pub fn agent_key(name: &str, suffix: &str) -> String {
format!("{name}/{suffix}")
}
pub fn get_usize(state: &HashMap<String, Value>, key: &str) -> usize {
state
.get(key)
.and_then(|v| v.as_u64())
.map(|v| v as usize)
.unwrap_or_default()
}
pub fn set_usize(state: &mut HashMap<String, Value>, key: &str, value: usize) {
state.insert(key.to_string(), Value::from(value as u64));
}
pub fn get_messages(state: &HashMap<String, Value>, key: &str) -> Vec<Message> {
if let Some(value) = state.get(key) {
if let Some(arr) = value.as_array() {
let mut messages = Vec::new();
for v in arr {
if let Ok(msg) = serde_json::from_value::<Message>(v.clone()) {
messages.push(msg);
}
}
return messages;
}
if let Ok(msg) = serde_json::from_value::<Message>(value.clone()) {
return vec![msg];
}
}
Vec::new()
}
pub fn set_messages(state: &mut HashMap<String, Value>, key: &str, messages: &[Message]) {
let json: Vec<Value> = messages
.iter()
.filter_map(|m| serde_json::to_value(m).ok())
.collect();
state.insert(key.to_string(), Value::Array(json));
}
pub fn get_stop_reason(state: &HashMap<String, Value>, key: &str) -> Option<StopReason> {
state
.get(key)
.and_then(|v| v.as_str())
.and_then(|s| match s {
"Complete" => Some(StopReason::Complete),
"MaxIterationsReached" => Some(StopReason::MaxIterationsReached),
"Cancelled" => Some(StopReason::Cancelled),
"OutputBudgetExceeded" => Some(StopReason::OutputBudgetExceeded),
"ReasoningBudgetExceeded" => Some(StopReason::ReasoningBudgetExceeded),
_ => None,
})
}
pub fn set_stop_reason(state: &mut HashMap<String, Value>, key: &str, reason: &StopReason) {
state.insert(key.to_string(), Value::String(format!("{:?}", reason)));
}
pub struct AgentState<'a> {
state: &'a mut HashMap<String, Value>,
agent_name: String,
message_key: String,
}
impl<'a> AgentState<'a> {
pub fn new(state: &'a mut HashMap<String, Value>, agent_name: &str, message_key: &str) -> Self {
Self {
state,
agent_name: agent_name.to_string(),
message_key: message_key.to_string(),
}
}
fn key(&self, suffix: &str) -> String {
agent_key(&self.agent_name, suffix)
}
pub fn message_key(&self) -> &str {
&self.message_key
}
pub fn messages(&self) -> Vec<Message> {
get_messages(self.state, &self.message_key)
}
pub fn set_messages(&mut self, messages: Vec<Message>) {
set_messages(self.state, &self.message_key, &messages);
}
pub fn push_assistant(&mut self, content: Vec<lellm_core::ContentBlock>) {
let msg = Message::Assistant {
content: content.clone(),
};
let tokens = estimate_tokens(&[msg.clone()]);
let est_key = self.key("estimated_tokens");
let current: usize = get_usize(self.state, &est_key);
set_usize(self.state, &est_key, current + tokens);
let mut msgs = self.messages();
msgs.push(msg);
set_messages(self.state, &self.message_key, &msgs);
}
pub fn push_tool_results(&mut self, results: Vec<Message>) {
let tokens = estimate_tokens(&results);
let est_key = self.key("estimated_tokens");
let current: usize = get_usize(self.state, &est_key);
set_usize(self.state, &est_key, current + tokens);
let mut msgs = self.messages();
msgs.extend(results);
set_messages(self.state, &self.message_key, &msgs);
}
pub fn next_iteration(&mut self) {
let key = self.key(SK_ITERATIONS);
let current: usize = get_usize(self.state, &key);
set_usize(self.state, &key, current + 1);
}
pub fn iterations(&self) -> usize {
get_usize(self.state, &self.key(SK_ITERATIONS))
}
pub fn reached_max(&self, max: usize) -> bool {
self.iterations() >= max
}
pub fn add_tool_calls(&mut self, count: usize) {
let key = self.key(SK_TOOL_CALLS);
let current: usize = get_usize(self.state, &key);
set_usize(self.state, &key, current + count);
}
pub fn tool_calls_executed(&self) -> usize {
get_usize(self.state, &self.key(SK_TOOL_CALLS))
}
pub fn add_output_tokens(&mut self, tokens: usize) {
let key = self.key(SK_OUTPUT_TOKENS);
let current: usize = get_usize(self.state, &key);
set_usize(self.state, &key, current + tokens);
}
pub fn add_reasoning_tokens(&mut self, tokens: usize) {
let key = self.key(SK_REASONING_TOKENS);
let current: usize = get_usize(self.state, &key);
set_usize(self.state, &key, current + tokens);
}
pub fn total_output_tokens(&self) -> usize {
get_usize(self.state, &self.key(SK_OUTPUT_TOKENS))
}
pub fn total_reasoning_tokens(&self) -> usize {
get_usize(self.state, &self.key(SK_REASONING_TOKENS))
}
pub fn add_output_from_content(&mut self, content: &[lellm_core::ContentBlock]) {
let mut output_tokens: usize = 0;
let mut reasoning_tokens: usize = 0;
for b in content {
match b {
lellm_core::ContentBlock::Text(t) => output_tokens += estimate_text(&t.text),
lellm_core::ContentBlock::Thinking(th) => {
reasoning_tokens += estimate_reasoning_block(th)
}
lellm_core::ContentBlock::Image { .. } | lellm_core::ContentBlock::ToolCall(_) => {}
}
}
self.add_output_tokens(output_tokens);
self.add_reasoning_tokens(reasoning_tokens);
}
pub fn exceeded_total_output(&self, max: Option<u32>) -> bool {
match max {
Some(limit) => self.total_output_tokens() >= limit as usize,
None => false,
}
}
pub fn exceeded_total_reasoning(&self, max: Option<u32>) -> bool {
match max {
Some(limit) => self.total_reasoning_tokens() >= limit as usize,
None => false,
}
}
pub fn init_messages(&mut self, messages: Vec<Message>) {
let tokens = estimate_tokens(&messages);
set_messages(self.state, &self.message_key, &messages);
set_usize(self.state, &self.key("estimated_tokens"), tokens);
set_usize(self.state, &self.key(SK_ITERATIONS), 0);
set_usize(self.state, &self.key(SK_TOOL_CALLS), 0);
set_usize(self.state, &self.key(SK_OUTPUT_TOKENS), 0);
set_usize(self.state, &self.key(SK_REASONING_TOKENS), 0);
}
pub fn finish(&self, stop_reason: StopReason, response: ChatResponse) -> ToolUseResult {
ToolUseResult {
stop_reason,
response,
messages: self.messages(),
iterations: self.iterations(),
tool_calls_executed: self.tool_calls_executed(),
}
}
pub fn finish_complete(&self, response: ChatResponse) -> ToolUseResult {
self.finish(StopReason::Complete, response)
}
pub fn finish_max_iterations(&self, response: ChatResponse) -> ToolUseResult {
self.finish(StopReason::MaxIterationsReached, response)
}
pub fn finish_cancelled(&self, response: ChatResponse) -> ToolUseResult {
self.finish(StopReason::Cancelled, response)
}
pub fn finish_output_budget(&self, response: ChatResponse) -> ToolUseResult {
self.finish(StopReason::OutputBudgetExceeded, response)
}
pub fn finish_reasoning_budget(&self, response: ChatResponse) -> ToolUseResult {
self.finish(StopReason::ReasoningBudgetExceeded, response)
}
pub fn set_stop_reason(&mut self, reason: &StopReason) {
set_stop_reason(self.state, &self.key(SK_STOP_REASON), reason);
}
pub fn inner(&self) -> &HashMap<String, Value> {
self.state
}
pub fn inner_mut(&mut self) -> &mut HashMap<String, Value> {
self.state
}
}
pub fn build_result_from_state(
state: &HashMap<String, Value>,
agent_name: &str,
message_key: &str,
stop_reason: StopReason,
response: ChatResponse,
) -> ToolUseResult {
let messages = get_messages(state, message_key);
let iterations = get_usize(state, &agent_key(agent_name, SK_ITERATIONS));
let tool_calls_executed = get_usize(state, &agent_key(agent_name, SK_TOOL_CALLS));
ToolUseResult {
stop_reason,
response,
messages,
iterations,
tool_calls_executed,
}
}
pub fn initial_state(messages: Vec<Message>, message_key: &str) -> lellm_graph::State {
let mut state = lellm_graph::State::new();
set_messages(&mut state, message_key, &messages);
set_usize(
&mut state,
&agent_key("agent", "estimated_tokens"),
estimate_tokens(&messages),
);
set_usize(&mut state, &agent_key("agent", SK_ITERATIONS), 0);
set_usize(&mut state, &agent_key("agent", SK_TOOL_CALLS), 0);
set_usize(&mut state, &agent_key("agent", SK_OUTPUT_TOKENS), 0);
set_usize(&mut state, &agent_key("agent", SK_REASONING_TOKENS), 0);
state
}