cd /news/developer-tools/i-built-a-mini-agent-tool-framework-… · home topics developer-tools article
[ARTICLE · art-35497] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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

A developer built a mini agent-tool framework to understand how LangGraph and CrewAI register tools, implementing a global tool registry using Python's __init_subclass__ method. The framework automatically registers tools when subclasses are defined, eliminating the need for manual tool lists and catching missing tool names at definition time.

read3 min views1 publishedJun 21, 2026

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":

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 timeBaseTool(LoggingMixin, RetryMixin, MetricsMixin, ABC)

composes logging, retries, and metrics without inheritance spaghettiParamSpec

-based decorators@log_execution

, @measure_time

) that wrap methods without breaking their signatures for mypystream()

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.

── more in #developer-tools 4 stories · sorted by recency
── more on @langgraph 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-built-a-mini-agent…] indexed:0 read:3min 2026-06-21 ·