Files
optimclaw/src/llm/reasoning.rs
T
Illia PolosukhinandClaude Opus 4.5 09032d69cb Integrate Codex patterns: task scheduler, TUI, sessions, compaction
Add Codex-inspired patterns for improved agent architecture:

**Task Scheduler (Phase 1 & 4)**
- New Task enum with Job, ToolExec, Background variants
- TaskHandler trait for custom background tasks
- Scheduler.spawn_subtask() and spawn_batch() for parallel execution
- Worker executes multiple tools in parallel via futures::join_all

**Tool Approval System (Phase 6)**
- Tool.requires_approval() method (default false for sandboxed tools)
- HttpTool marked as requiring approval (external network)
- MCP protocol annotations: destructive_hint, side_effects_hint

**Ratatui TUI CLI (Phase 2)**
- Replace blocking stdin with event-driven TUI
- ChatComposer with history navigation and tab completion
- ApprovalOverlay modal with y/n/a keyboard shortcuts
- Raw mode with proper terminal cleanup

**Session/Turn Model (Phase 3)**
- Session, Thread, Turn structs for conversation tracking
- Submission enum for user input, approvals, undo, interrupt
- UndoManager with checkpoint-based undo/redo

**Context Compaction (Phase 5)**
- ContextMonitor with token estimation and threshold checks
- CompactionStrategy: Summarize, Truncate, MoveToWorkspace
- ContextCompactor writes summaries to workspace daily logs

Co-Authored-By: Claude Opus 4.5 <[email protected]>
2026-02-02 23:41:57 -08:00

367 lines
11 KiB
Rust

