Finish long-running agent parity

This commit is contained in:
Georg Bauer
2026-07-26 13:41:53 +02:00
parent 65c9cbfc45
commit 171b041ba6
15 changed files with 896 additions and 324 deletions

View File

@@ -2,6 +2,7 @@ use crate::engine::ChatTurn;
pub(crate) const SUMMARY_MAX_TOKENS: i32 = 4096;
pub(crate) const TOOL_RESULT_RESERVE_TOKENS: u32 = 1024;
const MIN_SUMMARY_TOKENS: u32 = 256;
const SOFT_PERCENT: u32 = 85;
const MIN_FREE_TOKENS: u32 = 8192;
@@ -21,24 +22,23 @@ pub(crate) fn should_compact(used: u32, limit: u32) -> bool {
|| limit.saturating_sub(used) <= MIN_FREE_TOKENS.min(limit / 8)
}
pub(crate) fn tool_result_needs_compaction(used: u32, limit: u32, result: &str) -> bool {
should_compact(used, limit)
|| used
.saturating_add(result.len().min(u32::MAX as usize) as u32)
.saturating_add(TOOL_RESULT_RESERVE_TOKENS)
>= limit
pub(crate) fn tool_result_reserve(limit: u32) -> u32 {
TOOL_RESULT_RESERVE_TOKENS.min((limit / 8).max(16))
}
pub(crate) fn bounded_tool_result(used: u32, limit: u32, result: String) -> String {
if used
.saturating_add(result.len().min(u32::MAX as usize) as u32)
.saturating_add(TOOL_RESULT_RESERVE_TOKENS)
< limit
{
result
} else {
"Tool error: the result is too large for the remaining context after compaction. Retry with a smaller read/search/bash output.\n".into()
}
pub(crate) fn tool_result_fits(projected: u32, limit: u32, reserve: u32) -> bool {
limit > 0 && projected.saturating_add(reserve) < limit
}
pub(crate) fn bounded_tool_error(projected: u32, limit: u32, reserve: u32) -> String {
format!(
"Tool error: tool result still does not fit after context compaction (projected_prompt={projected} tokens, ctx={limit}, reserve={reserve}). Retry with a smaller read/search/bash output.\n"
)
}
pub(crate) fn summary_budget(prompt: u32, limit: u32) -> Option<i32> {
let room = limit.saturating_sub(prompt).saturating_sub(1);
(room >= MIN_SUMMARY_TOKENS).then_some(room.min(SUMMARY_MAX_TOKENS as u32) as i32)
}
pub(crate) fn tail_budget(context: u32) -> u32 {
@@ -132,11 +132,30 @@ mod tests {
}
#[test]
fn oversized_tool_result_becomes_a_bounded_retry_error() {
let result = "x".repeat(4_000);
assert!(tool_result_needs_compaction(6_000, 10_000, &result));
let error = bounded_tool_result(4_000, 5_000, result);
fn a_new_summary_replaces_the_previous_rebuild_summary() {
let current = summary_system_prompt("tools", Some("old state"));
assert!(current.contains("old state"));
let rebuilt = summary_system_prompt("tools", Some("new state"));
assert!(rebuilt.contains("new state"));
assert!(!rebuilt.contains("old state"));
}
#[test]
fn hard_trigger_and_retry_reserve_match_the_reference() {
assert_eq!(tool_result_reserve(4096), 512);
assert_eq!(tool_result_reserve(65_536), 1024);
assert!(tool_result_fits(3000, 4096, 512));
assert!(!tool_result_fits(3584, 4096, 512));
let error = bounded_tool_error(5000, 4096, 512);
assert!(error.starts_with("Tool error:"));
assert!(error.len() < 160);
assert!(error.contains("projected_prompt=5000 tokens"));
assert!(error.len() < 256);
}
#[test]
fn summary_budget_keeps_answer_room_and_rejects_exhausted_contexts() {
assert_eq!(summary_budget(1000, 8192), Some(SUMMARY_MAX_TOKENS));
assert_eq!(summary_budget(7900, 8192), Some(291));
assert_eq!(summary_budget(7936, 8192), None);
}
}