3.6 KiB
Systems Design Guide
This document describes the design and implementation of the Systems framework in Goose, which enables AI agents to interact with different systems through a unified tool-based interface.
Core Concepts
System
A System represents any component that can be operated by an AI agent. Systems expose their capabilities through Tools and maintain their own state. The core interface is defined by the System trait:
#[async_trait]
pub trait System: Send + Sync {
fn name(&self) -> &str;
fn description(&self) -> &str;
fn instructions(&self) -> &str;
fn tools(&self) -> &[Tool];
async fn status(&self) -> AnyhowResult<HashMap<String, Value>>;
async fn call_tool(&self, tool_name: &str, parameters: HashMap<String, Value>) -> ToolResult<Value>;
}
Tools
Tools are the primary way Systems expose functionality to agents. Each tool has:
- A name
- A description
- A set of parameters
- An implementation that executes the tool's functionality
A tool must take a Value and return an AgentResult (it must also be async). This is what makes it compatible with the tool calling framework from the agent.
async fn echo(&self, params: Value) -> AgentResult<Value>
Architecture
Component Overview
- System Trait: The core interface that all systems must implement
- Error Handling: Specialized error types for tool execution
- Proc Macros: Simplify tool definition and registration [not yet implemented]
Error Handling
The system uses two main error types:
ToolError: Specific errors related to tool executionanyhow::Error: General purpose errors for system status and other operations
This split allows precise error handling for tool execution while maintaining flexibility for general system operations.
Best Practices
Tool Design
- Clear Names: Use clear, action-oriented names for tools (e.g., "create_user" not "user")
- Descriptive Parameters: Each parameter should have a clear description
- Error Handling: Return specific errors when possible, the errors become "prompts"
- State Management: Be explicit about state modifications
System Implementation
- State Encapsulation: Keep system state private and controlled
- Error Propagation: Use
?operator withToolErrorfor tool execution - Status Clarity: Provide clear, structured status information
- Documentation: Document all tools and their effects
Example Implementation
Here's a complete example of a simple system:
use goose_macros::tool;
struct FileSystem {
registry: ToolRegistry,
root_path: PathBuf,
}
impl FileSystem {
#[tool(
name = "read_file",
description = "Read contents of a file"
)]
async fn read_file(&self, path: String) -> ToolResult<Value> {
let full_path = self.root_path.join(path);
let content = tokio::fs::read_to_string(full_path)
.await
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
Ok(json!({ "content": content }))
}
}
#[async_trait]
impl System for FileSystem {
// ... implement trait methods ...
}
Testing
Systems should be tested at multiple levels:
- Unit tests for individual tools
- Integration tests for system behavior
- Property tests for tool invariants
Example test:
#[tokio::test]
async fn test_echo_tool() {
let system = TestSystem::new();
let result = system.call_tool(
"echo",
hashmap!{ "message" => json!("hello") }
).await;
assert_eq!(result.unwrap(), json!({ "response": "hello" }));
}