# I built a mini agent-tool framework to actually understand how LangGraph/CrewAI register tools

> Source: <https://dev.to/sajid0875/i-built-a-mini-agent-tool-framework-to-actually-understand-how-langgraphcrewai-register-tools-15if>
> Published: 2026-06-21 10:52:00+00:00

I'm going through a 16-week Agentic AI syllabus right now, and Week 1 is "Python for Agentic Systems" — OOP, typing, decorators. Instead of just reading about it, I built a small CLI toolkit that mimics how real agent frameworks register and run tools.

This post is about one piece of it: **how a global tool registry works using __init_subclass__**, and why agent frameworks need this pattern at all.

Repo's at the bottom. Code below is real, from the actual project — not pseudocode.

Agent frameworks (LangGraph, CrewAI, PydanticAI) all need the same thing: a way for an LLM or planner to discover "what tools exist" without you manually maintaining a list somewhere.

The naive way is a dict you update by hand:

```
TOOLS = {
    "search": SearchTool,
    "summarize": SummarizeTool,
}
```

This works until you forget to add an entry. Then your planner silently can't find a tool that exists in your codebase.

Here's the actual registry from my project:

```
class ToolRegistry:
    __slots__ = ()
    _tools: dict[str, type[Any]] = {}

    @classmethod
    def register(cls, name: str, tool_cls: type[Any]) -> None:
        existing = cls._tools.get(name)
        if existing is not None and existing is not tool_cls:
            raise DuplicateToolError(
                f"tool name {name!r} is already registered by {existing.__name__}"
            )
        cls._tools[name] = tool_cls
```

And the part that actually calls `register()`

— `__init_subclass__`

on the base class:

```
class BaseTool(LoggingMixin, RetryMixin, MetricsMixin, ABC):
    def __init_subclass__(
        cls,
        *,
        tool_name: str | None = None,
        description: str = "",
        streamable: bool = False,
        abstract: bool = False,
        **kwargs: Any,
    ) -> None:
        super().__init_subclass__(**kwargs)
        if abstract:
            return

        if tool_name is None:
            raise TypeError(f"{cls.__name__} must define tool_name='...'")

        cls._tool_name = tool_name.strip().lower()
        cls.description = description.strip()
        cls._streamable = streamable
        ToolRegistry.register(cls._tool_name, cls)
```

`__init_subclass__`

fires automatically the moment Python *defines* a subclass — before you ever instantiate it. So a tool just declares itself:

```
class SearchTool(
    BaseTool,
    tool_name="search",
    description="Searches a small in-memory knowledge base.",
    streamable=True,
):
    def execute(self, context: ToolContext) -> str:
        ...
```

The moment this class body is parsed, `SearchTool`

is in the registry. No manual list. No import-time side-effect hacks. Forget `tool_name=`

and you get a `TypeError`

immediately — not a silent miss three files away.

Once tools self-register, a CLI (or a planner LLM) can just ask "what do you have":

``` php
def _list_tools() -> None:
    for name, tool_cls in ToolRegistry.items():
        tool = tool_cls()
        print(f"{name:<12} {tool.metadata['description']}")
bash
$ python main.py list-tools
search       Searches a small in-memory knowledge base and returns ranked notes.
summarize    Creates a compact extractive summary of user-provided text.
translate    Translates common demo phrases to Spanish or Urdu using a local lexicon.
```

This is structurally the same problem LangGraph and CrewAI solve with their own tool-discovery mechanisms. Different implementation, same underlying need: *a single source of truth that updates itself.*

This registry is one piece. The same codebase has:

`ValidatedField`

, `IdentifierField`

, `IntegerRange`

) validating `ToolConfig`

at assignment time`BaseTool(LoggingMixin, RetryMixin, MetricsMixin, ABC)`

composes logging, retries, and metrics without inheritance spaghetti`ParamSpec`

-based decorators`@log_execution`

, `@measure_time`

) that wrap methods without breaking their signatures for mypy`stream()`

yields tokens instead of faking it with string slicingI'll cover each of these in upcoming posts as I move through the syllabus.

```
git clone https://github.com/Sajid0875/agentic-systems-bootcamp
cd agent-ready-cli-toolkit
python main.py list-tools
python main.py describe search
python main.py run summarize "Agent frameworks register tools and stream results." --stream
```

If you're also learning agentic systems and want to compare notes on how different frameworks (CrewAI, PydanticAI, LangGraph) handle tool registration internally, drop it in the comments — genuinely curious how close/far off this mental model is from the real implementations.
