DEV Community

Mukunda Rao Katta
Mukunda Rao Katta

Posted on

agent-fn-registry: Register Your Agent Tools With Schema, Side Effects, and Defaults in One Place

1. The moment it stopped scaling

I built an agent with eight tools. The JSON schema for each tool was in a schemas.py file. The side effects documentation was in a comment in the function. The default argument values were hardcoded inside the function body or passed in from a config dict at call time.

When I added the ninth tool, I realized I was spending more time finding where each tool's configuration lived than actually writing the tool. When a colleague asked "which tools write to the database?", I had to grep the codebase and cross-reference a comment doc.

That is the problem agent-fn-registry solves. One place. One decorator. All four things together: the function, its schema, its side effects, its defaults.

There is a second problem this fixes: sending schemas to the model API. You need a list of tool schemas for the API call. If the schemas are scattered, you assemble that list at the call site by hand. When someone adds a new tool and forgets to add its schema to the list, the model never knows the tool exists. The registry makes registry.schemas() the only place you need to touch. Add the tool, register it, done. The schema appears in every future model call automatically.

The side effects tags solve a third problem. You want an agent mode where it can only read, not write. Without tags, you filter by hand. With tags, registry.filter(side_effects=[READ]) gives you a list in one call. You build the read-only schema list from that. No grep, no manual audit.

2. Shape of the fix

from agent_fn_registry import FnRegistry
from agent_fn_registry.side_effects import READ, WRITE

registry = FnRegistry()

SEARCH_SCHEMA = {
    "name": "search_web",
    "description": "Search the web for a query",
    "parameters": {
        "type": "object",
        "properties": {
            "query": {"type": "string"},
            "limit": {"type": "integer", "default": 10},
        },
        "required": ["query"],
    },
}

@registry.register(
    schema=SEARCH_SCHEMA,
    side_effects=[READ],
    defaults={"limit": 10}
)
def search_web(query: str, limit: int = 10) -> list:
    return web_search_client.search(query, limit=limit)
Enter fullscreen mode Exit fullscreen mode

At runtime, the registry gives you what you need:

# Get all tools as a list of schemas (for the model API call):
tool_schemas = registry.schemas()

# Get just the functions that have side effects marked WRITE:
write_tools = registry.filter(side_effects=[WRITE])

# Call a tool by name with merged defaults:
result = registry.call("search_web", query="python asyncio")
# limit defaults to 10 automatically

# Get the full entry for a specific tool:
entry = registry.get("search_web")
print(entry.fn)           # <function search_web>
print(entry.schema)       # the JSON schema dict
print(entry.side_effects) # [READ]
print(entry.defaults)     # {"limit": 10}
Enter fullscreen mode Exit fullscreen mode

The decorator handles the registration. No manual registry manipulation. No separate registration step. The function definition IS the registration.

3. What it does NOT do

It does not enforce the schema. If you call a tool with wrong argument types, the underlying Python function will fail however it normally fails. The registry stores the schema as metadata. Enforcement is up to you or a separate layer like tool-arg-coerce-py.

It does not route LLM tool calls. When the model returns a tool call JSON blob, you still parse it and call registry.call() yourself. The registry does not listen to model output.

It does not generate schemas from function signatures. If you want auto-generated schemas, look at tool-schema-from-fn. The registry expects you to provide the schema. That is intentional: auto-generation from type hints loses docstring descriptions and examples that you want in the actual schema.

It does not handle async functions specially. Async functions register fine. registry.call() returns a coroutine if the function is async. You await it yourself.

4. Inside the library

The repo is at MukundaKatta/agent-fn-registry. There are 26 tests.

The main types:

  • FnRegistry: the main class. Methods are register() (decorator), get(name), all(), schemas(), filter(side_effects=None), call(name, **kwargs).
  • ToolEntry: dataclass with fn, name, schema, side_effects, defaults.
  • SideEffect: string literal type with values READ, WRITE, IDEMPOTENT, DESTRUCTIVE.
  • RegistrationError: raised on duplicate name, missing schema name field, or bad side effect value.
  • CallError: raised when registry.call() is given a name that is not registered.

The call() method merges defaults with **kwargs before invoking fn. Explicit kwargs win over defaults. This means the caller always has control and the defaults are a convenience layer.

The filter() method uses intersection logic: a tool must have ALL requested side effects to be included. If you pass side_effects=[WRITE, DESTRUCTIVE], only tools tagged with both are returned.

The schemas() method returns schemas in registration order. Consistent order matters for prompt cache stability. If you re-register tools dynamically, schemas() output will change and that will invalidate your cached prompt prefix.

Thread safety: the registry uses a simple dict internally. Do not register tools from multiple threads at the same time. Registration should happen at module load time, not during request handling.

5. When this is useful, when it is not

Useful when:

  • You have more than five tools and you need to know quickly what each one does, what data it touches, and what its defaults are.
  • You are passing schemas to a model API and want a single source of truth instead of assembling the schema list from multiple places.
  • You want to filter tools by side effect to build read-only vs write-capable agent modes. For example: in a review-only context, only expose READ tools to the model.
  • You are writing tests and want to inspect what tools are registered without touching the production call path.

Not useful when:

  • You have two or three tools. Just keep them in a list. A registry adds overhead.
  • You want automatic schema generation. The registry is schema-agnostic. Use tool-schema-from-fn for generation, then feed the result here.
  • Your tool set is dynamic at runtime (tools appear and disappear based on user state). The registry is designed for a stable tool set. Dynamic tool routing is a different problem.

6. Install

The package is pending PyPI publication.

# PyPI (pending):
pip install agent-fn-registry

# From source:
git clone https://github.com/MukundaKatta/agent-fn-registry
cd agent-fn-registry
pip install -e .
Enter fullscreen mode Exit fullscreen mode

No runtime dependencies. Python 3.9+.

# Run the tests:
pytest tests/ -v
# 26 tests, all passing
Enter fullscreen mode Exit fullscreen mode

7. Siblings in the stack

Library What it does
tool-schema-from-fn Generate JSON schema from Python function signatures
tool-side-effects-tag Closed READ/WRITE/IDEMPOTENT/DESTRUCTIVE tag set
tool-arg-defaults Fill missing tool call args from schema defaults
agentvet Validate agent tool calls before execution
tool-arg-coerce-py Coerce LLM tool args to expected Python types

The most natural combination: tool-schema-from-fn to generate the initial schema from your function's type hints and docstring, then agent-fn-registry to register it with side effects and defaults, then tool-arg-coerce-py to fix whatever the model sends that is slightly off-type.

8. What comes next

Three things I want before the first stable release.

First, a validate_call() method that runs the JSON schema against the kwargs before calling the function. Right now call-time validation is the caller's responsibility. Most pipelines want it built in.

Second, export to provider-specific formats. registry.to_anthropic() returns the list in Anthropic tool format. registry.to_openai() returns OpenAI format. Right now schemas() returns the raw schema dict you gave it. Making it provider-aware is a small adapter layer but saves boilerplate in every project.

Third, side effect based access control hooks. A callback you register on the FnRegistry that fires before any tool with WRITE or DESTRUCTIVE tags is called. Currently you filter manually. Built-in pre-call hooks would make read-only agent modes cleaner.

The core registry itself is intentionally minimal. Everything else is an opt-in layer on top.

Source: github.com/MukundaKatta/agent-fn-registry

Top comments (0)