Skip to content

Tool Interfaces

Overview

A good tool interface is like a well-designed API—clear, predictable, and impossible to misuse.

LLMs need tools with explicit contracts they can understand and follow reliably.


The Problem

Without Good Design

# BAD
def tool_1(x):
 """Do something with x"""
 return result

# LLM doesn't know:
# - What type of x?
# - What does "something" mean?
# - What type is result?
# - When does it fail?
# - What are side effects?

With Good Design

# GOOD
@agent_tool
def search_database(
 query: str, # What we're searching for (1-500 chars)
 table: str, # Which table ("users", "products", "orders")
 limit: int = 10 # Max results (1-100, default 10)
) -> dict:
 """
 Search database table for matching records.

 Returns:
 {
 "success": bool,
 "results": List[dict], # Matching records
 "count": int, # Number of results
 "query_time_ms": float
 }
 """

5 Principles of Good Tool Design

1. Explicit Contracts

# Specify everything
@tool(
 name="calculate",
 description="Perform arithmetic on two numbers",
 required_params=["a", "b", "operation"],
 optional_params=["precision"]
)
def calculate(
 a: float, # First number
 b: float, # Second number
 operation: Literal["add", "subtract", "multiply", "divide"], # Operation type
 precision: int = 2 # Decimal places (0-10)
) -> dict:
 """Calculate result with specified precision"""
...

2. Clear Error Handling

def tool_with_errors():
 """
 Raises:
 - ValueError: If arguments are invalid
 - TimeoutError: If operation takes > 30 seconds
 - PermissionError: If user lacks access

 Returns: {...} on success
 """
...

3. Meaningful Defaults

# GOOD
def search(
 query: str,
 max_results: int = 10, # Reasonable default
 timeout_sec: int = 30
):
...

# BAD
def search(query: str, max_results: int = 999999):
...

4. Consistent Responses

# GOOD
{
 "success": bool,
 "data": Any, # Actual result
 "error": Optional[str], # Error message if failed
 "metadata": {
 "execution_time_ms": float,
 "timestamp": str
 }
}

# BAD
# Sometimes returns string, sometimes dict, sometimes list

5. Comprehensive Examples

@tool
def send_email(
 to: str, # Email address
 subject: str, # Email subject
 body: str, # Email body (markdown)
 cc: Optional[List[str]] = None # CC recipients
) -> dict:
 """
 Send an email message.

 Examples:

 Example 1: Simple email
 send_email(
 to="user@example.com",
 subject="Hello",
 body="Hi there!"
)

 Example 2: With CC
 send_email(
 to="manager@example.com",
 subject="Report",
 body="Q3 Results:\n- Revenue: $50K\n- Growth: 15%",
 cc=["cto@example.com"]
)
 """

Tool Interface Patterns

Pattern 1: Dictionary-Based Tools

tools_registry = {
 "calculate": {
 "description": "Perform arithmetic",
 "function": calculate_func,
 "params": {
 "a": {"type": "float", "required": True},
 "b": {"type": "float", "required": True},
 "op": {"type": "string", "enum": ["add", "subtract", "multiply"]}
 },
 "returns": {"type": "dict", "properties": {...}}
 },
 "search": {
 "description": "Search documents",
 "function": search_func,
 "params": {...}
 }
}

Pattern 2: Class-Based Tools

class Tool(ABC):
 @property
 def name(self) -> str:
 """Tool name"""

 @property
 def description(self) -> str:
 """What it does"""

 @property
 def parameters(self) -> dict:
 """Parameter schema"""

 @abstractmethod
 def call(self, **kwargs) -> dict:
 """Execute the tool"""

class CalculatorTool(Tool):
 @property
 def name(self):
 return "calculator"

 @property
 def description(self):
 return "Perform arithmetic operations"

 @property
 def parameters(self):
 return {
 "a": {"type": "number"},
 "b": {"type": "number"},
 "operation": {"type": "string", "enum": ["add", "subtract"]}
 }

 def call(self, a: float, b: float, operation: str) -> dict:
 if operation == "add":
 result = a + b
 elif operation == "subtract":
 result = a - b
 else:
 return {"error": f"Unknown operation: {operation}"}

 return {"success": True, "result": result}

