AI Tools Executor
An executor layer between AI agents and tools. The agent gets only 3 meta-tools — tools are discovered on-demand and invoked via Python function call syntax, not JSON.
The Problem
Every agent framework dumps all tool schemas into the LLM context on every turn. With 50 tools at ~500 tokens each, that's 25,000 tokens wasted — on tools that will never be used.
The Solution
The agent sees exactly 3 meta-tools:
| Meta-Tool | Purpose |
|---|---|
search_tools(query) |
Discover available tools |
execute(calls) |
Run one or more tool calls |
describe_tool(name) |
Get detailed docs for a specific tool |
Everything else — validation, execution, result formatting — happens behind the scenes.
Installation
pip install ai-tools-executor
Or with uv:
uv add ai-tools-executor
Quick Start
1. Register Tools
from ai_tools_executor import tool
@tool(description="Fetch real-time stock price.", category="finance", tags=["stock", "price"])
def get_stock_price(symbol: str) -> dict:
"""Fetch real-time stock price for a ticker symbol.
Args:
symbol: Stock ticker (e.g. 'GOOG', 'AAPL').
Examples:
get_stock_price(symbol="GOOG")
"""
return {"symbol": symbol, "price": 182.63, "currency": "USD"}
@tool(description="Search the web for information.", category="search", tags=["web", "google"])
def search_web(query: str, max_results: int = 5) -> list[dict]:
"""Search the web using a text query.
Args:
query: Natural language search query.
max_results: Number of results to return (1-20).
"""
return [{"title": "...", "url": "...", "snippet": "..."}]
2. Use the Executor
from ai_tools_executor import ToolExecutor
executor = ToolExecutor()
# Meta-tool 1: Search for tools by intent
print(executor.search_tools("stock price lookup"))
# def get_stock_price(symbol: str) -> dict:
# """Fetch real-time stock price."""
# Meta-tool 2: Execute tool calls using Python syntax
results = executor.execute("get_stock_price(symbol='GOOG')")
r = results[0]
r.ok # True
r.tool # "get_stock_price"
r.result # {"symbol": "GOOG", "price": 182.63, "currency": "USD"}
# Execute multiple calls at once
results = executor.execute(
"[get_stock_price(symbol='GOOG'), search_web(query='market trends')]"
)
# Serialise for transport when needed
r.to_dict() # plain dict
r.to_json() # JSON string
# Meta-tool 3: Get detailed docs when needed
print(executor.describe_tool("search_web"))
Key Features
Function Call Syntax (not JSON)
# Traditional agent tool calling (JSON — verbose, error-prone)
{"tool": "get_stock_price", "params": {"symbol": "GOOG"}}
# This package (Python syntax — native, token-efficient)
get_stock_price(symbol="GOOG")
| JSON Tool Calling | Function Call Syntax | |
|---|---|---|
| Tokens | ~20 per call | ~7 per call (65% less) |
| LLM fluency | Synthetic format | Native — trained on billions of function calls |
| Validation | Custom JSON schema validator | ast.parse() — same as IDEs/linters |
| Error rate | Higher (JSON syntax errors) | Lower (function calls are natural to LLMs) |
Safe AST Parsing
No code is ever executed. The parser uses Python's ast module to extract function names and arguments — exactly like an IDE or linter:
import ast
tree = ast.parse('get_stock_price(symbol="GOOG")', mode="eval")
# Extracts: function_name="get_stock_price", kwargs={"symbol": "GOOG"}
# Validates against registry, then calls the real function
Partial Failure on Multi-Call
When some calls succeed and others fail, you get both:
results = executor.execute(
"[get_stock_price(symbol='GOOG'), bad_tool(x=1)]"
)
results[0].ok # True — stock price succeeded
results[0].result # {"symbol": "GOOG", "price": 182.63, ...}
results[1].ok # False — bad_tool failed
results[1].error # "ToolNotFoundError: Tool 'bad_tool' not found ..."
Structured Error Messages
Errors follow a consistent format so the agent can self-correct:
ValidationError: Missing required parameter(s) for 'get_stock_price': symbol
Input: get_stock_price()
Error: Missing required parameter(s) for 'get_stock_price': symbol
Expected: def get_stock_price(symbol: str) -> dict:
"""Fetch real-time stock price."""
Hint: Required: symbol
Pluggable Search
Swap search strategies via the SearchStrategy ABC:
from ai_tools_executor import ToolExecutor, SearchStrategy
class SemanticSearch(SearchStrategy):
def search(self, query, tools, *, max_results=5):
# Your embedding-based search here
...
executor = ToolExecutor(search_strategy=SemanticSearch())
Async Execution
Run independent tool calls concurrently:
results = await executor.execute_async(
"[get_stock_price(symbol='GOOG'), get_weather(city='London')]"
)
Hot-Reload
Register and unregister tools at runtime:
from ai_tools_executor import get_default_registry
registry = get_default_registry()
registry.unregister("old_tool")
# Register new tools with @tool — no restart needed
Architecture
AI Agent (LLM)
│ Only sees: search_tools + execute + describe_tool
│
├── search_tools(query) ──► Tool Search ──► Registry ──► Ranked results
│
├── execute(calls) ──► AST Parser ──► Validator ──► Tool Function ──► Results
│
└── describe_tool(name) ──► Registry ──► Full docstring
For the complete architecture document, see docs/architecture.md.
Project Structure
src/ai_tools_executor/
├── __init__.py # Public API
├── decorator.py # @tool decorator, ToolInfo, ParameterInfo
├── exceptions.py # Structured error hierarchy
├── executor.py # ToolExecutor (3 meta-tools)
├── models.py # ToolCallResult, CallStatus (frozen dataclasses)
├── parser.py # AST call parser + Layer 1 validation
├── registry.py # Thread-safe ToolRegistry
├── meta_tools.py # Meta-tool schema + handle_tool_call handler
└── search.py # Pluggable search strategies
Development
# Clone and install
git clone https://github.com/surajairi/ai-tools-executor.git
cd ai-tools-executor
uv sync
# Run tests
uv run pytest tests/ -v
# Lint
uv run ruff check src/ tests/
Requirements
- Python ≥ 3.10
- No external dependencies (stdlib only)
License
Release files for ai-tools-executor 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ai_tools_executor-0.3.0.tar.gz | 23.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ai_tools_executor-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.3 kB
Release files / ai_tools_executor-0.3.0.tar.gz
| Download URL | ai_tools_executor-0.3.0.tar.gz |
|---|---|
| Size | 23.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3f03e96e069d1ef6b841b628a8e97d615d83838f6ccd62de800d3c6c8852ef13
|
|
BLAKE2b-256 checksum How to use checksums |
75821382b05ca63927258b7be3932be82bcb6223ffbcb2f0b76d3b73cd97080e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.8
|
Release files / ai_tools_executor-0.3.0-py3-none-any.whl
| Download URL | ai_tools_executor-0.3.0-py3-none-any.whl |
|---|---|
| Size | 20.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9f3ba865ddc9ee9959473cf2ff91ccf7a66ba3f09fe99a65d453447607a57005
|
|
BLAKE2b-256 checksum How to use checksums |
4585e7c6068a82fceedb2850386845096139ae2fdc9f5a13cf705f0ce70f0997
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.8
|