How to Add a Custom Tool to OpenMontage Using Inheritance and Auto-Discovery
OpenMontage automatically discovers every concrete tool class that inherits from BaseTool and resides within the tools package, requiring no explicit registration code.
The OpenMontage framework provides a pluggable architecture that allows developers to extend functionality without modifying core source files. By leveraging Python inheritance and the built-in auto-discovery mechanism, you can add custom capabilities that agents and CLI commands consume immediately. This guide walks through the exact implementation details based on the current source code in calesthio/OpenMontage.
Understanding the Tool Registry Architecture
The discovery engine lives in tools/tool_registry.py and implements a singleton pattern that scans the entire tools package at runtime.
The Discovery Mechanism
According to the source code, the ToolRegistry.discover() method (lines 18-33) uses pkgutil.walk_packages to iterate through every module under the tools directory. When it encounters a class that inherits from BaseTool, it instantiates the class and adds it to the internal self._tools mapping. The singleton registry instance (defined at lines 90-92) ensures this discovery happens once per application lifecycle, though you can trigger it manually via registry.ensure_discovered().
The Base Class Contract
All tools must inherit from BaseTool, defined in tools/base_tool.py (lines 27-45). This abstract base class enforces a strict contract:
- Metadata attributes:
name,version,capability,tier,stability,runtime - Schema definitions:
input_schemaandoutput_schemafor validation - Dependency checking:
dependencieslist andcheck_dependencies()method (lines 5-12) - Execution logic: The abstract
execute(self, inputs: dict) -> ToolResultmethod
Creating a Custom Tool Class
To add a custom tool, create a new Python file anywhere under the tools/ directory and subclass BaseTool.
Required Implementation Steps
- Import the base class and supporting types:
from tools.base_tool import BaseTool, ToolResult, ToolTier, ToolStability, ToolRuntime
- Override metadata attributes to describe your tool's identity and requirements:
class MyAwesomeTool(BaseTool):
name = "my_awesome_tool"
version = "0.1.0"
tier = ToolTier.CORE
stability = ToolStability.EXPERIMENTAL
runtime = ToolRuntime.LOCAL
capability = "text_generation"
provider = "my_company"
- Define input and output schemas using JSON Schema format:
input_schema = {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
}
output_schema = {
"type": "object",
"properties": {"reply": {"type": "string"}},
}
- Implement the
executemethod containing your business logic:
def execute(self, inputs: dict) -> ToolResult:
msg = inputs.get("message", "")
reply = f"Echo: {msg}"
return ToolResult(success=True, data={"reply": reply})
Complete Working Example
Place this file at tools/custom/my_awesome_tool.py:
from tools.base_tool import BaseTool, ToolResult, ToolTier, ToolStability, ToolRuntime
class MyAwesomeTool(BaseTool):
"""A simple example that echoes a string back to the user."""
# ---- Identity ---------------------------------------------------------
name = "my_awesome_tool"
version = "0.1.0"
tier = ToolTier.CORE
stability = ToolStability.EXPERIMENTAL
runtime = ToolRuntime.LOCAL
# ---- Capabilities ------------------------------------------------------
capability = "text_generation"
provider = "my_company"
capabilities = ["echo"]
best_for = ["quick demos"]
# ---- Dependencies -------------------------------------------------------
dependencies = [] # No external deps
install_instructions = "Just import – nothing to install."
# ---- Schemas ------------------------------------------------------------
input_schema = {
"type": "object",
"properties": {"message": {"type": "string"}},
"required": ["message"],
}
output_schema = {
"type": "object",
"properties": {"reply": {"type": "string"}},
}
# ---- Execution ----------------------------------------------------------
def execute(self, inputs: dict) -> ToolResult:
msg = inputs.get("message", "")
reply = f"Echo: {msg}"
return ToolResult(success=True, data={"reply": reply})
Registering Your Tool via Auto-Discovery
No explicit registration is required. The auto-discovery system finds your tool automatically when you place the subclass in a module under the tools package.
File Placement Requirements
- Location: Any subdirectory of
tools/(e.g.,tools/custom/,tools/contrib/) - Module visibility: The file must be importable as part of the
toolspackage - Class visibility: The class must be defined at module level (not nested inside functions)
The registry uses pkgutil.walk_packages (as implemented in tool_registry.py lines 27-33) to locate modules, then inspects each for BaseTool subclasses using Python's __subclasses__() mechanism.
Handling Dependencies
If your tool requires external binaries, environment variables, or Python packages, populate the dependencies list and check_dependencies() will validate availability at runtime (see tools/base_tool.py lines 4-9). This allows agents to skip unavailable tools gracefully.
Verifying and Testing Your Tool
After adding your file, verify registration through the singleton registry instance.
Check Registry Discovery
from tools.tool_registry import registry
# Trigger discovery if not already done
registry.ensure_discovered()
# List all available tools
print(registry.list_all())
# Output: ['my_awesome_tool', ...other tools...]
# Retrieve specific tool instance
tool = registry.get("my_awesome_tool")
print(tool)
# Output: <MyAwesomeTool ...>
Execute Direct Testing
Test the tool logic directly without the agent framework:
tool = registry.get("my_awesome_tool")
result = tool.execute({"message": "Hello"})
print(result.success) # True
print(result.data) # {'reply': 'Echo: Hello'}
The tool immediately appears in the auto-generated provider menu used by agents and becomes available to CLI commands throughout OpenMontage.
Summary
- Inherit from
BaseTool: Define your tool class intools/base_tool.pyand override required metadata and theexecute()method. - Place in tools package: Save your module anywhere under the
tools/directory; subdirectories are automatically scanned. - Auto-discovery activates: The
ToolRegistryintools/tool_registry.pyusespkgutil.walk_packagesto find and instantiate your class without explicit registration. - Dependencies are optional: Use the
dependencieslist to enablecheck_dependencies()validation for external requirements. - Immediate availability: Once discovered, tools appear in
registry.list_all(),registry.get(name), and agent provider menus.
Frequently Asked Questions
Where exactly should I save my custom tool file?
Save your Python file in any subdirectory of the tools/ folder, such as tools/custom/my_tool.py. The registry recursively scans all subdirectories using pkgutil.walk_packages, so organizational subfolders are supported natively. Ensure the directory contains an __init__.py file to make it a proper Python package.
Do I need to import my tool anywhere or edit the registry?
No. The auto-discovery mechanism in tools/tool_registry.py automatically imports every module in the tools package and registers any concrete subclass of BaseTool it finds. You do not need to modify tool_registry.py or any initialization files.
What methods and attributes are mandatory when subclassing BaseTool?
You must override the execute(self, inputs: dict) -> ToolResult method with your implementation logic. Required attributes include name, version, capability, tier, stability, runtime, input_schema, and output_schema. These are defined in tools/base_tool.py and enforced by the discovery system.
How do I handle external dependencies like API keys or binaries?
Populate the dependencies class attribute with a list of requirement strings (e.g., ["ffmpeg", "OPENAI_API_KEY"]). The BaseTool.check_dependencies() method (lines 5-12 in base_tool.py) validates these at runtime, allowing agents to filter out unavailable tools. Include installation instructions in the install_instructions attribute for user guidance.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →