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:
22
crates/gyandoot-agent/Cargo.toml
Normal file
22
crates/gyandoot-agent/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "gyandoot-agent"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
description = "GyanDoot v2 — Core agent loop, tool execution, context compression"
|
||||
|
||||
[dependencies]
|
||||
gyandoot-core = { path = "../gyandoot-core" }
|
||||
gyandoot-providers = { path = "../gyandoot-providers" }
|
||||
gyandoot-memory = { path = "../gyandoot-memory" }
|
||||
gyandoot-security = { path = "../gyandoot-security" }
|
||||
gyandoot-skills = { path = "../gyandoot-skills" }
|
||||
tokio.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror.workspace = true
|
||||
anyhow.workspace = true
|
||||
tracing.workspace = true
|
||||
uuid.workspace = true
|
||||
chrono.workspace = true
|
||||
async-trait.workspace = true
|
||||
263
crates/gyandoot-agent/src/agent_loop.rs
Normal file
263
crates/gyandoot-agent/src/agent_loop.rs
Normal file
@@ -0,0 +1,263 @@
|
||||
//! The core agent inference loop.
|
||||
//!
|
||||
//! The `AgentLoop` manages the cycle: receive message → build context →
|
||||
//! call LLM → process tool calls → repeat or respond.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use gyandoot_core::{
|
||||
AgentConfig, AgentStatus, ConversationContext, Message, MessageRole, ToolCall, ToolResult,
|
||||
};
|
||||
use gyandoot_memory::palace::MemPalace;
|
||||
use gyandoot_providers::Provider;
|
||||
use gyandoot_security::sanitizer::Sanitizer;
|
||||
use gyandoot_skills::SkillsManager;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, error, info, span, Level};
|
||||
|
||||
use crate::context::ContextCompressor;
|
||||
use crate::tools::ToolExecutor;
|
||||
|
||||
/// The main agent execution loop.
|
||||
pub struct AgentLoop {
|
||||
config: AgentConfig,
|
||||
provider: Box<dyn Provider + Send + Sync>,
|
||||
tool_executor: ToolExecutor,
|
||||
context_compressor: ContextCompressor,
|
||||
memory: Arc<Mutex<MemPalace>>,
|
||||
skills_manager: SkillsManager,
|
||||
sanitizer: Sanitizer,
|
||||
status: Arc<Mutex<AgentStatus>>,
|
||||
}
|
||||
|
||||
impl AgentLoop {
|
||||
/// Create a new agent loop with the given configuration.
|
||||
///
|
||||
/// This initializes the LLM provider, tool executor, memory system, and
|
||||
/// skills manager based on the provided config.
|
||||
pub fn new(config: AgentConfig) -> Self {
|
||||
let provider = create_provider(&config);
|
||||
let tool_executor = ToolExecutor::new();
|
||||
let context_compressor = ContextCompressor::new();
|
||||
let memory = Arc::new(Mutex::new(MemPalace::new()));
|
||||
let skills_manager = SkillsManager::new();
|
||||
let sanitizer = Sanitizer::new();
|
||||
|
||||
Self {
|
||||
config,
|
||||
provider,
|
||||
tool_executor,
|
||||
context_compressor,
|
||||
memory,
|
||||
skills_manager,
|
||||
sanitizer,
|
||||
status: Arc::new(Mutex::new(AgentStatus::Idle)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the agent loop interactively, reading from stdin and writing to stdout.
|
||||
///
|
||||
/// This is the primary entry point for CLI usage. The loop reads user input,
|
||||
/// calls the LLM, executes any requested tools, and continues until the
|
||||
/// user exits or an error occurs.
|
||||
pub async fn run(&self) -> anyhow::Result<()> {
|
||||
let span = span!(Level::INFO, "agent_loop", agent = %self.config.name);
|
||||
let _guard = span.enter();
|
||||
|
||||
info!("Agent loop started");
|
||||
|
||||
let mut messages: Vec<Message> = vec![
|
||||
Message::new(MessageRole::System, &self.config.system_prompt),
|
||||
];
|
||||
|
||||
loop {
|
||||
// Read user input
|
||||
let user_input = self.read_input().await?;
|
||||
|
||||
// Check for exit conditions
|
||||
if self.should_exit(&user_input) {
|
||||
info!("User requested exit");
|
||||
break;
|
||||
}
|
||||
|
||||
// Sanitize input
|
||||
let clean_input = self.sanitizer.sanitize(&user_input);
|
||||
|
||||
// Add user message
|
||||
messages.push(Message::new(MessageRole::User, clean_input));
|
||||
|
||||
// Build conversation context
|
||||
let context = ConversationContext {
|
||||
messages: messages.clone(),
|
||||
system_prompt: self.config.system_prompt.clone(),
|
||||
tools: self.tool_executor.definitions(),
|
||||
max_tokens: self.config.max_tokens,
|
||||
};
|
||||
|
||||
// Call the LLM
|
||||
*self.status.lock().await = AgentStatus::Thinking;
|
||||
let response = self.provider.chat(context).await?;
|
||||
|
||||
// Process assistant response
|
||||
messages.push(Message::new(MessageRole::Assistant, &response.content));
|
||||
|
||||
// Handle tool calls
|
||||
if let Some(tool_calls) = response.tool_calls {
|
||||
for tool_call in tool_calls {
|
||||
debug!("Executing tool: {}", tool_call.name);
|
||||
let result = self.tool_executor.execute(&tool_call).await?;
|
||||
messages.push(
|
||||
Message::new(MessageRole::Tool, &result.output)
|
||||
.with_tool_result(result.output.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
// Call LLM again with tool results
|
||||
let context = ConversationContext {
|
||||
messages: messages.clone(),
|
||||
system_prompt: self.config.system_prompt.clone(),
|
||||
tools: self.tool_executor.definitions(),
|
||||
max_tokens: self.config.max_tokens,
|
||||
};
|
||||
|
||||
let follow_up = self.provider.chat(context).await?;
|
||||
messages.push(Message::new(MessageRole::Assistant, &follow_up.content));
|
||||
self.display_output(&follow_up.content);
|
||||
} else {
|
||||
self.display_output(&response.content);
|
||||
}
|
||||
|
||||
// Store in memory
|
||||
{
|
||||
let mut memory = self.memory.lock().await;
|
||||
memory.store_conversation_turn(&messages).await?;
|
||||
}
|
||||
|
||||
// Compress context if too large
|
||||
if self.should_compress(&messages) {
|
||||
debug!("Context size threshold reached, compressing");
|
||||
messages = self.context_compressor.compress(messages).await;
|
||||
}
|
||||
|
||||
*self.status.lock().await = AgentStatus::Idle;
|
||||
}
|
||||
|
||||
info!("Agent loop finished");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Read input from stdin (CLI mode).
|
||||
async fn read_input(&self) -> anyhow::Result<String> {
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
let mut stdout = tokio::io::stdout();
|
||||
use tokio::io::AsyncWriteExt;
|
||||
stdout.write_all(b"\n> ").await?;
|
||||
stdout.flush().await?;
|
||||
|
||||
let stdin = tokio::io::stdin();
|
||||
let reader = tokio::io::BufReader::new(stdin);
|
||||
let mut lines = reader.lines();
|
||||
let mut input = String::new();
|
||||
|
||||
if let Some(line) = lines.next_line().await? {
|
||||
input = line;
|
||||
}
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
/// Check if the input indicates the user wants to exit.
|
||||
fn should_exit(&self, input: &str) -> bool {
|
||||
matches!(input.trim().to_lowercase().as_str(), "exit" | "quit" | "/exit" | "/quit")
|
||||
}
|
||||
|
||||
/// Display output to the user.
|
||||
fn display_output(&self, output: &str) {
|
||||
println!("\n{}", output);
|
||||
}
|
||||
|
||||
/// Determine if context compression is needed based on message count and token estimate.
|
||||
fn should_compress(&self, messages: &[Message]) -> bool {
|
||||
// Rough estimate: ~4 chars per token, compress when over 75% of max
|
||||
let estimated_tokens: usize = messages.iter().map(|m| m.content.len() / 4).sum();
|
||||
estimated_tokens > (self.config.max_tokens * 3 / 4)
|
||||
}
|
||||
|
||||
/// Get the current agent status.
|
||||
pub async fn get_status(&self) -> AgentStatus {
|
||||
self.status.lock().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create the appropriate LLM provider based on configuration.
|
||||
fn create_provider(config: &AgentConfig) -> Box<dyn Provider + Send + Sync> {
|
||||
match config.provider.default.as_str() {
|
||||
"openai" => {
|
||||
let openai_config = config
|
||||
.provider
|
||||
.openai
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(gyandoot_providers::openai::OpenAIProvider::new(
|
||||
openai_config.api_key.clone(),
|
||||
openai_config.model.clone(),
|
||||
openai_config.base_url.clone(),
|
||||
))
|
||||
}
|
||||
"anthropic" => {
|
||||
let anthropic_config = config
|
||||
.provider
|
||||
.anthropic
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(gyandoot_providers::anthropic::AnthropicProvider::new(
|
||||
anthropic_config.api_key.clone(),
|
||||
anthropic_config.model.clone(),
|
||||
))
|
||||
}
|
||||
"ollama" => {
|
||||
let ollama_config = config
|
||||
.provider
|
||||
.ollama
|
||||
.clone()
|
||||
.unwrap_or_default();
|
||||
Box::new(gyandoot_providers::ollama::OllamaProvider::new(
|
||||
ollama_config.base_url.clone(),
|
||||
ollama_config.model.clone(),
|
||||
))
|
||||
}
|
||||
other => {
|
||||
tracing::warn!("Unknown provider '{}', falling back to OpenAI", other);
|
||||
Box::new(gyandoot_providers::openai::OpenAIProvider::new(
|
||||
String::new(),
|
||||
config.model.clone(),
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_should_exit() {
|
||||
let config = AgentConfig::default();
|
||||
let provider = gyandoot_providers::openai::OpenAIProvider::new(
|
||||
"test".to_string(),
|
||||
"gpt-4o".to_string(),
|
||||
None,
|
||||
);
|
||||
let _agent = AgentLoop {
|
||||
config,
|
||||
provider: Box::new(provider),
|
||||
tool_executor: ToolExecutor::new(),
|
||||
context_compressor: ContextCompressor::new(),
|
||||
memory: Arc::new(Mutex::new(MemPalace::new())),
|
||||
skills_manager: SkillsManager::new(&gyandoot_core::SkillsConfig::default()),
|
||||
sanitizer: Sanitizer::new(),
|
||||
status: Arc::new(Mutex::new(AgentStatus::Idle)),
|
||||
};
|
||||
}
|
||||
}
|
||||
162
crates/gyandoot-agent/src/context.rs
Normal file
162
crates/gyandoot-agent/src/context.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
//! Context compression logic.
|
||||
//!
|
||||
//! When the agent's conversation context grows too large, this module
|
||||
//! intelligently compresses or summarizes older messages while preserving
|
||||
//! critical information.
|
||||
|
||||
use gyandoot_core::Message;
|
||||
use tracing::debug;
|
||||
|
||||
/// Compresses conversation context when it exceeds size thresholds.
|
||||
pub struct ContextCompressor {
|
||||
max_messages_before_compress: usize,
|
||||
}
|
||||
|
||||
impl ContextCompressor {
|
||||
/// Create a new context compressor.
|
||||
///
|
||||
/// By default, compression triggers when there are more than 30 messages
|
||||
/// (roughly corresponding to 15 turns of conversation).
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
max_messages_before_compress: 30,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a compressor with a custom message threshold.
|
||||
pub fn with_threshold(max_messages: usize) -> Self {
|
||||
Self {
|
||||
max_messages_before_compress: max_messages,
|
||||
}
|
||||
}
|
||||
|
||||
/// Compress the message list by summarizing older messages.
|
||||
///
|
||||
/// Strategy:
|
||||
/// - Keep the system prompt (first message).
|
||||
/// - Keep the most recent N messages (recent context).
|
||||
/// - Summarize the messages in between into a single condensed message.
|
||||
/// - Preserve all tool call/result pairs.
|
||||
pub async fn compress(&self, messages: Vec<Message>) -> Vec<Message> {
|
||||
if messages.len() <= self.max_messages_before_compress {
|
||||
return messages;
|
||||
}
|
||||
|
||||
debug!(
|
||||
"Compressing {} messages (threshold: {})",
|
||||
messages.len(),
|
||||
self.max_messages_before_compress
|
||||
);
|
||||
|
||||
let mut compressed: Vec<Message> = Vec::with_capacity(self.max_messages_before_compress);
|
||||
|
||||
// Always keep the system prompt
|
||||
if let Some(system) = messages.first() {
|
||||
compressed.push(system.clone());
|
||||
}
|
||||
|
||||
// Keep the most recent N-2 messages (reserve space for summary)
|
||||
let recent_count = (self.max_messages_before_compress / 2).max(2);
|
||||
let keep_start = messages.len().saturating_sub(recent_count);
|
||||
|
||||
// Summarize middle messages
|
||||
let middle: Vec<&Message> = messages[1..keep_start]
|
||||
.iter()
|
||||
.filter(|m| m.role != gyandoot_core::MessageRole::Tool)
|
||||
.collect();
|
||||
|
||||
if !middle.is_empty() {
|
||||
let summary = self.summarize_messages(&middle);
|
||||
compressed.push(Message::new(
|
||||
gyandoot_core::MessageRole::System,
|
||||
format!("[Summarized context from previous conversation: {}]", summary),
|
||||
));
|
||||
}
|
||||
|
||||
// Append the most recent messages
|
||||
for msg in messages.iter().skip(keep_start) {
|
||||
compressed.push(msg.clone());
|
||||
}
|
||||
|
||||
debug!("Compressed to {} messages", compressed.len());
|
||||
compressed
|
||||
}
|
||||
|
||||
/// Create a text summary from a slice of messages.
|
||||
fn summarize_messages(&self, messages: &[&Message]) -> String {
|
||||
let count = messages.len();
|
||||
let user_msgs: Vec<&str> = messages
|
||||
.iter()
|
||||
.filter(|m| m.role == gyandoot_core::MessageRole::User)
|
||||
.map(|m| m.content.as_str())
|
||||
.collect();
|
||||
|
||||
if user_msgs.is_empty() {
|
||||
format!("{} previous assistant messages", count)
|
||||
} else if user_msgs.len() <= 3 {
|
||||
format!(
|
||||
"Earlier discussion: {}",
|
||||
user_msgs.join(" | ")
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{} previous turns covering topics: {}, ... (and {} more)",
|
||||
count,
|
||||
user_msgs[..3].join(", "),
|
||||
user_msgs.len().saturating_sub(3)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContextCompressor {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gyandoot_core::MessageRole;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_compress_below_threshold() {
|
||||
let compressor = ContextCompressor::with_threshold(10);
|
||||
let messages: Vec<Message> = (0..5)
|
||||
.map(|i| Message::new(MessageRole::User, format!("message {}", i)))
|
||||
.collect();
|
||||
|
||||
let result = compressor.compress(messages.clone()).await;
|
||||
assert_eq!(result.len(), 5);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_compress_above_threshold() {
|
||||
let compressor = ContextCompressor::with_threshold(10);
|
||||
let mut messages = vec![Message::new(MessageRole::System, "You are an AI.")];
|
||||
for i in 0..20 {
|
||||
messages.push(Message::new(MessageRole::User, format!("user message {}", i)));
|
||||
messages.push(Message::new(MessageRole::Assistant, format!("response {}", i)));
|
||||
}
|
||||
|
||||
let result = compressor.compress(messages).await;
|
||||
assert!(result.len() < 20);
|
||||
assert_eq!(result[0].content, "You are an AI.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summarize_empty() {
|
||||
let compressor = ContextCompressor::new();
|
||||
let summary = compressor.summarize_messages(&[]);
|
||||
assert!(summary.contains("0"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_summarize_few() {
|
||||
let compressor = ContextCompressor::new();
|
||||
let msg = Message::new(MessageRole::User, "hello");
|
||||
let summary = compressor.summarize_messages(&[&msg]);
|
||||
assert!(summary.contains("hello"));
|
||||
}
|
||||
}
|
||||
13
crates/gyandoot-agent/src/lib.rs
Normal file
13
crates/gyandoot-agent/src/lib.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
//! GyanDoot Agent — Core agent loop, tool execution, context compression.
|
||||
//!
|
||||
//! This crate implements the main agent inference loop: receiving messages,
|
||||
//! invoking LLM providers, executing tool calls, compressing context when
|
||||
//! needed, and interacting with memory.
|
||||
|
||||
pub mod agent_loop;
|
||||
pub mod tools;
|
||||
pub mod context;
|
||||
|
||||
pub use agent_loop::AgentLoop;
|
||||
pub use tools::ToolExecutor;
|
||||
pub use context::ContextCompressor;
|
||||
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