Skip to content

Commit ff10d2b

Browse files
authored
feat: add token budget circuit breaker and session log to agent loop (#245)
**Key Changes:** - Introduced token budget circuit breaker to halt agent loop when cumulative input/output token limits are exceeded - Added append-only JSONL session log for crash recovery and debugging - Enhanced context compaction with proactive and reactive trimming and semantic recap - Exposed new configuration options for budget, logging, and prompt cache through environment variables **Added:** - Token budget enforcement (`BudgetConfig`) that ends loops with `BudgetExceeded` reason when input, output, or total token budgets are reached; configurable via env vars - Append-only JSONL session log (`SessionLog` and `SessionLogConfig`) that records all conversation events, tool results, usage stats, and outcomes for each agent loop in a crash-tolerant manner - Proactive context compaction logic, including semantic recap of trimmed messages, with configurability for compaction cadence and thresholds - New public exports for session logging and budget configs in agent loop modules and library root **Changed:** - Agent loop runner now checks token budgets each iteration and exits early if limits are exceeded - Context compaction upgraded to support both proactive (threshold-based) and reactive (hard ceiling) trimming, with detailed logging and semantic recaps of dropped content - Anthropic provider now attaches prompt-cache breakpoints to system/tool blocks when enabled, improving cache hit rates and efficiency - Configuration loading for agent loop now layers environment variable overrides on top of defaults for agent, context, budget, and session log settings - All agent loop terminal paths (success, error, budget, assistance) now consistently log outcomes to the session log if enabled **Removed:** - Previous static context trimming that lacked proactive compaction and semantic recaps - Unused or redundant context compaction code paths in favor of unified `maybe_compact` and semantic recap mechanism
1 parent 53ca3c9 commit ff10d2b

19 files changed

Lines changed: 1702 additions & 157 deletions

File tree

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎ares-cli/src/orchestrator/blue/callbacks.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,9 @@ impl BlueCallbackHandler {
145145
format!("Sub-agent hit max steps ({} steps)", outcome.steps)
146146
}
147147
ares_llm::LoopEndReason::MaxTokens => "Sub-agent hit max tokens".to_string(),
148+
ares_llm::LoopEndReason::BudgetExceeded { reason } => {
149+
format!("Sub-agent budget exceeded: {reason}")
150+
}
148151
ares_llm::LoopEndReason::Error(e) => format!("Sub-agent error: {e}"),
149152
};
150153

‎ares-cli/src/orchestrator/blue/investigation.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,9 @@ fn process_outcome(outcome: &AgentLoopOutcome, investigation_id: &str) -> Invest
425425
LoopEndReason::MaxTokens => InvestigationOutcome::Failed {
426426
error: format!("Investigation {investigation_id} hit max tokens"),
427427
},
428+
LoopEndReason::BudgetExceeded { reason } => InvestigationOutcome::Failed {
429+
error: format!("Investigation {investigation_id} budget exceeded: {reason}"),
430+
},
428431
LoopEndReason::Error(err) => InvestigationOutcome::Failed { error: err.clone() },
429432
}
430433
}

‎ares-cli/src/orchestrator/dispatcher/submission.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,28 @@ impl Dispatcher {
399399
agent_name: Some(tt.clone()),
400400
}
401401
}
402+
LoopEndReason::BudgetExceeded { reason } => {
403+
let mut result_json = json!({
404+
"steps": outcome.steps,
405+
"tool_calls": outcome.tool_calls_dispatched,
406+
});
407+
if let Some(disc) = merged_discoveries {
408+
result_json["discoveries"] = disc;
409+
}
410+
if !tool_outputs_json.is_empty() {
411+
result_json["tool_outputs"] =
412+
Value::Array(tool_outputs_json.clone());
413+
}
414+
TaskResult {
415+
task_id: tid.clone(),
416+
success: false,
417+
result: Some(result_json),
418+
error: Some(format!("Budget exceeded: {reason}")),
419+
completed_at: Some(Utc::now()),
420+
worker_pod: Some("rust-llm-runner".into()),
421+
agent_name: Some(tt.clone()),
422+
}
423+
}
402424
LoopEndReason::Error(err) => TaskResult {
403425
task_id: tid.clone(),
404426
success: false,

‎ares-cli/src/orchestrator/llm_runner.rs‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,10 @@ impl LlmTaskRunner {
4545
temperature: Option<f32>,
4646
technique_priorities: Vec<(String, i32)>,
4747
) -> Self {
48-
let config = AgentLoopConfig {
49-
model: model_name,
50-
temperature,
51-
..AgentLoopConfig::default()
52-
};
48+
// Layer env-var overrides (ARES_AGENT_*, ARES_CONTEXT_*, ARES_BUDGET_*,
49+
// ARES_SESSION_LOG_*) on top of compiled defaults so operators can
50+
// tune the loop without a code change.
51+
let config = AgentLoopConfig::from_env(model_name, temperature);
5352
Self {
5453
provider,
5554
dispatcher,
@@ -287,6 +286,13 @@ fn log_outcome(task_id: &str, outcome: &AgentLoopOutcome) {
287286
"LLM agent hit max tokens"
288287
);
289288
}
289+
LoopEndReason::BudgetExceeded { reason } => {
290+
warn!(
291+
task_id = task_id,
292+
steps = outcome.steps,
293+
"LLM agent budget circuit breaker tripped: {reason}"
294+
);
295+
}
290296
LoopEndReason::Error(err) => {
291297
warn!(
292298
task_id = task_id,

‎ares-cli/src/worker/blue_task_loop.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,12 @@ async fn execute_blue_task(
282282
"Hit max tokens".into(),
283283
agent_name,
284284
),
285+
LoopEndReason::BudgetExceeded { reason } => BlueTaskResult::failure(
286+
&task.task_id,
287+
&task.investigation_id,
288+
format!("Budget exceeded: {reason}"),
289+
agent_name,
290+
),
285291
LoopEndReason::Error(err) => {
286292
error!(task_id = %task.task_id, err = %err, "Blue task error");
287293
BlueTaskResult::failure(

‎ares-llm/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ blue = []
2727
tokio = { workspace = true }
2828
serde_json = { workspace = true }
2929
async-trait = "0.1"
30+
tempfile = "3"

‎ares-llm/examples/smoke_test.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ async fn main() -> Result<()> {
164164
retry: RetryConfig::default(),
165165
context: ContextConfig::default(),
166166
max_tool_calls_per_name: 10,
167+
..AgentLoopConfig::default()
167168
};
168169

169170
let outcome = run_agent_loop(

0 commit comments

Comments
 (0)