mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
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:
@@ -61,15 +61,14 @@ impl ExecutionLoop {
|
||||
// Transition to Running
|
||||
self.thread.transition_to(ThreadState::Running, None)?;
|
||||
|
||||
// Inject system prompt if none exists
|
||||
// Inject CodeAct/RLM system prompt if none exists
|
||||
if !self.thread.messages.iter().any(|m| m.role == crate::types::message::MessageRole::System) {
|
||||
self.thread.messages.insert(
|
||||
0,
|
||||
ThreadMessage::system(
|
||||
"You are a helpful assistant. Use the available tools to accomplish the user's request. \
|
||||
Respond concisely.",
|
||||
),
|
||||
);
|
||||
// Get available actions for the prompt
|
||||
let active_leases = self.leases.active_for_thread(self.thread.id).await;
|
||||
let actions = self.effects.available_actions(&active_leases).await
|
||||
.unwrap_or_default();
|
||||
let system_prompt = crate::executor::prompt::build_codeact_system_prompt(&actions);
|
||||
self.thread.messages.insert(0, ThreadMessage::system(system_prompt));
|
||||
}
|
||||
|
||||
let max_iterations = self.thread.config.max_iterations;
|
||||
@@ -163,7 +162,7 @@ impl ExecutionLoop {
|
||||
let active_leases = self.leases.active_for_thread(self.thread.id).await;
|
||||
|
||||
// 5. Build context
|
||||
let (messages, actions) =
|
||||
let (messages, _actions) =
|
||||
build_step_context(&self.thread.messages, &active_leases, &self.effects).await?;
|
||||
|
||||
// 6. Create step
|
||||
@@ -174,6 +173,10 @@ impl ExecutionLoop {
|
||||
});
|
||||
|
||||
// 7. Call LLM
|
||||
// CodeAct/RLM: send NO structured tool definitions — tools are described
|
||||
// in the system prompt as Python functions. The LLM produces text with
|
||||
// ```repl code blocks that the bridge detects and converts to LlmResponse::Code.
|
||||
// This avoids the LLM using structured tool calls instead of writing code.
|
||||
let force_text = iteration >= max_iterations.saturating_sub(1);
|
||||
let config = LlmCallConfig {
|
||||
force_text,
|
||||
@@ -181,7 +184,7 @@ impl ExecutionLoop {
|
||||
..LlmCallConfig::default()
|
||||
};
|
||||
|
||||
let llm_output = self.llm.complete(&messages, &actions, &config).await?;
|
||||
let llm_output = self.llm.complete(&messages, &[], &config).await?;
|
||||
step.tokens_used = llm_output.usage;
|
||||
self.thread.total_tokens_used += llm_output.usage.total();
|
||||
step.llm_response = Some(llm_output.response.clone());
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod compaction;
|
||||
pub mod context;
|
||||
pub mod intent;
|
||||
pub mod loop_engine;
|
||||
pub mod prompt;
|
||||
pub mod scripting;
|
||||
pub mod structured;
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//! System prompt construction for the execution loop.
|
||||
//!
|
||||
//! Builds a CodeAct/RLM system prompt that instructs the LLM to write
|
||||
//! Python code in ```repl blocks with tools available as callable functions.
|
||||
|
||||
use crate::types::capability::ActionDef;
|
||||
|
||||
/// Build the system prompt for CodeAct/RLM execution.
|
||||
///
|
||||
/// The prompt instructs the LLM to:
|
||||
/// - Write Python code in ```repl fenced blocks
|
||||
/// - Call tools as regular Python functions
|
||||
/// - Use llm_query(prompt, context) for sub-agent calls
|
||||
/// - Use FINAL(answer) to return the final answer
|
||||
/// - Access thread context via the `context` variable
|
||||
pub fn build_codeact_system_prompt(actions: &[ActionDef]) -> String {
|
||||
let mut prompt = String::from(CODEACT_PREAMBLE);
|
||||
|
||||
// Add tool documentation
|
||||
if !actions.is_empty() {
|
||||
prompt.push_str("\n## Available tools (call as Python functions)\n\n");
|
||||
for action in actions {
|
||||
prompt.push_str(&format!("- `{}(", action.name));
|
||||
// Extract parameter names from JSON schema
|
||||
if let Some(props) = action.parameters_schema.get("properties")
|
||||
&& let Some(obj) = props.as_object()
|
||||
{
|
||||
let params: Vec<&str> = obj.keys().map(String::as_str).collect();
|
||||
prompt.push_str(¶ms.join(", "));
|
||||
}
|
||||
prompt.push_str(&format!(")` — {}\n", action.description));
|
||||
}
|
||||
}
|
||||
|
||||
prompt.push_str(CODEACT_POSTAMBLE);
|
||||
prompt
|
||||
}
|
||||
|
||||
const CODEACT_PREAMBLE: &str = "\
|
||||
You are an AI assistant with a Python REPL environment. You solve tasks by writing and executing Python code.
|
||||
|
||||
## How to respond
|
||||
|
||||
Write Python code inside ```repl fenced blocks. The code will be executed, and you'll see the output.
|
||||
|
||||
```repl
|
||||
result = web_fetch(url=\"https://api.example.com/data\")
|
||||
print(result)
|
||||
```
|
||||
|
||||
You can write multiple code blocks across turns. Variables persist between blocks within the same turn.
|
||||
|
||||
## Special functions
|
||||
|
||||
- `llm_query(prompt, context=None)` — Ask a sub-agent to analyze text or answer a question. Returns a string. Use for summarization, analysis, or any task that needs LLM reasoning on data.
|
||||
- `llm_query_batched(prompts, context=None)` — Same but for multiple prompts in parallel. Returns a list of strings.
|
||||
- `FINAL(answer)` — Call this when you have the final answer. The argument is returned to the user.
|
||||
|
||||
## Context variables
|
||||
|
||||
- `context` — List of prior conversation messages (each is a dict with 'role' and 'content')
|
||||
- `goal` — The current task description
|
||||
- `step_number` — Current execution step
|
||||
- `previous_results` — Dict of prior tool call results
|
||||
|
||||
## Important rules
|
||||
|
||||
1. Always write code in ```repl blocks — plain text responses are for brief explanations only
|
||||
2. When you have the final answer, call `FINAL(answer)` inside a code block
|
||||
3. Tool results are returned as Python objects — use them directly, don't parse JSON
|
||||
4. If a tool call fails, the error appears as a Python exception — handle it or try a different approach
|
||||
5. For large data, process it in chunks using llm_query() on subsets rather than loading everything into context
|
||||
6. Outputs are truncated to 8000 chars — use variables to store large intermediate results";
|
||||
|
||||
const CODEACT_POSTAMBLE: &str = "
|
||||
|
||||
## Strategy
|
||||
|
||||
1. First, examine the context and understand the task
|
||||
2. Break complex tasks into steps
|
||||
3. Use tools to gather information or take actions
|
||||
4. Use llm_query() to analyze or summarize large text
|
||||
5. Call FINAL() with the answer when done
|
||||
|
||||
Think step by step. Execute code immediately — don't just describe what you would do.";
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user