diff --git a/src/agent/compactor.rs b/src/agent/compactor.rs index ea2856b45..d308fd5dd 100644 --- a/src/agent/compactor.rs +++ b/src/agent/compactor.rs @@ -172,7 +172,24 @@ impl Compactor { return Ok(()); } - let remove_count = total / 2; + let mut remove_count = total / 2; + + // Advance past User messages containing ToolResult blocks so we never + // orphan tool_results whose matching tool_use was in a removed assistant + // message (the Anthropic API rejects orphaned tool_result blocks). + while remove_count < total.saturating_sub(2) { + let has_tool_result = matches!( + history.get(remove_count), + Some(Message::User { content }) + if content.iter().any(|item| + matches!(item, UserContent::ToolResult(_))) + ); + if has_tool_result { + remove_count += 1; + } else { + break; + } + } let removed: Vec = history.drain(..remove_count).collect(); drop(removed); @@ -206,12 +223,29 @@ async fn run_compaction( let (removed_messages, remove_count) = { let mut hist = history.write().await; let total = hist.len(); - let remove_count = ((total as f32 * fraction) as usize) + let mut remove_count = ((total as f32 * fraction) as usize) .max(1) .min(total.saturating_sub(2)); if remove_count == 0 { return Ok(0); } + + // Advance past User messages containing ToolResult blocks so we never + // orphan tool_results whose matching tool_use was in a removed message. + while remove_count < total.saturating_sub(2) { + let has_tool_result = matches!( + hist.get(remove_count), + Some(Message::User { content }) + if content.iter().any(|item| + matches!(item, UserContent::ToolResult(_))) + ); + if has_tool_result { + remove_count += 1; + } else { + break; + } + } + let removed: Vec = hist.drain(..remove_count).collect(); (removed, remove_count) }; diff --git a/src/agent/worker.rs b/src/agent/worker.rs index bfaf89170..dccf358df 100644 --- a/src/agent/worker.rs +++ b/src/agent/worker.rs @@ -746,9 +746,29 @@ impl Worker { let estimated = estimate_history_tokens(history); let usage = estimated as f32 / context_window as f32; - let remove_count = ((total as f32 * fraction) as usize) + let mut remove_count = ((total as f32 * fraction) as usize) .max(1) .min(total.saturating_sub(2)); + + // Advance the boundary past User messages that contain ToolResult blocks. + // If we stop right after an Assistant message with ToolCalls, the next + // User message holds the corresponding ToolResults. Leaving those orphaned + // causes the Anthropic API to reject the request with: + // "unexpected tool_use_id found in tool_result blocks" + while remove_count < total.saturating_sub(2) { + let has_tool_result = matches!( + history.get(remove_count), + Some(rig::message::Message::User { content }) + if content.iter().any(|item| + matches!(item, rig::message::UserContent::ToolResult(_))) + ); + if has_tool_result { + remove_count += 1; + } else { + break; + } + } + let removed: Vec = history.drain(..remove_count).collect(); compacted_history.extend(removed.iter().cloned()); diff --git a/src/config/types.rs b/src/config/types.rs index 111f63238..9cce13984 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -842,7 +842,7 @@ impl Default for BrowserConfig { Self { enabled: true, headless: true, - evaluate_enabled: false, + evaluate_enabled: true, executable_path: None, screenshot_dir: None, persist_session: false, diff --git a/src/tools/browser.rs b/src/tools/browser.rs index e218131cc..378a4d818 100644 --- a/src/tools/browser.rs +++ b/src/tools/browser.rs @@ -2108,12 +2108,17 @@ impl Tool for BrowserCloseTool { task.abort(); } - if let Some(mut browser) = browser - && let Err(error) = browser.close().await - { - let message = format!("failed to close browser: {error}"); - tracing::warn!(policy = "close_browser", %message); - return Err(BrowserError::new(message)); + if let Some(mut browser) = browser { + if let Err(error) = browser.close().await { + // The browser connection may already be dead (e.g. "oneshot canceled" + // from a dropped WebSocket). That's fine — the browser is effectively + // closed. Log it but don't fail the tool call. + tracing::warn!( + policy = "close_browser", + %error, + "browser.close() failed, treating as already closed" + ); + } } if !persistent_profile && let Some(dir) = user_data_dir {