DEV Community

Sajid Islam
Sajid Islam

Posted on

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

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.

The problem

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,
}
Enter fullscreen mode Exit fullscreen mode

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

The fix: self-registering classes

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

__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:
        ...
Enter fullscreen mode Exit fullscreen mode

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.

Why this matters for agent frameworks specifically

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

def _list_tools() -> None:
    for name, tool_cls in ToolRegistry.items():
        tool = tool_cls()
        print(f"{name:<12} {tool.metadata['description']}")
Enter fullscreen mode Exit fullscreen mode
$ 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.
Enter fullscreen mode Exit fullscreen mode

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.

What's also in the project

This registry is one piece. The same codebase has:

  • Descriptors (ValidatedField, IdentifierField, IntegerRange) validating ToolConfig at assignment time
  • Mixins + MROBaseTool(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
  • Generator-based streamingstream() yields tokens instead of faking it with string slicing

I'll cover each of these in upcoming posts as I move through the syllabus.

Try it

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
Enter fullscreen mode Exit fullscreen mode

Repo: https://github.com/Sajid0875/agentic-systems-bootcamp/tree/main/Week%201%20/Session%201/project_agent_cli%20

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.

Top comments (0)