>cogtrix v0.3.0

Custom tools

Authoring Cogtrix Tools

This guide explains how to add new tools to Cogtrix — either by dropping a .py file into a directory (file-drop) or by packaging a tool as an installable Python package (entry-point).


Quick overview

Cogtrix discovers tools from three sources, in this order:

TrackHowUse when
Built-insrc/tools/*.py in the packageContributing to the project itself
File-drop*.py files in a configured directoryLocal one-off tools, rapid prototyping
Entry-pointInstalled package declares cogtrix.tools EPDistributable, versioned tool packages

1. Tool module format

Every tool module — built-in, file-drop, or entry-point — uses the same format.

Minimal example

# my_tools.py
from pydantic import BaseModel, Field


class SayHelloInput(BaseModel):
    name: str = Field(..., description="Name to greet.")


def say_hello(name: str) -> str:
    """Return a greeting."""
    return f"Hello, {name}!"


TOOL_CONFIGS = [
    {
        "name": "say_hello",
        "description": "Greet a person by name.",
        "input_schema": SayHelloInput,
        "function": say_hello,
        "requires_confirmation": False,
    }
]

TOOL_CONFIG vs TOOL_CONFIGS

VariableUse
TOOL_CONFIGSList of dicts — preferred when a module provides multiple tools
TOOL_CONFIGSingle dict — shorthand for single-tool modules

Each config dict must contain:

KeyTypeRequiredDescription
namestrUnique tool name (lowercase, underscores)
descriptionstrOne-sentence description shown in /tools
input_schemaBaseModel subclassPydantic schema for the tool’s arguments
functioncallableThe Python function that implements the tool
requires_confirmationboolIf True, user must approve before execution (default False)

Optional: TOOL_SETUP

If your module needs access to the Cogtrix Config object at load time (e.g. to read API keys from the config file), define a TOOL_SETUP function:

def TOOL_SETUP(config) -> None:
    """Called automatically by ToolRegistry after this module is loaded."""
    global _api_key
    _api_key = config.services.get("my_service", {}).get("api_key", "")

TOOL_SETUP is called once per process, right after the module is imported and before tools are registered.

Optional: is_configured

If your tool requires external credentials or dependencies that may not be available, define is_configured() to gate registration:

def is_configured() -> bool:
    """Return True only if the tool is ready to use."""
    return bool(_api_key)

When is_configured() returns False, the module is silently skipped and its tools are not added to the catalog.


2. File-drop tools

The simplest way to add a custom tool is to drop a .py file into a directory and tell Cogtrix to scan it.

Configure the scan directory

In your Cogtrix config file (.cogtrix.yml):

tool_dirs:
  - ~/.cogtrix/tools          # user-specific tools
  - /opt/company/cogtrix/tools  # team-shared tools

Or use the environment variable (colon-separated):

export COGTRIX_TOOL_DIRS="$HOME/.cogtrix/tools:/opt/company/cogtrix/tools"

Rules for file-drop modules

  • File name must end in .py
  • Files whose names start with _ are skipped (use this for helpers)
  • Symlinks that resolve outside the declared directory are rejected
  • The module is imported with a synthetic name cogtrix_plugin_<stem> so it never conflicts with built-in tool modules

Example directory layout

~/.cogtrix/tools/
    my_calendar.py    ← loaded
    jira_tools.py     ← loaded
    _helpers.py       ← skipped (leading underscore)

3. Entry-point (installable) tools

For tools you want to distribute as a Python package:

Step 1 — Write your plugin class

# my_cogtrix_plugin/tools.py
from pydantic import BaseModel, Field
from cogtrix.plugins import hookimpl   # or: from src.plugins import hookimpl


class EchoInput(BaseModel):
    text: str = Field(..., description="Text to echo back.")


def echo(text: str) -> str:
    """Echo text back to the caller."""
    return text


class MyPlugin:
    @hookimpl
    def cogtrix_tools(self) -> list[dict]:
        return [
            {
                "name": "echo",
                "description": "Echo text back unchanged.",
                "input_schema": EchoInput,
                "function": echo,
            }
        ]

Note: hookimpl is the pluggy hook implementation marker. It is optional but recommended because it future-proofs your plugin against hook signature changes.

Step 2 — Declare the entry-point

In pyproject.toml:

[project.entry-points."cogtrix.tools"]
my_plugin = "my_cogtrix_plugin.tools:MyPlugin"

In setup.cfg:

[options.entry_points]
cogtrix.tools =
    my_plugin = my_cogtrix_plugin.tools:MyPlugin

Step 3 — Install and run

pip install .          # or: uv add .
cogtrix               # MyPlugin's tools appear in /tools

Alternative: point directly to a module

If your module already has TOOL_CONFIGS at the top level, the entry-point can point directly to the module (no class needed):

[project.entry-points."cogtrix.tools"]
my_plugin = "my_cogtrix_plugin.tools"

Cogtrix detects that the loaded object is a module and reads TOOL_CONFIGS or TOOL_CONFIG from it directly.


4. Testing your tool

Unit test

# tests/test_my_tool.py
from my_cogtrix_plugin.tools import echo

def test_echo():
    assert echo("hello") == "hello"

Integration test (file-drop)

import tempfile, textwrap, pathlib
from src.plugins.loader import ToolPluginLoader

def test_file_drop():
    src = textwrap.dedent('''
        from pydantic import BaseModel, Field

        class PingInput(BaseModel):
            msg: str = Field(..., description="Message.")

        def ping(msg: str) -> str:
            return msg

        TOOL_CONFIGS = [{
            "name": "ping",
            "description": "Ping.",
            "input_schema": PingInput,
            "function": ping,
        }]
    ''')
    with tempfile.TemporaryDirectory() as tmp:
        pathlib.Path(tmp, "ping.py").write_text(src)
        loader = ToolPluginLoader()
        modules = loader.load_all([tmp])
    assert len(modules) == 1
    assert modules[0].TOOL_CONFIGS[0]["name"] == "ping"

5. Reference: discovery order

  1. Built-in tools in src/tools/ are always loaded first.
  2. File-drop directories are scanned in the order they appear in tool_dirs.
  3. Entry-point plugins are loaded last (order within the group is implementation-defined).

If two tools share the same name, the last one registered wins with a WARNING logged. Use unique, namespaced names to avoid collisions (e.g. acme_search rather than search).


6. Reference: full TOOL_CONFIG schema

{
    # Required
    "name": str,               # "my_tool"
    "description": str,        # "Does X given Y."
    "input_schema": type,      # class MyToolInput(BaseModel): ...
    "function": callable,      # def my_tool(...) -> str: ...

    # Optional
    "requires_confirmation": bool,  # default False
}

The function must be synchronous (no async def). For async operations, run them with asyncio.run() or loop.run_until_complete() inside the function body.