mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 15:40:18 +00:00
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]>
367 lines
11 KiB
Rust
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());
|
|
}
|
|
}
|