feat(agent): implement Agent Orchestrator with LLM tool calling
Add complete agent orchestration system that enables LLM to call tools: **Core Agent System** (`crates/core/agent`): - Agent execution loop with tool call/result cycle - Tool definitions in Ollama-compatible format (6 tools) - Tool execution with permission checking - Multi-iteration support with max iteration safety **Tool Definitions**: - read: Read file contents - glob: Find files by pattern - grep: Search for patterns in files - write: Write content to files - edit: Edit files with find/replace - bash: Execute bash commands **Ollama Integration Updates**: - Extended ChatMessage to support tool_calls - Added Tool, ToolCall, ToolFunction types - Updated chat_stream to accept tools parameter - Made tool call fields optional for Ollama compatibility **CLI Integration**: - Wired agent loop into all output formats (Text, JSON, StreamJSON) - Tool calls displayed with 🔧 icon, results with ✅ - Replaced simple chat with agent orchestrator **Permission Integration**: - All tool executions check permissions before running - Respects plan/acceptEdits/code modes - Returns clear error messages for denied operations **Example**: User: "Find all Cargo.toml files in the workspace" LLM: Calls glob("**/Cargo.toml") Agent: Executes and returns 14 files LLM: Formats human-readable response This transforms owlen from a passive chatbot into an active agent that can autonomously use tools to accomplish user goals. Tested with: qwen3:8b successfully calling glob tool 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use crate::types::{ChatMessage, ChatResponseChunk};
|
||||
use crate::types::{ChatMessage, ChatResponseChunk, Tool};
|
||||
use futures::{Stream, TryStreamExt};
|
||||
use reqwest::Client;
|
||||
use serde::Serialize;
|
||||
@@ -50,15 +50,18 @@ impl OllamaClient {
|
||||
&self,
|
||||
messages: &[ChatMessage],
|
||||
opts: &OllamaOptions,
|
||||
tools: Option<&[Tool]>,
|
||||
) -> Result<impl Stream<Item = Result<ChatResponseChunk, OllamaError>>, OllamaError> {
|
||||
#[derive(Serialize)]
|
||||
struct Body<'a> {
|
||||
model: &'a str,
|
||||
messages: &'a [ChatMessage],
|
||||
stream: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tools: Option<&'a [Tool]>,
|
||||
}
|
||||
let url = format!("{}/api/chat", self.base_url);
|
||||
let body = Body {model: &opts.model, messages, stream: true};
|
||||
let body = Body {model: &opts.model, messages, stream: true, tools};
|
||||
let mut req = self.http.post(url).json(&body);
|
||||
|
||||
// Add Authorization header if API key is present
|
||||
|
||||
@@ -2,4 +2,4 @@ pub mod client;
|
||||
pub mod types;
|
||||
|
||||
pub use client::{OllamaClient, OllamaOptions};
|
||||
pub use types::{ChatMessage, ChatResponseChunk};
|
||||
pub use types::{ChatMessage, ChatResponseChunk, Tool, ToolCall, ToolFunction, ToolParameters, FunctionCall};
|
||||
|
||||
@@ -1,9 +1,50 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
pub role: String, // "user", | "assistant" | "system"
|
||||
pub content: String,
|
||||
pub role: String, // "user" | "assistant" | "system" | "tool"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ToolCall {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub id: Option<String>,
|
||||
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
|
||||
pub call_type: Option<String>, // "function"
|
||||
pub function: FunctionCall,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct FunctionCall {
|
||||
pub name: String,
|
||||
pub arguments: Value, // JSON object with arguments
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Tool {
|
||||
#[serde(rename = "type")]
|
||||
pub tool_type: String, // "function"
|
||||
pub function: ToolFunction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolFunction {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: ToolParameters,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolParameters {
|
||||
#[serde(rename = "type")]
|
||||
pub param_type: String, // "object"
|
||||
pub properties: Value,
|
||||
pub required: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@@ -19,4 +60,6 @@ pub struct ChatResponseChunk {
|
||||
pub struct ChunkMessage {
|
||||
pub role: Option<String>,
|
||||
pub content: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<ToolCall>>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user