use crabllm_core::{Message, Role, ToolCall};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct HistoryEntry {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub agent: String,
#[serde(skip)]
pub sender: String,
#[serde(skip)]
pub auto_injected: bool,
pub message: Message,
}
impl HistoryEntry {
pub fn system(content: impl Into<String>) -> Self {
Self::from_message(Message::system(content))
}
pub fn user(content: impl Into<String>) -> Self {
Self::from_message(Message::user(content))
}
pub fn user_with_sender(content: impl Into<String>, sender: impl Into<String>) -> Self {
let mut entry = Self::user(content);
entry.sender = sender.into();
entry
}
pub fn assistant(
content: impl Into<String>,
reasoning: Option<String>,
tool_calls: Option<&[ToolCall]>,
) -> Self {
let content: String = content.into();
let has_tool_calls = tool_calls.is_some_and(|tcs| !tcs.is_empty());
let message_content = if content.is_empty() && has_tool_calls {
Some(serde_json::Value::Null)
} else {
Some(serde_json::Value::String(content))
};
Self::from_message(Message {
role: Role::Assistant,
content: message_content,
tool_calls: tool_calls.map(|tcs| tcs.to_vec()),
tool_call_id: None,
name: None,
reasoning_content: reasoning.filter(|s| !s.is_empty()),
extra: Default::default(),
})
}
pub fn tool(
content: impl Into<String>,
call_id: impl Into<String>,
name: impl Into<String>,
) -> Self {
Self::from_message(Message::tool(call_id, name, content))
}
pub fn from_message(message: Message) -> Self {
Self {
agent: String::new(),
sender: String::new(),
auto_injected: false,
message,
}
}
pub fn auto_injected(mut self) -> Self {
self.auto_injected = true;
self
}
pub fn role(&self) -> &Role {
&self.message.role
}
pub fn text(&self) -> &str {
self.message.content_str().unwrap_or("")
}
pub fn reasoning(&self) -> &str {
self.message.reasoning_content.as_deref().unwrap_or("")
}
pub fn tool_calls(&self) -> &[ToolCall] {
self.message.tool_calls.as_deref().unwrap_or(&[])
}
pub fn tool_call_id(&self) -> &str {
self.message.tool_call_id.as_deref().unwrap_or("")
}
pub fn estimate_tokens(&self) -> usize {
let chars = self.text().len()
+ self.reasoning().len()
+ self.tool_call_id().len()
+ self
.tool_calls()
.iter()
.map(|tc| tc.function.name.len() + tc.function.arguments.len())
.sum::<usize>();
(chars / 4).max(1)
}
pub fn to_wire_message(&self) -> Message {
if self.message.role != Role::Assistant || self.agent.is_empty() {
return self.message.clone();
}
let tagged = format!("<from agent=\"{}\">\n{}\n</from>", self.agent, self.text());
Message {
role: Role::Assistant,
content: Some(serde_json::Value::String(tagged)),
tool_calls: self.message.tool_calls.clone(),
tool_call_id: self.message.tool_call_id.clone(),
name: self.message.name.clone(),
reasoning_content: self.message.reasoning_content.clone(),
extra: self.message.extra.clone(),
}
}
}
pub fn estimate_tokens(entries: &[HistoryEntry]) -> usize {
entries.iter().map(|e| e.estimate_tokens()).sum()
}