//! LLM reasoning capabilities for planning, tool selection, and evaluation.
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::error::LlmError;
use crate::llm::{
ChatMessage, CompletionRequest, LlmProvider, ToolCompletionRequest, ToolDefinition,
};
use crate::safety::SafetyLayer;
/// Context for reasoning operations.
pub struct ReasoningContext {
/// Conversation history.
pub messages: Vec<ChatMessage>,
/// Available tools.
pub available_tools: Vec<ToolDefinition>,
/// Job description if working on a job.
pub job_description: Option<String>,
/// Current state description.
pub current_state: Option<String>,
}
impl ReasoningContext {
/// Create a new reasoning context.
pub fn new() -> Self {
Self {
messages: Vec::new(),
available_tools: Vec::new(),
job_description: None,
current_state: None,
}
}
/// Add a message to the context.
pub fn with_message(mut self, message: ChatMessage) -> Self {
self.messages.push(message);
self
}
/// Set available tools.
pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
self.available_tools = tools;
self
}
/// Set job description.
pub fn with_job(mut self, description: impl Into<String>) -> Self {
self.job_description = Some(description.into());
self
}
}
impl Default for ReasoningContext {
fn default() -> Self {
Self::new()
}
}
/// A planned action to take.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlannedAction {
/// Tool to use.
pub tool_name: String,
/// Parameters for the tool.
pub parameters: serde_json::Value,
/// Reasoning for this action.
pub reasoning: String,
/// Expected outcome.
pub expected_outcome: String,
}
/// Result of planning.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActionPlan {
/// Overall goal understanding.
pub goal: String,
/// Planned sequence of actions.
pub actions: Vec<PlannedAction>,
/// Estimated total cost.
pub estimated_cost: Option<f64>,
/// Estimated total time in seconds.
pub estimated_time_secs: Option<u64>,
/// Confidence in the plan (0-1).
pub confidence: f64,
}
/// Result of tool selection.
#[derive(Debug, Clone)]
pub struct ToolSelection {
/// Selected tool name.
pub tool_name: String,
/// Parameters for the tool.
pub parameters: serde_json::Value,
/// Reasoning for the selection.
pub reasoning: String,
/// Alternative tools considered.
pub alternatives: Vec<String>,
}
/// Reasoning engine for the agent.
pub struct Reasoning {
llm: Arc<dyn LlmProvider>,
safety: Arc<SafetyLayer>,
}
impl Reasoning {
/// Create a new reasoning engine.
pub fn new(llm: Arc<dyn LlmProvider>, safety: Arc<SafetyLayer>) -> Self {
Self { llm, safety }
}
/// Generate a plan for completing a goal.
pub async fn plan(&self, context: &ReasoningContext) -> Result<ActionPlan, LlmError> {
let system_prompt = self.build_planning_prompt(context);
let mut messages = vec![ChatMessage::system(system_prompt)];
messages.extend(context.messages.clone());
if let Some(ref job) = context.job_description {
messages.push(ChatMessage::user(format!(
"Please create a plan to complete this job:\n\n{}",
job
)));
}
let request = CompletionRequest::new(messages)
.with_max_tokens(2048)
.with_temperature(0.3);
let response = self.llm.complete(request).await?;
// Parse the plan from the response
self.parse_plan(&response.content)
}
/// Select the best tool for the current situation.
pub async fn select_tool(
&self,
context: &ReasoningContext,
) -> Result<Option<ToolSelection>, LlmError> {
let tools = self.select_tools(context).await?;
Ok(tools.into_iter().next())
}
/// Select tools to execute (may return multiple for parallel execution).
///
/// The LLM may return multiple tool calls if it determines they can be
/// executed in parallel. This enables more efficient job completion.
pub async fn select_tools(
&self,
context: &ReasoningContext,
) -> Result<Vec<ToolSelection>, LlmError> {
if context.available_tools.is_empty() {
return Ok(vec![]);
}
let request =
ToolCompletionRequest::new(context.messages.clone(), context.available_tools.clone())
.with_max_tokens(1024)
.with_tool_choice("auto");
let response = self.llm.complete_with_tools(request).await?;
let reasoning = response.content.unwrap_or_default();
let selections: Vec<ToolSelection> = response
.tool_calls
.into_iter()
.map(|tool_call| ToolSelection {
tool_name: tool_call.name,
parameters: tool_call.arguments,
reasoning: reasoning.clone(),
alternatives: vec![],
})
.collect();
Ok(selections)
}
/// Evaluate whether a task was completed successfully.
pub async fn evaluate_success(
&self,
context: &ReasoningContext,
result: &str,
) -> Result<SuccessEvaluation, LlmError> {
let system_prompt = r#"You are an evaluation assistant. Your job is to determine if a task was completed successfully.
Analyze the task description and the result, then provide:
1. Whether the task was successful (true/false)
2. A confidence score (0-1)
3. Detailed reasoning
4. Any issues found
5. Suggestions for improvement
Respond in JSON format:
{
"success": true/false,
"confidence": 0.0-1.0,
"reasoning": "...",
"issues": ["..."],
"suggestions": ["..."]
}"#;
let mut messages = vec![ChatMessage::system(system_prompt)];
if let Some(ref job) = context.job_description {
messages.push(ChatMessage::user(format!(
"Task description:\n{}\n\nResult:\n{}",
job, result
)));
} else {
messages.push(ChatMessage::user(format!(
"Result to evaluate:\n{}",
result
)));
}
let request = CompletionRequest::new(messages)
.with_max_tokens(1024)
.with_temperature(0.1);
let response = self.llm.complete(request).await?;
self.parse_evaluation(&response.content)
}
/// Generate a response to a user message.
pub async fn respond(&self, context: &ReasoningContext) -> Result<String, LlmError> {
let system_prompt = self.build_conversation_prompt();
let mut messages = vec![ChatMessage::system(system_prompt)];
messages.extend(context.messages.clone());
let request = CompletionRequest::new(messages)
.with_max_tokens(2048)
.with_temperature(0.7);
let response = self.llm.complete(request).await?;
Ok(response.content)
}
fn build_planning_prompt(&self, context: &ReasoningContext) -> String {
let tools_desc = if context.available_tools.is_empty() {
"No tools available.".to_string()
} else {
context
.available_tools
.iter()
.map(|t| format!("- {}: {}", t.name, t.description))
.collect::<Vec<_>>()
.join("\n")
};
format!(
r#"You are a planning assistant for an autonomous agent. Your job is to create detailed, actionable plans.
Available tools:
{tools_desc}
When creating a plan:
1. Break down the goal into specific, achievable steps
2. Select the most appropriate tool for each step
3. Consider dependencies between steps
4. Estimate costs and time realistically
5. Identify potential failure points
Respond with a JSON plan in this format:
{{
"goal": "Clear statement of the goal",
"actions": [
{{
"tool_name": "tool_to_use",
"parameters": {{}},
"reasoning": "Why this action",
"expected_outcome": "What should happen"
}}
],
"estimated_cost": 0.0,
"estimated_time_secs": 0,
"confidence": 0.0-1.0
}}"#
)
}
fn build_conversation_prompt(&self) -> String {
r#"You are a helpful AI agent assistant. You help users with tasks by:
1. Understanding their requests clearly
2. Asking clarifying questions when needed
3. Providing accurate, helpful responses
4. Being honest about limitations
Be concise but thorough. If you're unsure, say so."#
.to_string()
}
fn parse_plan(&self, content: &str) -> Result<ActionPlan, LlmError> {
// Try to extract JSON from the response
let json_str = extract_json(content).unwrap_or(content);
serde_json::from_str(json_str).map_err(|e| LlmError::InvalidResponse {
provider: self.llm.model_name().to_string(),
reason: format!("Failed to parse plan: {}", e),
})
}
fn parse_evaluation(&self, content: &str) -> Result<SuccessEvaluation, LlmError> {
let json_str = extract_json(content).unwrap_or(content);
serde_json::from_str(json_str).map_err(|e| LlmError::InvalidResponse {
provider: self.llm.model_name().to_string(),
reason: format!("Failed to parse evaluation: {}", e),
})
}
}
/// Result of success evaluation.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuccessEvaluation {
pub success: bool,
pub confidence: f64,
pub reasoning: String,
#[serde(default)]
pub issues: Vec<String>,
#[serde(default)]
pub suggestions: Vec<String>,
}
/// Extract JSON from text that might contain other content.
fn extract_json(text: &str) -> Option<&str> {
// Find the first { and last } to extract JSON
let start = text.find('{')?;
let end = text.rfind('}')?;
if start < end {
Some(&text[start..=end])
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_json() {
let text = r#"Here's the plan:
{"goal": "test", "actions": []}
That's my plan."#;
let json = extract_json(text).unwrap();
assert!(json.starts_with('{'));
assert!(json.ends_with('}'));
}
#[test]
fn test_reasoning_context_builder() {
let context = ReasoningContext::new()
.with_message(ChatMessage::user("Hello"))
.with_job("Test job");
assert_eq!(context.messages.len(), 1);
assert!(context.job_description.is_some());
}
}