feat(engine): enable CodeAct/RLM mode with code block detection

The engine now operates in CodeAct/RLM mode:

System prompt (executor/prompt.rs):
- Instructs LLM to write Python in ```repl fenced blocks
- Documents available tools as callable Python functions
- Documents llm_query(), llm_query_batched(), FINAL()
- Documents context variables (context, goal, step_number, previous_results)
- Strategy guidance: examine context, break into steps, use tools, call FINAL()

Code block detection (bridge/llm_adapter.rs):
- extract_code_block() scans LLM text responses for ```repl or ```python blocks
- When detected, returns LlmResponse::Code instead of LlmResponse::Text
- The ExecutionLoop routes Code responses through Monty for execution

No structured tool definitions sent to LLM:
- Tools are described in the system prompt as Python functions
- The LLM call sends empty actions array, forcing text-mode responses
- This ensures the LLM writes code blocks (CodeAct) instead of
  structured tool calls (which would bypass the REPL)

85 tests passing, zero clippy warnings.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
2026-03-22 12:38:07 -07:00
co-authored by Claude Opus 4.6
parent 4e8b94a555
commit 749c208b3c
4 changed files with 135 additions and 12 deletions
+36 -2
View File
@@ -102,7 +102,7 @@ impl LlmBackend for LlmBridgeAdapter {
reason: e.to_string(),
})?;
// Convert response
// Convert response — check for code blocks (CodeAct/RLM pattern)
let llm_response = if !response.tool_calls.is_empty() {
LlmResponse::ActionCalls {
calls: response
@@ -117,7 +117,15 @@ impl LlmBackend for LlmBridgeAdapter {
content: response.content.clone(),
}
} else {
LlmResponse::Text(response.content.unwrap_or_default())
let text = response.content.unwrap_or_default();
// Detect ```repl or ```python fenced code blocks
match extract_code_block(&text) {
Some(code) => LlmResponse::Code {
code,
content: Some(text),
},
None => LlmResponse::Text(text),
}
};
Ok(LlmOutput {
@@ -181,3 +189,29 @@ fn action_def_to_tool_def(action: &ActionDef) -> ToolDefinition {
parameters: action.parameters_schema.clone(),
}
}
/// Extract Python code from ```repl or ```python fenced blocks.
///
/// Matches the pattern used by RLM implementations (fast-rlm uses ```repl,
/// official RLM uses ```repl, some models output ```python).
fn extract_code_block(text: &str) -> Option<String> {
// 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..]
.find('\n')
.map(|i| code_start + i + 1)
.unwrap_or(code_start);
// 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());
}
}
}
}
None
}