use std::sync::Arc;
use async_trait::async_trait;
use lellm_core::ContentBlock;
use lellm_graph::{GraphError, LeafContext, LeafNode};
use super::super::config::{ToolUseConfig, empty_response};
use super::super::context::{ContextBudget, ContextCompactor, estimate_reasoning_block};
use super::super::event::StopReason;
use super::super::typed_state::{AgentMutation, AgentState};
fn estimate_round_reasoning_tokens(content: &[ContentBlock]) -> usize {
content
.iter()
.filter_map(|b| match b {
ContentBlock::Thinking(th) => Some(estimate_reasoning_block(th)),
_ => None,
})
.sum()
}
#[derive(Debug, Clone)]
pub struct StopConfig {
pub max_iterations: usize,
pub max_reasoning_tokens: Option<u32>,
pub max_total_output_tokens: Option<u32>,
pub max_total_reasoning_tokens: Option<u32>,
}
impl StopConfig {
pub fn from_tool_use_config(config: &ToolUseConfig) -> Self {
Self {
max_iterations: config.max_iterations,
max_reasoning_tokens: config.request_options.max_reasoning_tokens,
max_total_output_tokens: config.max_total_output_tokens,
max_total_reasoning_tokens: config.max_total_reasoning_tokens,
}
}
}
pub struct PostLLMGuard {
pub name: String,
pub stop_config: StopConfig,
}
impl PostLLMGuard {
pub fn new(name: impl Into<String>, stop_config: StopConfig) -> Self {
Self {
name: name.into(),
stop_config,
}
}
}
#[async_trait]
impl LeafNode<AgentState> for PostLLMGuard {
async fn execute(&self, ctx: &mut LeafContext<'_, AgentState>) -> Result<(), GraphError> {
let state = ctx.state().clone();
if state.stop_reason.is_some() {
ctx.end();
return Ok(());
}
if state.reached_max(self.stop_config.max_iterations) {
ctx.record(AgentMutation::SetStopReason(
StopReason::MaxIterationsReached,
));
ctx.end();
return Ok(());
}
let last_response = state.last_response.clone().unwrap_or_else(empty_response);
let mut stopped = false;
if let Some(limit) = self.stop_config.max_reasoning_tokens {
let round_reasoning = estimate_round_reasoning_tokens(&last_response.content);
if round_reasoning > limit as usize {
tracing::warn!(
round_reasoning,
max_reasoning_tokens = limit,
"single-round reasoning budget exceeded"
);
ctx.record(AgentMutation::SetStopReason(
StopReason::ReasoningBudgetExceeded,
));
stopped = true;
}
}
if !stopped && state.exceeded_output(self.stop_config.max_total_output_tokens) {
ctx.record(AgentMutation::SetStopReason(
StopReason::OutputBudgetExceeded,
));
stopped = true;
}
if !stopped && state.exceeded_reasoning(self.stop_config.max_total_reasoning_tokens) {
ctx.record(AgentMutation::SetStopReason(
StopReason::ReasoningBudgetExceeded,
));
}
if stopped {
ctx.end();
return Ok(());
}
if last_response.has_tool_calls() {
ctx.goto("tool");
return Ok(());
}
ctx.record(AgentMutation::SetStopReason(StopReason::Complete));
ctx.end();
Ok(())
}
}
#[derive(Clone)]
pub struct CompactorNode {
pub name: String,
pub compactor: Arc<dyn ContextCompactor>,
pub budget: ContextBudget,
}
impl CompactorNode {
pub fn new(
name: impl Into<String>,
compactor: Arc<dyn ContextCompactor>,
budget: ContextBudget,
) -> Self {
Self {
name: name.into(),
compactor,
budget,
}
}
}
#[async_trait]
impl LeafNode<AgentState> for CompactorNode {
async fn execute(&self, ctx: &mut LeafContext<'_, AgentState>) -> Result<(), GraphError> {
let state = ctx.state();
if !self.budget.should_compact(state.estimated_context_tokens()) {
return Ok(());
}
let result = self.compactor.compact(&state.messages, &self.budget);
if result.removed_messages > 0 {
ctx.record(AgentMutation::ReplaceMessages(result.messages));
ctx.record(AgentMutation::IncrementCompactCount);
tracing::debug!(
agent = %self.name,
before_tokens = result.before_tokens,
after_tokens = result.after_tokens,
removed = result.removed_messages,
"context compacted"
);
}
Ok(())
}
}
pub struct BudgetCondition {
pub name: String,
pub budget: ContextBudget,
}
impl BudgetCondition {
pub fn new(name: impl Into<String>, budget: ContextBudget) -> Self {
Self {
name: name.into(),
budget,
}
}
}
#[async_trait]
impl LeafNode<AgentState> for BudgetCondition {
async fn execute(&self, ctx: &mut LeafContext<'_, AgentState>) -> Result<(), GraphError> {
let state = ctx.state();
if self.budget.should_compact(state.estimated_context_tokens()) {
ctx.goto("compactor");
} else {
ctx.goto("llm");
}
Ok(())
}
}