From cd59c28cf73f8e2d37d6d152355eb07cb16d7a6d Mon Sep 17 00:00:00 2001 From: Brent Rager Date: Wed, 3 Jun 2026 14:52:04 -0400 Subject: [PATCH] Pearl th-91075b: preserve assistant turn structure in prior_messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `run_agent_streaming()` was silently dropping any assistant message whose content cleaned to empty (i.e. when the prior turn was almost entirely [runner] tool prose). That broke inter-turn context — the LLM on turn 2 saw system + turn-1-user + turn-2-user and confabulated "I don't see a plan above" because the assistant's turn-1 reply had been silently removed. Now: empty-cleaned assistant turns get a short placeholder "(prior turn: ran tools; output omitted from prose history)" so the LLM at least knows an assistant turn happened. Tool calls themselves are already in the runner's structured channel, so we don't lose any tool semantics — only the turn-ordering signal was missing. Bench impact (deepseek-v4-flash, strict-coach default per Phase 1): cleanup-impossible-task : 0.500 → 1.000 (smooth now honestly refuses; 'I cannot perform the requested cleanup due to the directory not existing' triggers the HonestNo detector) cleanup-pycache-debris : 0.500 (unchanged — assistant turn was 'Proceed?' alone, which preserved correctly; the remaining gap is th-e5a0e5 where the fixer system prompt doesn't tell the model to enumerate plans in text before asking for confirmation) AGGREGATE : 0.500 → 0.750 --- crates/smooth-code/src/app.rs | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/crates/smooth-code/src/app.rs b/crates/smooth-code/src/app.rs index e903b680..2a15911f 100644 --- a/crates/smooth-code/src/app.rs +++ b/crates/smooth-code/src/app.rs @@ -1169,12 +1169,33 @@ async fn run_agent_streaming(message: &str, tx: mpsc::UnboundedSender>() .join("\n"); let trimmed = cleaned.trim(); - if trimmed.is_empty() { - continue; - } + // Pearl th-91075b: do NOT drop the whole assistant turn when + // its cleaned content is empty (e.g. when the assistant + // turn was almost entirely [runner] tool prose). Dropping + // the turn here was breaking inter-turn context — the LLM + // on turn 2 would see system + turn-1-user + turn-2-user + // and respond "I don't see a plan above" because the + // assistant's turn-1 reply had been silently removed. + // + // Preserve the turn structure with a brief placeholder so + // the LLM at least knows "an assistant turn happened here, + // it consisted of tool ops." The tool calls themselves are + // already in the runner's structured tool-call channel — + // this prose path only needs to keep the turn ordering + // intact. + let content = if trimmed.is_empty() { + match msg.role { + crate::state::ChatRole::Assistant => "(prior turn: ran tools; output omitted from prose history)".to_string(), + // User turns that are empty are truly nothing — + // skip those as before. + _ => continue, + } + } else { + trimmed.to_string() + }; out.push(crate::client::PriorMessage { role: role.to_string(), - content: trimmed.to_string(), + content, }); } out