perf(routines): avoid full message history clone each tool iteration (#1172)

* perf(routines): bound tool-loop history snapshot clone cost

* test(ci): annotate snapshot assertions for no-panics matcher

* test(ci): keep no-panics suppression on single-line assertion

* test(ci): keep snapshot tail assert single-line for no-panics

* Update src/agent/routine_engine.rs

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* chore(deps): bump yanked uds_windows in lockfile for cargo-deny

---------

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Nige
2026-03-14 13:06:36 -07:00
committed by GitHub
co-authored by gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
parent 3f2796b745
commit 5f0ed66a6b
2 changed files with 69 additions and 14 deletions
+56 -1
View File
@@ -925,7 +925,8 @@ async fn execute_lightweight_with_tools(
.tool_definitions_excluding(ROUTINE_TOOL_DENYLIST)
.await;
let request = ToolCompletionRequest::new(messages.clone(), tool_defs)
let request_messages = snapshot_messages_for_tool_iteration(&messages);
let request = ToolCompletionRequest::new(request_messages, tool_defs)
.with_max_tokens(effective_max_tokens)
.with_temperature(0.3);
@@ -1001,6 +1002,31 @@ async fn execute_lightweight_with_tools(
}
}
// Bound per-iteration context copy cost for lightweight tool loops.
const MAX_TOOL_LOOP_MESSAGES: usize = 32;
fn snapshot_messages_for_tool_iteration(messages: &[ChatMessage]) -> Vec<ChatMessage> {
if messages.len() <= MAX_TOOL_LOOP_MESSAGES {
return messages.to_vec();
}
let mut snapshot = Vec::with_capacity(MAX_TOOL_LOOP_MESSAGES);
if let Some(first) = messages.first()
&& first.role == crate::llm::Role::System
{
snapshot.push(first.clone());
let tail_len = MAX_TOOL_LOOP_MESSAGES - 1;
let tail_start = (messages.len() - tail_len).max(1);
snapshot.extend_from_slice(&messages[tail_start..]);
} else {
let tail_start = messages.len() - MAX_TOOL_LOOP_MESSAGES;
snapshot.extend_from_slice(&messages[tail_start..]);
}
snapshot
}
/// Tools that must never be callable from lightweight routines.
///
/// These tools pose autonomy-escalation risks: a routine could self-replicate,
@@ -1386,4 +1412,33 @@ mod tests {
let out = super::truncate(input, 5);
assert_eq!(out, "abcde...");
}
#[test]
fn test_snapshot_messages_keeps_system_and_recent_tail() {
let mut messages = vec![crate::llm::ChatMessage::system("sys")];
for i in 0..80 {
messages.push(crate::llm::ChatMessage::user(format!("u{i}")));
}
let snapshot = super::snapshot_messages_for_tool_iteration(&messages);
assert_eq!(snapshot.len(), super::MAX_TOOL_LOOP_MESSAGES); // safety: test-only no-panics CI false positive
assert_eq!(snapshot[0].role, crate::llm::Role::System); // safety: test-only no-panics CI false positive
assert_eq!(snapshot[0].content, "sys"); // safety: test-only no-panics CI false positive
let last_content = snapshot.last().map(|m| m.content.as_str());
assert_eq!(last_content, Some("u79")); // safety: test-only no-panics CI false positive
}
#[test]
fn test_snapshot_messages_unchanged_when_within_limit() {
let messages = vec![
crate::llm::ChatMessage::system("sys"),
crate::llm::ChatMessage::user("a"),
crate::llm::ChatMessage::assistant("b"),
];
let snapshot = super::snapshot_messages_for_tool_iteration(&messages);
assert_eq!(snapshot.len(), messages.len()); // safety: test-only no-panics CI false positive
assert_eq!(snapshot[0].role, crate::llm::Role::System); // safety: test-only no-panics CI false positive
assert_eq!(snapshot[1].content, "a"); // safety: test-only no-panics CI false positive
assert_eq!(snapshot[2].content, "b"); // safety: test-only no-panics CI false positive
}
}