diff --git a/src/crates/assembly/core/src/agentic/execution/round_executor.rs b/src/crates/assembly/core/src/agentic/execution/round_executor.rs index c06191057..5b5b56cb4 100644 --- a/src/crates/assembly/core/src/agentic/execution/round_executor.rs +++ b/src/crates/assembly/core/src/agentic/execution/round_executor.rs @@ -5,6 +5,9 @@ use super::model_exchange_trace::prepare_model_exchange_trace; use super::stream_processor::{StreamProcessOptions, StreamProcessor, StreamResult}; use super::types::{FinishReason, RoundContext, RoundResult}; +use super::write_content_sanitizer::{ + contains_tool_invocation_artifacts, strip_tool_invocation_artifacts, +}; use crate::agentic::core::{Message, ToolCall}; use crate::agentic::events::{ AgenticEvent, EventPriority, EventQueue, ModelRoundAttemptDiagnostic, @@ -1180,6 +1183,12 @@ impl RoundExecutor { let parsed_memory_citation = Self::parsed_memory_citation_from_stream_result(&stream_result); let (clean_text, _) = strip_bitfun_memory_citations(&stream_result.full_text); + let clean_text = if contains_tool_invocation_artifacts(&clean_text) { + warn!("Detected tool invocation artifacts in assistant text, stripping to prevent context explosion"); + strip_tool_invocation_artifacts(&clean_text) + } else { + clean_text + }; let assistant_message = Message::assistant_with_reasoning(reasoning, clean_text, tool_calls.clone()) .with_turn_id(context.dialog_turn_id.clone()) diff --git a/src/crates/assembly/core/src/agentic/execution/write_content_sanitizer.rs b/src/crates/assembly/core/src/agentic/execution/write_content_sanitizer.rs index e8486892c..cceb3d542 100644 --- a/src/crates/assembly/core/src/agentic/execution/write_content_sanitizer.rs +++ b/src/crates/assembly/core/src/agentic/execution/write_content_sanitizer.rs @@ -120,4 +120,27 @@ mod tests { "export const value = 1;" ); } + + /// Regression test for issue #1492: when a model leaks `` XML as + /// plain text content instead of structured tool-call deltas, the assistant + /// text must be detected and stripped so the artifacts do not pollute the + /// next round's context and cause infinite recursion. + #[test] + fn strips_leaked_tool_calls_xml_from_assistant_text() { + let leaked = concat!( + "I'll help you with that.\n", + "\n", + "\n", + "src/main.rs\n", + "\n", + "\n", + "Let me read the file first." + ); + assert!(contains_tool_invocation_artifacts(leaked)); + let stripped = strip_tool_invocation_artifacts(leaked); + assert!(!stripped.contains("