Creating Tools
This guide covers how to create custom tools for your agents.
Tool Anatomy
A Tool consists of four parts:
| Component | Type | Description |
|---|---|---|
name |
str |
Unique identifier for the tool |
description |
str |
What the tool does (shown to the LLM) |
parameters |
type[BaseModel] |
Pydantic model defining input schema |
executor |
Callable |
Function that executes the tool |
Basic Example
from pydantic import BaseModel, Field
from stirrup import Tool, ToolResult, ToolUseCountMetadata
class GreetParams(BaseModel):
"""Parameters for the greet tool."""
name: str = Field(description="Name of the person to greet")
formal: bool = Field(default=False, description="Use formal greeting")
def greet(params: GreetParams) -> ToolResult[ToolUseCountMetadata]:
if params.formal:
greeting = f"Good day, {params.name}."
else:
greeting = f"Hey {params.name}!"
return ToolResult(
content=greeting,
metadata=ToolUseCountMetadata(),
)
GREET_TOOL = Tool(
name="greet",
description="Greet someone by name",
parameters=GreetParams,
executor=greet,
)
Parameter Schemas
Use Pydantic models with Field descriptions to define tool parameters. The descriptions are included in the tool schema sent to the LLM.
Required vs Optional Parameters
class SearchParams(BaseModel):
query: str = Field(description="Search query") # Required
max_results: int = Field(default=10, description="Max results") # Optional
include_images: bool = Field(default=False, description="Include images")
Complex Types
from typing import Literal
class AnalyzeParams(BaseModel):
text: str = Field(description="Text to analyze")
language: Literal["en", "es", "fr"] = Field(description="Language code")
options: list[str] = Field(default_factory=list, description="Analysis options")
Annotated Types
from typing import Annotated
class CalculateParams(BaseModel):
expression: Annotated[str, Field(description="Mathematical expression")]
precision: Annotated[int, Field(default=2, ge=0, le=10, description="Decimal places")]
Sync vs Async Executors
Tools can use either synchronous or asynchronous executors:
Synchronous
def my_tool(params: MyParams) -> ToolResult[ToolUseCountMetadata]:
result = do_something(params)
return ToolResult(content=result, metadata=ToolUseCountMetadata())
Asynchronous
async def my_async_tool(params: MyParams) -> ToolResult[ToolUseCountMetadata]:
result = await do_something_async(params)
return ToolResult(content=result, metadata=ToolUseCountMetadata())
By default, synchronous executors run in a separate thread (run_sync_in_thread=True).
Tool Results
Tools return ToolResult[M] where M is the metadata type:
from stirrup import ToolResult, ToolUseCountMetadata
# Simple text result
return ToolResult(
content="Operation completed successfully",
metadata=ToolUseCountMetadata(),
)
# Result with structured content
return ToolResult(
content=f"Found {len(results)} items:\n" + "\n".join(results),
metadata=ToolUseCountMetadata(),
)
Returning Images
from stirrup import ImageContentBlock
async def screenshot_tool(params: ScreenshotParams) -> ToolResult[ToolUseCountMetadata]:
image_bytes = await take_screenshot()
return ToolResult(
content=[
"Here's the screenshot:",
ImageContentBlock(data=image_bytes),
],
metadata=ToolUseCountMetadata(),
)
Tool Metadata
Metadata is aggregated across tool calls in a run. Use it to track usage statistics.
Built-in Metadata
from stirrup import ToolUseCountMetadata
# Tracks number of times tool was called
return ToolResult(content="done", metadata=ToolUseCountMetadata())
Custom Metadata
Create custom metadata by implementing the Addable protocol:
from stirrup import Addable
from pydantic import BaseModel
class APICallMetadata(BaseModel, Addable):
"""Track API calls and costs."""
calls: int = 1
tokens_used: int = 0
cost_usd: float = 0.0
def __add__(self, other: "APICallMetadata") -> "APICallMetadata":
return APICallMetadata(
calls=self.calls + other.calls,
tokens_used=self.tokens_used + other.tokens_used,
cost_usd=self.cost_usd + other.cost_usd,
)
async def api_tool(params: APIParams) -> ToolResult[APICallMetadata]:
response = await call_api(params)
return ToolResult(
content=response.text,
metadata=APICallMetadata(
tokens_used=response.tokens,
cost_usd=response.cost,
),
)
Accessing Aggregated Metadata
from stirrup import aggregate_metadata
finish_params, history, metadata = await session.run("task")
# metadata is dict[str, list[Any]] - tool_name -> list of metadata objects
aggregated = aggregate_metadata(metadata)
# Access aggregated values
print(f"API calls: {aggregated['api_tool'].calls}")
print(f"Total cost: ${aggregated['api_tool'].cost_usd:.2f}")
Using Tools with Agents
Adding to Default Tools
from stirrup import Agent
from stirrup.clients.chat_completions_client import ChatCompletionsClient
from stirrup.tools import DEFAULT_TOOLS
client = ChatCompletionsClient(model="gpt-5")
agent = Agent(
client=client,
name="my_agent",
tools=[*DEFAULT_TOOLS, GREET_TOOL, MY_OTHER_TOOL],
)
Replacing Default Tools
from stirrup import Agent
from stirrup.clients.chat_completions_client import ChatCompletionsClient
from stirrup.tools import CALCULATOR_TOOL
client = ChatCompletionsClient(model="gpt-5")
agent = Agent(
client=client,
name="custom_agent",
tools=[GREET_TOOL, CALCULATOR_TOOL], # Only these tools available
)
Next Steps
- Tool Providers - Tools with lifecycle management
- Code Execution - Execution backends
- Extending Tools - Advanced tool patterns