Pattern 3: Decorator-Based Tools

@register_tool(
 description="Send email notifications",
 examples=[
 {"input": {"to": "user@example.com", "message": "Hello"},
 "output": {"success": True}}
]
)
def send_notification(
 to: str, # Recipient email
 message: str, # Message to send (max 1000 chars)
 priority: str = "normal" # "low", "normal", "high"
) -> dict:
 """Send email notification to user"""
...

-

Tool Metadata Specification

TOOL_SPEC = {
 # Identification
 "name": "tool_name", # Unique identifier
 "version": "1.0.0", # Semantic versioning

 # Description
 "description": "What this tool does", # Brief (1 sentence)
 "long_description": "...", # Detailed (2-3 sentences)
 "category": "communication", # categorization

 # Parameters
 "parameters": {
 "type": "object",
 "properties": {
 "param_name": {
 "type": "string", # string, number, boolean, array, object
 "description": "What this parameter means",
 "examples": ["example1", "example2"],
 "constraints": {
 "minLength": 1,
 "maxLength": 500,
 "pattern": "^[a-z]+$"
 }
 }
 },
 "required": ["required_param"],
 "optional": ["optional_param"]
 },

 # Return value
 "returns": {
 "type": "object",
 "description": "What the tool returns on success",
 "properties": {
 "success": {"type": "boolean"},
 "data": {"type": "object"},
 "error": {"type": "string"}
 }
 },

 # Error handling
 "errors": [
 {"code": "INVALID_INPUT", "description": "Input validation failed"},
 {"code": "TIMEOUT", "description": "Operation took too long"},
 {"code": "PERMISSION_DENIED", "description": "User lacks permission"}
],

 # Usage
 "examples": [
 {
 "description": "Simple example",
 "input": {"param1": "value1"},
 "output": {"success": True, "data": {...}}
 }
],

 # Constraints
 "constraints": {
 "rate_limit": "100 calls/minute",
 "timeout_sec": 30,
 "requires_auth": True,
 "costs_tokens": True
 },

 # Integration
 "dependencies": ["database", "auth"],
 "side_effects": ["Sends email", "Updates database"],
 "idempotent": False
}

Tool Abstraction Layer

class ToolInterface:
 """Unified interface for all tools"""

 def validate_inputs(self, inputs: dict) -> tuple[bool, str]:
 """Validate inputs match schema"""
 try:
 jsonschema.validate(inputs, self.schema["parameters"])
 return True, ""
 except jsonschema.ValidationError as e:
 return False, f"Invalid input: {e.message}"

 def call(self, **inputs) -> dict:
 """Execute tool with validation"""
 valid, error = self.validate_inputs(inputs)
 if not valid:
 return {"success": False, "error": error}

 try:
 result = self._execute(**inputs)
 return {"success": True, "data": result}
 except Exception as e:
 return {"success": False, "error": str(e)}

 def to_openai_schema(self) -> dict:
 """Convert to OpenAI function calling format"""
 return {
 "type": "function",
 "function": {
 "name": self.name,
 "description": self.description,
 "parameters": self.schema["parameters"]
 }
 }

 def to_claude_schema(self) -> dict:
 """Convert to Claude tool_use format"""
 return {
 "name": self.name,
 "description": self.description,
 "input_schema": self.schema["parameters"]
 }

Tool Registry

