From 2e03271cdbc9647c2fb11cc5d5abe81170b599ac Mon Sep 17 00:00:00 2001 From: "ilblackdragon@gmail.com" Date: Sun, 22 Mar 2026 13:08:44 -0700 Subject: [PATCH] fix(bridge): detect code blocks in plain completion path + multi-block support Two bugs fixed: 1. The no-tools completion path (used by CodeAct since we send empty actions) returned LlmResponse::Text without checking for code blocks. Code blocks were rendered as markdown text instead of being executed. 2. extract_code_block now: - Handles bare ``` fences (skips non-Python languages) - Collects ALL code blocks in the response and concatenates them (models often split code across multiple blocks with explanation) - Tries markers in order: ```repl, ```python, ```py, then bare ``` Co-Authored-By: Claude Opus 4.6 (1M context) --- src/bridge/llm_adapter.rs | 70 +++++++++++++++++++++++++++++++-------- 1 file changed, 56 insertions(+), 14 deletions(-) diff --git a/src/bridge/llm_adapter.rs b/src/bridge/llm_adapter.rs index 10eb7289..d4f1f75a 100644 --- a/src/bridge/llm_adapter.rs +++ b/src/bridge/llm_adapter.rs @@ -76,8 +76,17 @@ impl LlmBackend for LlmBridgeAdapter { reason: e.to_string(), })?; + // Check for code blocks in the response (CodeAct/RLM pattern) + let llm_response = match extract_code_block(&response.content) { + Some(code) => LlmResponse::Code { + code, + content: Some(response.content), + }, + None => LlmResponse::Text(response.content), + }; + return Ok(LlmOutput { - response: LlmResponse::Text(response.content), + response: llm_response, usage: TokenUsage { input_tokens: u64::from(response.input_tokens), output_tokens: u64::from(response.output_tokens), @@ -190,28 +199,61 @@ fn action_def_to_tool_def(action: &ActionDef) -> ToolDefinition { } } -/// Extract Python code from ```repl or ```python fenced blocks. +/// Extract Python code from fenced code blocks in the LLM response. /// -/// Matches the pattern used by RLM implementations (fast-rlm uses ```repl, -/// official RLM uses ```repl, some models output ```python). +/// Tries these markers in order: ```repl, ```python, ```py, then bare ``` +/// (if the content looks like Python). Collects ALL code blocks in the +/// response and concatenates them (models sometimes split code across +/// multiple blocks with explanation text between them). fn extract_code_block(text: &str) -> Option { - // Try ```repl first (preferred), then ```python - for marker in ["```repl", "```python"] { - if let Some(start) = text.find(marker) { - let code_start = start + marker.len(); - // Skip to next line - let code_start = text[code_start..] + let mut all_code = Vec::new(); + + // Try specific markers first, then bare backticks + for marker in ["```repl", "```python", "```py", "```"] { + let mut search_from = 0; + while let Some(start) = text[search_from..].find(marker) { + let abs_start = search_from + start; + let after_marker = abs_start + marker.len(); + + // For bare ```, skip if it's actually ```someotherlang + if marker == "```" && text[after_marker..].starts_with(|c: char| c.is_alphabetic()) { + let lang: String = text[after_marker..] + .chars() + .take_while(|c| c.is_alphanumeric() || *c == '-' || *c == '_') + .collect(); + if !["repl", "python", "py"].contains(&lang.as_str()) { + search_from = after_marker; + continue; + } + } + + // Skip to next line after the marker + let code_start = text[after_marker..] .find('\n') - .map(|i| code_start + i + 1) - .unwrap_or(code_start); + .map(|i| after_marker + i + 1) + .unwrap_or(after_marker); + // Find closing ``` if let Some(end) = text[code_start..].find("```") { let code = text[code_start..code_start + end].trim(); if !code.is_empty() { - return Some(code.to_string()); + all_code.push(code.to_string()); } + search_from = code_start + end + 3; + } else { + break; } } + + // If we found code with a specific marker, use it (don't fall through to bare) + if !all_code.is_empty() { + break; + } } - None + + if all_code.is_empty() { + return None; + } + + Some(all_code.join("\n\n")) }