use crate::{AgentEvent, ProviderError, ToolMetadata};
use thiserror::Error;
#[derive(Debug, Clone, Copy)]
pub struct AgentLimits {
pub max_turns: usize,
pub max_tool_calls: usize,
pub permit_candidate_writes: bool,
pub context_byte_budget: usize,
}
impl Default for AgentLimits {
fn default() -> Self {
Self {
max_turns: 12,
max_tool_calls: 24,
permit_candidate_writes: false,
context_byte_budget: 256 * 1024,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct AgentOutput {
pub answer: String,
pub events: Vec<AgentEvent>,
pub used_bounded_sql_query: bool,
pub tool_metadata: Vec<ToolMetadata>,
pub usage: crate::TokenUsage,
}
#[derive(Debug, Error)]
pub enum AgentError {
#[error("{0}")]
Provider(#[from] ProviderError),
#[error("agent limit reached: {0}")]
Limit(&'static str),
#[error("provider returned an unsupported tool call")]
InvalidToolCall,
#[error("conversation history is invalid")]
InvalidHistory,
#[error("conversation context exceeds the safe limit")]
ContextLimit,
#[error("request cancelled")]
Cancelled,
}
pub(super) fn message_size(message: &crate::ChatMessage) -> usize {
message.content.len()
+ message.role.len()
+ message
.tool_calls
.iter()
.map(|call| {
call.id.len()
+ call.name.len()
+ serde_json::to_string(&call.arguments).map_or(0, |text| text.len())
})
.sum::<usize>()
}
pub(super) fn completion_summary(base: &str, truncated: bool) -> String {
if truncated {
format!("{base} (truncated)")
} else {
base.to_owned()
}
}
const TRUNCATED_MARKER: &str = "…[truncated: tool result exceeded the conversation byte budget]";
pub(super) fn trim_to_budget(messages: &mut Vec<crate::ChatMessage>, budget: usize) {
loop {
let total = messages.iter().map(message_size).sum::<usize>();
if total <= budget {
return;
}
let groups = tool_groups(messages);
if groups.len() >= 2 {
let oldest = groups[0].clone();
messages.drain(oldest);
continue;
}
let Some(idx) = messages
.iter()
.enumerate()
.filter(|(_, message)| message.role == "tool")
.max_by_key(|(_, message)| message.content.len())
.map(|(index, _)| index)
else {
return;
};
let content_len = messages[idx].content.len();
let overhead = total - content_len;
let target = budget.saturating_sub(overhead + TRUNCATED_MARKER.len());
if target >= content_len {
return;
}
let bound = super::tools::floor_boundary(&messages[idx].content, target);
messages[idx].content.truncate(bound);
messages[idx].content.push_str(TRUNCATED_MARKER);
}
}
fn tool_groups(messages: &[crate::ChatMessage]) -> Vec<std::ops::Range<usize>> {
let mut groups = Vec::new();
let mut i = 0;
while i < messages.len() {
if !messages[i].tool_calls.is_empty() {
let start = i;
i += 1;
while i < messages.len() && messages[i].role == "tool" {
i += 1;
}
groups.push(start..i);
} else {
i += 1;
}
}
groups
}