class ToolRegistry:
 """Manage collection of tools"""

 def __init__(self):
 self.tools = {}
 self.categories = {}

 def register(self, tool: Tool) -> None:
 """Register a new tool"""
 if tool.name in self.tools:
 raise ValueError(f"Tool {tool.name} already registered")

 self.tools[tool.name] = tool

 # Index by category
 category = tool.metadata.get("category")
 if category not in self.categories:
 self.categories[category] = []
 self.categories[category].append(tool.name)

 def get_tool(self, name: str) -> Tool:
 """Get tool by name"""
 if name not in self.tools:
 raise ValueError(f"Tool {name} not found")
 return self.tools[name]

 def get_tools_by_category(self, category: str) -> List[Tool]:
 """Get all tools in category"""
 names = self.categories.get(category, [])
 return [self.tools[name] for name in names]

 def list_tools(self) -> dict:
 """List all available tools with descriptions"""
 return {
 name: tool.description 
 for name, tool in self.tools.items()
 }

3 Tool Interface Warnings

Warning 1: Ambiguous Descriptions

# WRONG
@tool(description="Process data")
def process_data(x):
...

# LLM doesn't know:
# - What type of data?
# - How is it processed?
# - What's returned?

# RIGHT
@tool(
 description="Clean and validate user input data (names, emails, etc) "
 "by removing whitespace, checking format, and returning "
 "standardized version"
)
def validate_user_data(
 data: str, # User input (name or email)
 data_type: Literal["name", "email"] # What kind of data
) -> dict:
 """Returns {'valid': bool, 'cleaned': str, 'error': str}"""
...

Key Lesson: Be specific. LLMs work better with detailed descriptions.

Warning 2: Missing Examples

# WRONG
@tool(description="Send message")
def send_message(to, message):
...

# LLM doesn't know:
# - Email or phone number?
# - How long can message be?
# - What format for recipient?

# RIGHT
@tool(
 description="Send message via email or SMS",
 examples=[
 {
 "description": "Send email",
 "input": {"to": "user@example.com", "message": "Hi!"},
 "output": {"success": True, "message_id": "msg_123"}
 },
 {
 "description": "Send SMS",
 "input": {"to": "+1234567890", "message": "Hello"},
 "output": {"success": True, "message_id": "msg_124"}
 }
]
)
def send_message(
 to: str, # Email address or phone number
 message: str # Message content (max 500 chars)
) -> dict:
...

Key Lesson: Examples prevent misuse and improve accuracy.

Warning 3: Leaky Abstractions

# WRONG
@tool
def query_database(
 sql: str, # Raw SQL query (DANGER!)
 connection_pool_size: int = 10
) -> List[dict]:
...

# Problems:
# - LLM can write malicious SQL
# - Expose internal connection details
# - Breaks if schema changes

# RIGHT
@tool
def search_users(
 search_term: str, # Name or email to search
 limit: int = 10 # Max results
) -> dict:
 """
 Search for users by name or email.

 Returns: {'users': [{'id': str, 'name': str, 'email': str}]}
 """
 # Internal: construct safe SQL
 # SELECT * FROM users WHERE name LIKE ? OR email LIKE ? LIMIT ?
...

Key Lesson: Hide implementation details. Only expose what's necessary.


Best Practices

1. Version Tools

@tool(version="1.2.0")
def my_tool():
 """Track breaking changes"""

# Deprecation path:
# v1.0
# v1.1
# v1.2
# v2.0

2. Document Rate Limits

@tool(
 constraints={
 "rate_limit": "100 calls per minute",
 "quota_per_day": "10,000 calls"
 }
)
def expensive_tool():
 """Tool with usage constraints"""
...

3. Enable Tool Discovery

def list_available_tools() -> dict:
 """Return tools agent can use"""

 return {
 tool.name: {
 "description": tool.description,
 "parameters": tool.parameters,
 "category": tool.category,
 "rate_limit": tool.rate_limit
 }
 for tool in registry.list_all()
 }

Key Takeaways

  1. Good interfaces are explicit - LLMs need clear contracts
  2. Examples matter - Show, don't just tell
  3. Hide complexity - Expose only what's necessary
  4. Validate inputs - Catch errors early
  5. Consistent responses - Predictable format
  6. Document thoroughly - Every parameter, return value, error
  7. Version your tools - Track changes over time

-

Next Steps

-

Last Updated: August 9, 2026