fix(agent): improve ReAct parser and tool schemas for better LLM compatibility

- Fix ACTION_INPUT regex to properly capture multiline JSON responses
  - Changed from stopping at first newline to capturing all remaining text
  - Resolves parsing errors when LLM generates formatted JSON with line breaks

- Enhance tool schemas with detailed descriptions and parameter specifications
  - Add comprehensive Message schema for generate_text tool
  - Clarify distinction between resources/get (file read) and resources/list (directory listing)
  - Include clear usage guidance in tool descriptions

- Set default model to llama3.2:latest instead of invalid "ollama"

- Add parse error debugging to help troubleshoot LLM response issues

The agent infrastructure now correctly handles multiline tool arguments and
provides better guidance to LLMs through improved tool schemas. Remaining
errors are due to LLM quality (model making poor tool choices or generating
malformed responses), not infrastructure bugs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-09 19:43:07 +02:00
parent 05e90d3e2b
commit 33d11ae223
25 changed files with 1348 additions and 121 deletions

View File

@@ -1,4 +1,5 @@
use anyhow::Result;
use owlen_core::mcp::remote_client::RemoteMcpClient;
use owlen_core::{
provider::{Provider, ProviderConfig},
session::{SessionController, SessionOutcome},
@@ -14,7 +15,8 @@ use uuid::Uuid;
use crate::config;
use crate::events::Event;
use owlen_core::mcp::remote_client::RemoteMcpClient;
// Agent executor moved to separate binary `owlen-agent`. The TUI no longer directly
// imports `AgentExecutor` to avoid a circular dependency on `owlen-cli`.
use std::collections::{BTreeSet, HashSet};
use std::sync::Arc;
@@ -108,6 +110,18 @@ pub enum SessionEvent {
endpoints: Vec<String>,
callback_id: Uuid,
},
/// Agent iteration update (shows THOUGHT/ACTION/OBSERVATION)
AgentUpdate {
content: String,
},
/// Agent execution completed with final answer
AgentCompleted {
answer: String,
},
/// Agent execution failed
AgentFailed {
error: String,
},
}
pub const HELP_TAB_COUNT: usize = 7;
@@ -138,11 +152,13 @@ pub struct ChatApp {
loading_animation_frame: usize, // Frame counter for loading animation
is_loading: bool, // Whether we're currently loading a response
current_thinking: Option<String>, // Current thinking content from last assistant message
pending_key: Option<char>, // For multi-key sequences like gg, dd
clipboard: String, // Vim-style clipboard for yank/paste
command_buffer: String, // Buffer for command mode input
// Holds the latest formatted Agentic ReAct actions (thought/action/observation)
agent_actions: Option<String>,
pending_key: Option<char>, // For multi-key sequences like gg, dd
clipboard: String, // Vim-style clipboard for yank/paste
command_buffer: String, // Buffer for command mode input
command_suggestions: Vec<String>, // Filtered command suggestions based on current input
selected_suggestion: usize, // Index of selected suggestion
selected_suggestion: usize, // Index of selected suggestion
visual_start: Option<(usize, usize)>, // Visual mode selection start (row, col) for Input panel
visual_end: Option<(usize, usize)>, // Visual mode selection end (row, col) for scrollable panels
focused_panel: FocusedPanel, // Currently focused panel for scrolling
@@ -156,6 +172,12 @@ pub struct ChatApp {
selected_theme_index: usize, // Index of selected theme in browser
pending_consent: Option<ConsentDialogState>, // Pending consent request
system_status: String, // System/status messages (tool execution, status, etc)
/// Simple execution budget: maximum number of tool calls allowed per session.
_execution_budget: usize,
/// Agent mode enabled
agent_mode: bool,
/// Agent running flag
agent_running: bool,
}
#[derive(Clone, Debug)]
@@ -210,6 +232,7 @@ impl ChatApp {
loading_animation_frame: 0,
is_loading: false,
current_thinking: None,
agent_actions: None,
pending_key: None,
clipboard: String::new(),
command_buffer: String::new(),
@@ -228,6 +251,9 @@ impl ChatApp {
selected_theme_index: 0,
pending_consent: None,
system_status: String::new(),
_execution_budget: 50,
agent_mode: false,
agent_running: false,
};
Ok((app, session_rx))
@@ -396,6 +422,8 @@ impl ChatApp {
("privacy-enable", "Enable a privacy-sensitive tool"),
("privacy-disable", "Disable a privacy-sensitive tool"),
("privacy-clear", "Clear stored secure data"),
("agent", "Enable agent mode for autonomous task execution"),
("stop-agent", "Stop the running agent"),
]
}
@@ -1495,6 +1523,25 @@ impl ChatApp {
self.command_suggestions.clear();
return Ok(AppState::Running);
}
// "run-agent" command removed to break circular dependency on owlen-cli.
"agent" => {
if self.agent_running {
self.status = "Agent is already running".to_string();
} else {
self.agent_mode = true;
self.status = "Agent mode enabled. Next message will be processed by agent.".to_string();
}
}
"stop-agent" => {
if self.agent_running {
self.agent_running = false;
self.agent_mode = false;
self.status = "Agent execution stopped".to_string();
self.agent_actions = None;
} else {
self.status = "No agent is currently running".to_string();
}
}
"n" | "new" => {
self.controller.start_new_conversation(None, None);
self.status = "Started new conversation".to_string();
@@ -2166,6 +2213,28 @@ impl ChatApp {
});
self.status = "Consent required - Press Y to allow, N to deny".to_string();
}
SessionEvent::AgentUpdate { content } => {
// Update agent actions panel with latest ReAct iteration
self.set_agent_actions(content);
}
SessionEvent::AgentCompleted { answer } => {
// Agent finished, add final answer to conversation
self.controller
.conversation_mut()
.push_assistant_message(answer);
self.agent_running = false;
self.agent_mode = false;
self.agent_actions = None;
self.status = "Agent completed successfully".to_string();
self.stop_loading_animation();
}
SessionEvent::AgentFailed { error } => {
// Agent failed, show error
self.error = Some(format!("Agent failed: {}", error));
self.agent_running = false;
self.agent_actions = None;
self.stop_loading_animation();
}
}
Ok(())
}
@@ -2577,6 +2646,11 @@ impl ChatApp {
self.pending_llm_request = false;
// Check if agent mode is enabled
if self.agent_mode {
return self.process_agent_request().await;
}
// Step 1: Show loading model status and start animation
self.status = format!("Loading model '{}'...", self.controller.selected_model());
self.start_loading_animation();
@@ -2640,6 +2714,77 @@ impl ChatApp {
}
}
async fn process_agent_request(&mut self) -> Result<()> {
use owlen_core::agent::{AgentConfig, AgentExecutor};
use owlen_core::mcp::remote_client::RemoteMcpClient;
use std::sync::Arc;
self.agent_running = true;
self.status = "Agent is running...".to_string();
self.start_loading_animation();
// Get the last user message
let user_message = self
.controller
.conversation()
.messages
.iter()
.rev()
.find(|m| m.role == owlen_core::types::Role::User)
.map(|m| m.content.clone())
.unwrap_or_default();
// Create agent config
let config = AgentConfig {
max_iterations: 10,
model: self.controller.selected_model().to_string(),
temperature: Some(0.7),
max_tokens: None,
max_tool_calls: 20,
};
// Get the provider
let provider = self.controller.provider().clone();
// Create MCP client
let mcp_client = match RemoteMcpClient::new() {
Ok(client) => Arc::new(client),
Err(e) => {
self.error = Some(format!("Failed to initialize MCP client: {}", e));
self.agent_running = false;
self.agent_mode = false;
self.stop_loading_animation();
return Ok(());
}
};
// Create agent executor
let executor = AgentExecutor::new(provider, mcp_client, config, None);
// Run agent
match executor.run(user_message).await {
Ok(answer) => {
self.controller
.conversation_mut()
.push_assistant_message(answer);
self.agent_running = false;
self.agent_mode = false;
self.agent_actions = None;
self.status = "Agent completed successfully".to_string();
self.stop_loading_animation();
Ok(())
}
Err(e) => {
self.error = Some(format!("Agent failed: {}", e));
self.agent_running = false;
self.agent_mode = false;
self.agent_actions = None;
self.stop_loading_animation();
Ok(())
}
}
}
pub async fn process_pending_tool_execution(&mut self) -> Result<()> {
if self.pending_tool_execution.is_none() {
return Ok(());
@@ -2813,6 +2958,26 @@ impl ChatApp {
self.current_thinking.as_ref()
}
/// Get a reference to the latest agent actions, if any.
pub fn agent_actions(&self) -> Option<&String> {
self.agent_actions.as_ref()
}
/// Set the current agent actions content.
pub fn set_agent_actions(&mut self, actions: String) {
self.agent_actions = Some(actions);
}
/// Check if agent mode is enabled
pub fn is_agent_mode(&self) -> bool {
self.agent_mode
}
/// Check if agent is currently running
pub fn is_agent_running(&self) -> bool {
self.agent_running
}
pub fn get_rendered_lines(&self) -> Vec<String> {
match self.focused_panel {
FocusedPanel::Chat => {