fix(engine): include tool results in code step output for LLM context

The LLM was ignoring tool results and answering from training data
because the compact output metadata didn't include what tools returned.
Tool results lived only as ActionResult messages (role: Tool) which
some providers flatten or the model ignores.

Now the code step output includes:
- stdout from Python print() statements
- [tool_name result] with the actual output (truncated to 4K per tool)
- [tool_name error] for failed tools
- [return] for the code's return value
- Total output truncated to 8K chars to prevent context bloat

This ensures the model sees web_search results, API responses, etc.
in the next iteration and can reason about them instead of hallucinating.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
2026-03-22 21:25:48 -07:00
co-authored by Claude Opus 4.6
parent 7087964c22
commit 51e1bf5533
@@ -401,11 +401,43 @@ impl ExecutionLoop {
step.action_results = code_result.action_results;
// Compact output metadata (RLM pattern: truncated, not full)
let metadata = crate::executor::scripting::compact_output_metadata(
&code_result.stdout,
&code_result.return_value,
);
// 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.
let mut output_parts = Vec::new();
if !code_result.stdout.is_empty() {
output_parts.push(code_result.stdout.clone());
}
for result in &step.action_results {
let output_str = serde_json::to_string(&result.output).unwrap_or_default();
let truncated = if output_str.len() > 4000 {
format!("{}... [truncated, {} total chars]", &output_str[..4000], output_str.len())
} else {
output_str
};
if result.is_error {
output_parts.push(format!("[{} error] {}", result.action_name, truncated));
} else {
output_parts.push(format!("[{} result] {}", result.action_name, truncated));
}
}
if code_result.return_value != serde_json::Value::Null {
output_parts.push(format!(
"[return] {}",
serde_json::to_string_pretty(&code_result.return_value).unwrap_or_default()
));
}
let output_text = if output_parts.is_empty() {
"[code executed, no output]".to_string()
} else {
output_parts.join("\n")
};
// Truncate total output to prevent context bloat
let metadata = if output_text.len() > 8000 {
format!("[TRUNCATED: last 8000 of {} chars]\n{}", output_text.len(), &output_text[output_text.len()-8000..])
} else {
output_text
};
self.thread.add_message(ThreadMessage::system(metadata));
step.status = StepStatus::Completed;