mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-29 00:49:31 +00:00
feat(engine): persist variables across code steps via state dict
Monty creates a fresh runtime per code step, so variables are lost
between steps. This caused the model to re-paste tool results from
system messages, wasting tokens.
Fix: maintain a `persisted_state` JSON dict in the ExecutionLoop that
accumulates across steps:
- Tool results stored by tool name: state["web_search"] = {results...}
- Return values stored: state["last_return"], state["step_0_return"]
- Injected as a `state` Python variable in each new MontyRun
Now the model can do:
Step 1: results = web_search(query="...") # tool result saved in state
Step 2: data = state["web_search"] # access previous result
summary = llm_query("summarize", str(data))
FINAL(summary)
System prompt updated to document the `state` variable.
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
@@ -97,6 +97,10 @@ impl ExecutionLoop {
|
||||
let max_nudges = self.thread.config.max_tool_intent_nudges;
|
||||
let nudge_enabled = self.thread.config.enable_tool_intent_nudge;
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
// Persisted state across code steps — accumulates return values
|
||||
// and tool results so the next step can access them via `state`.
|
||||
let mut persisted_state = serde_json::json!({});
|
||||
let mut nudge_count: u32 = 0;
|
||||
let mut consecutive_errors: u32 = 0;
|
||||
let mut compaction_count: u32 = 0;
|
||||
@@ -423,7 +427,7 @@ impl ExecutionLoop {
|
||||
step_id: step.id,
|
||||
};
|
||||
|
||||
// Execute via Monty
|
||||
// Execute via Monty with persisted state from prior steps
|
||||
let code_result = crate::executor::scripting::execute_code(
|
||||
&code,
|
||||
&self.thread,
|
||||
@@ -433,6 +437,7 @@ impl ExecutionLoop {
|
||||
&self.policy,
|
||||
&exec_ctx,
|
||||
&[],
|
||||
&persisted_state,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -484,6 +489,17 @@ impl ExecutionLoop {
|
||||
|
||||
step.action_results = code_result.action_results;
|
||||
|
||||
// Accumulate state for next step: return value + tool results.
|
||||
// This makes variables "persist" across code steps via `state`.
|
||||
if code_result.return_value != serde_json::Value::Null {
|
||||
persisted_state[format!("step_{}_return", self.thread.step_count)] =
|
||||
code_result.return_value.clone();
|
||||
persisted_state["last_return"] = code_result.return_value.clone();
|
||||
}
|
||||
for result in &step.action_results {
|
||||
persisted_state[&result.action_name] = result.output.clone();
|
||||
}
|
||||
|
||||
// Build comprehensive output for the LLM to see what happened.
|
||||
// Include stdout, tool results, and return value so the model
|
||||
// can reason about the outputs in the next iteration.
|
||||
|
||||
@@ -61,7 +61,8 @@ You can write multiple code blocks across turns. Variables persist between block
|
||||
- `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
|
||||
- `state` — Dict of persisted data from previous steps. Contains tool results keyed by tool name (e.g. `state['web_search']`) and return values (`state['last_return']`, `state['step_0_return']`). Use this to access data from previous steps without re-calling tools.
|
||||
- `previous_results` — Dict of prior tool call results (from ActionResult messages)
|
||||
|
||||
## Important rules
|
||||
|
||||
|
||||
@@ -151,7 +151,13 @@ pub fn build_orientation_preamble(thread: &Thread) -> String {
|
||||
// ── Context injection (RLM 3.4) ────────────────────────────
|
||||
|
||||
/// Build Monty input variables from thread state.
|
||||
fn build_context_inputs(thread: &Thread) -> (Vec<String>, Vec<MontyObject>) {
|
||||
///
|
||||
/// `persisted_state` carries variables from previous code steps so the
|
||||
/// REPL feels persistent even though each step creates a fresh MontyRun.
|
||||
fn build_context_inputs(
|
||||
thread: &Thread,
|
||||
persisted_state: &serde_json::Value,
|
||||
) -> (Vec<String>, Vec<MontyObject>) {
|
||||
let mut names = Vec::new();
|
||||
let mut values = Vec::new();
|
||||
|
||||
@@ -190,6 +196,12 @@ fn build_context_inputs(thread: &Thread) -> (Vec<String>, Vec<MontyObject>) {
|
||||
names.push("step_number".into());
|
||||
values.push(MontyObject::Int(thread.step_count as i64));
|
||||
|
||||
// `state` — persisted variables from previous code steps.
|
||||
// This is a dict that accumulates: return values, tool results, etc.
|
||||
// The model can read `state["results"]`, `state["prev_return"]`, etc.
|
||||
names.push("state".into());
|
||||
values.push(json_to_monty(persisted_state));
|
||||
|
||||
// `previous_results` — dict of {call_id: result_json} from prior steps
|
||||
let result_pairs: Vec<(MontyObject, MontyObject)> = thread
|
||||
.messages
|
||||
@@ -226,6 +238,7 @@ pub async fn execute_code(
|
||||
policy: &PolicyEngine,
|
||||
context: &ThreadExecutionContext,
|
||||
capability_policies: &[crate::types::capability::PolicyRule],
|
||||
persisted_state: &serde_json::Value,
|
||||
) -> Result<CodeExecutionResult, EngineError> {
|
||||
let mut stdout = String::new();
|
||||
let mut action_results = Vec::new();
|
||||
@@ -234,8 +247,8 @@ pub async fn execute_code(
|
||||
let mut final_answer: Option<String> = None;
|
||||
let mut had_error = false;
|
||||
|
||||
// Build context variables (RLM 3.4)
|
||||
let (input_names, input_values) = build_context_inputs(thread);
|
||||
// Build context variables including persisted state from prior steps
|
||||
let (input_names, input_values) = build_context_inputs(thread, persisted_state);
|
||||
|
||||
// Parse and compile (wrap in catch_unwind — Monty 0.0.x can panic)
|
||||
let runner = match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
|
||||
Reference in New Issue
Block a user