Source code for agenter.tools
"""Tool abstraction for adding custom tools to the agent.
Uses the same format as Claude Agent SDK's @tool decorator.
Example:
from agenter import tool, ToolResult
@tool("search", "Search the web", {"query": str})
async def search(args: dict) -> ToolResult:
query = args["query"]
results = await do_search(query)
return ToolResult(output=results, success=True)
"""
from __future__ import annotations
import asyncio
import inspect
from typing import TYPE_CHECKING, Protocol, runtime_checkable
import structlog
from .data_models import ToolError, ToolErrorCode, ToolResult
if TYPE_CHECKING:
from collections.abc import Callable
logger = structlog.get_logger(__name__)
[docs]
@runtime_checkable
class Tool(Protocol):
"""Protocol for custom tools.
Custom tools must implement this protocol to be used with the SDK.
Attributes:
name: Unique identifier for the tool.
description: Human-readable description of what the tool does.
input_schema: JSON Schema defining the tool's input parameters.
"""
name: str
description: str
input_schema: dict
[docs]
async def execute(self, inputs: dict) -> ToolResult:
"""Execute the tool with given inputs.
Args:
inputs: Dictionary of input parameters matching input_schema.
Returns:
ToolResult with output and success status.
"""
...
[docs]
class FunctionTool:
"""Tool wrapping a function. Compatible with Claude Agent SDK style.
This class wraps a regular or async function as a Tool that can be
used with the SDK. The wrapped function can return various formats
which will be normalized to ToolResult.
Args:
name: Unique identifier for the tool.
description: Human-readable description of what the tool does.
input_schema: JSON Schema defining the tool's input parameters.
func: The function to wrap (can be sync or async).
Example:
def my_func(inputs: dict) -> str:
return f"Hello, {inputs['name']}!"
tool = FunctionTool(
name="greet",
description="Greet a user",
input_schema={"type": "object", "properties": {"name": {"type": "string"}}},
func=my_func,
)
"""
[docs]
def __init__(
self,
name: str,
description: str,
input_schema: dict,
func: Callable,
) -> None:
self.name = name
self.description = description
self.input_schema = input_schema
self._func = func
[docs]
async def execute(self, inputs: dict) -> ToolResult:
"""Execute the wrapped function.
The function can return:
- ToolResult: Used directly
- tuple[str, bool]: Converted to ToolResult(output, success)
- str: Converted to ToolResult(output=str, success=True)
- dict: JSON-encoded and returned as successful ToolResult
- Any other type: Converted to string
Args:
inputs: Input parameters for the function.
Returns:
ToolResult with the function's output.
"""
try:
if inspect.iscoroutinefunction(self._func):
result = await self._func(inputs)
else:
result = await asyncio.to_thread(self._func, inputs)
# Handle different return formats
if isinstance(result, ToolResult):
return result
elif isinstance(result, str):
return ToolResult(output=result, success=True)
elif isinstance(result, dict):
# MCP-style response
import json
return ToolResult(output=json.dumps(result), success=True)
else:
return ToolResult(output=str(result), success=True)
except Exception as e:
logger.exception("tool_execution_failed", tool_name=self.name)
return ToolResult(
output=f"Error: {e}",
success=False,
error=ToolError(code=ToolErrorCode.EXECUTION_ERROR, message=str(e)),
)
[docs]
def tool(
name: str,
description: str,
input_schema: dict | None = None,
) -> Callable[[Callable], FunctionTool]:
"""Decorator to create a Tool from a function.
Compatible with Claude Agent SDK style:
@tool("greet", "Greet a user", {"name": str})
async def greet(args):
return f"Hello, {args['name']}!"
Or with JSON schema:
@tool(
name="search",
description="Search the web",
input_schema={"type": "object", "properties": {"query": {"type": "string"}}}
)
async def search(inputs):
return results, True
"""
def decorator(func: Callable) -> FunctionTool:
# Convert simple type hints to JSON schema if needed
schema = input_schema or {}
if schema and not schema.get("type"):
# Convert {"name": str} to JSON schema
schema = _types_to_schema(schema)
return FunctionTool(
name=name,
description=description,
input_schema=schema,
func=func,
)
return decorator
def _types_to_schema(types: dict) -> dict:
"""Convert Python types to JSON schema using Pydantic.
Supports all Pydantic-compatible types (Optional, Union, Enum, etc.).
"""
from pydantic import create_model
# Create fields definition for create_model
# usage: name=(type, default) or (type, Field(...))
# We assume all fields in the dict are required (no default)
fields = {k: (v, ...) for k, v in types.items()}
# Create dynamic model
model = create_model("ToolInput", **fields) # type: ignore[call-overload]
# Generate schema
schema = model.model_json_schema()
# Clean up schema (remove title/description of the container)
schema.pop("title", None)
schema.pop("description", None)
return schema