Initial scaffold: GyanDoot v2.0 in Rust
Full Cargo workspace with 11 crates: - gyandoot-core: Shared types, config, error types - gyandoot-agent: Agent loop, tool execution, context compression - gyandoot-providers: LLM provider abstraction (OpenAI, Anthropic, Ollama) - gyandoot-memory: MemPalace persistent memory system - gyandoot-platform: Platform adapters (Telegram, Discord, Webhook, CLI) - gyandoot-security: Credential vault, sanitizer, threat detector - gyandoot-kanban: Task decomposition, swarm orchestration, diagnostics - gyandoot-subagents: Sub-agent spawning and orchestration - gyandoot-skills: Skills discovery, loading, preprocessing, bundles - gyandoot-profile: Multi-agent profile manager - gyandoot-gateway: Gateway sessions, routing, access control Plus: CLI entry point with clap, cross-platform installer script, KANBAN.md, .gnpl/AGENTS.md Workspace compiles clean: cargo check passes with 0 errors.
This commit is contained in:
146
crates/gyandoot-agent/src/tools.rs
Normal file
146
crates/gyandoot-agent/src/tools.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
//! Tool execution subsystem.
|
||||
//!
|
||||
//! Discovers available tools, executes tool calls, and returns results.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use gyandoot_core::{ToolCall, ToolDefinition, ToolResult};
|
||||
use tracing::{debug, error, warn};
|
||||
|
||||
/// Executes tool calls made by the LLM.
|
||||
pub struct ToolExecutor {
|
||||
tools: HashMap<String, Box<dyn Tool + Send + Sync>>,
|
||||
}
|
||||
|
||||
/// Trait that all tools must implement.
|
||||
#[async_trait::async_trait]
|
||||
pub trait Tool: Send + Sync {
|
||||
fn name(&self) -> &str;
|
||||
fn description(&self) -> &str;
|
||||
fn parameters(&self) -> serde_json::Value;
|
||||
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<String>;
|
||||
}
|
||||
|
||||
impl ToolExecutor {
|
||||
/// Create a new executor with no tools loaded.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tools: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a tool implementation.
|
||||
pub fn register(&mut self, tool: Box<dyn Tool + Send + Sync>) {
|
||||
let name = tool.name().to_string();
|
||||
debug!("Registering tool: {}", name);
|
||||
self.tools.insert(name, tool);
|
||||
}
|
||||
|
||||
/// Return all registered tool definitions (for LLM function calling).
|
||||
pub fn definitions(&self) -> Vec<ToolDefinition> {
|
||||
self.tools
|
||||
.values()
|
||||
.map(|t| ToolDefinition {
|
||||
name: t.name().to_string(),
|
||||
description: t.description().to_string(),
|
||||
parameters: t.parameters(),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Execute a single tool call and return the result.
|
||||
pub async fn execute(&self, call: &ToolCall) -> anyhow::Result<ToolResult> {
|
||||
let span = tracing::span!(tracing::Level::INFO, "tool_exec", tool = %call.name);
|
||||
let _guard = span.enter();
|
||||
|
||||
match self.tools.get(&call.name) {
|
||||
Some(tool) => {
|
||||
debug!("Executing tool '{}' with args: {}", call.name, call.arguments);
|
||||
match tool.execute(call.arguments.clone()).await {
|
||||
Ok(output) => {
|
||||
debug!("Tool '{}' succeeded", call.name);
|
||||
Ok(ToolResult {
|
||||
tool_call_id: call.id.clone(),
|
||||
success: true,
|
||||
output,
|
||||
error: None,
|
||||
})
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Tool '{}' failed: {}", call.name, e);
|
||||
Ok(ToolResult {
|
||||
tool_call_id: call.id.clone(),
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(e.to_string()),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
warn!("Tool '{}' not found", call.name);
|
||||
Ok(ToolResult {
|
||||
tool_call_id: call.id.clone(),
|
||||
success: false,
|
||||
output: String::new(),
|
||||
error: Some(format!("Tool '{}' is not available", call.name)),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ToolExecutor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
struct EchoTool;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Tool for EchoTool {
|
||||
fn name(&self) -> &str {
|
||||
"echo"
|
||||
}
|
||||
fn description(&self) -> &str {
|
||||
"Echo back the input"
|
||||
}
|
||||
fn parameters(&self) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {"type": "string"}
|
||||
},
|
||||
"required": ["message"]
|
||||
})
|
||||
}
|
||||
async fn execute(&self, args: serde_json::Value) -> anyhow::Result<String> {
|
||||
Ok(args["message"].as_str().unwrap_or("").to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tool_registration_and_execution() {
|
||||
let mut executor = ToolExecutor::new();
|
||||
executor.register(Box::new(EchoTool));
|
||||
|
||||
let defs = executor.definitions();
|
||||
assert_eq!(defs.len(), 1);
|
||||
assert_eq!(defs[0].name, "echo");
|
||||
|
||||
let call = ToolCall {
|
||||
id: "1".to_string(),
|
||||
name: "echo".to_string(),
|
||||
arguments: serde_json::json!({"message": "hello"}),
|
||||
};
|
||||
|
||||
let result = executor.execute(&call).await.unwrap();
|
||||
assert!(result.success);
|
||||
assert_eq!(result.output, "hello");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user