tool_agent¶
Level 2 · Tools and Skills — capability-based tool filtering with ToolRegistry.
A tool can declare the dependencies it needs to function via a requires list.
The ToolRegistry then exposes only the tools whose requirements are actually
satisfied by the current deps. This example registers two tools with different
requirements, wires only one of the two backends, and shows the registry
filtering the unsatisfiable tool out of the active set.
- Lines: ~49
- Prerequisites: none
- Key concepts:
ToolRegistry, capability filtering, therequiresdeclaration
Run it¶
Installed via pip? Copy this example into your project with the CLI (no checkout needed):
Or, from a source checkout (run from the repo root):
Expected output:
Full code¶
"""Tool agent -- ToolRegistry capability-based filtering.
Usage: python -m examples.tool_agent
"""
import asyncio
from typing import Any
from symfonic.core import BaseAgentDeps
from symfonic.core.documents.in_memory import InMemoryDocumentStore
from symfonic.core.protocols import DocumentStore, SkillStore
from symfonic.core.testing import MockModelProvider
from symfonic.core.tool_interface import ToolRegistry
class DocTool:
"""Requires DocumentStore."""
name = "doc_read"
description = "Read documents"
requires: list[type] = [DocumentStore]
async def __call__(self, state: Any, **kw: Any) -> str:
return "doc"
class SkillTool:
"""Requires SkillStore."""
name = "skill_list"
description = "List skills"
requires: list[type] = [SkillStore]
async def __call__(self, state: Any, **kw: Any) -> str:
return "skills"
async def main() -> None:
registry = ToolRegistry()
registry.register(DocTool())
registry.register(SkillTool())
# Only DocumentStore registered -- SkillStore absent
deps = BaseAgentDeps(ModelProvider=MockModelProvider())
deps.register(DocumentStore, InMemoryDocumentStore())
active = registry.get_active_tools(deps)
print(f"Active: {[t.name for t in active]}")
print(f"All: {[t.name for t in registry.all_tools()]}")
excluded = [t for t in registry.all_tools() if t not in active]
print(f"Excluded: {[t.name for t in excluded]}")
if __name__ == "__main__":
asyncio.run(main())
Step by step¶
1. Declare tools with their requirements¶
class DocTool:
name = "doc_read"
description = "Read documents"
requires: list[type] = [DocumentStore]
async def __call__(self, state: Any, **kw: Any) -> str:
return "doc"
A tool is any object exposing name, description, an async __call__, and an
optional requires list of the protocols it depends on. DocTool declares it
needs a DocumentStore; SkillTool declares it needs a SkillStore. The
requires list is the contract the registry uses to decide availability.
This example uses the plain class form to make the
requirescontract explicit. In application code you would more often use the@symfonic_tooldecorator, which derives the schema from your type hints and docstring.
2. Register the tools¶
ToolRegistry is the container. Registering a tool makes it known — not
necessarily active. Whether it becomes active depends on step 4.
3. Wire only some dependencies¶
deps = BaseAgentDeps(ModelProvider=MockModelProvider())
deps.register(DocumentStore, InMemoryDocumentStore())
# note: no SkillStore is registered
We deliberately provide a DocumentStore but not a SkillStore. This is the
whole point of the demo: the environment can satisfy DocTool but not
SkillTool.
4. Let the registry filter¶
get_active_tools(deps) returns only the tools whose every requires entry is
present in deps. DocTool passes (its DocumentStore is registered);
SkillTool is filtered out (no SkillStore). The model therefore only ever
sees tools it can actually call — no runtime "capability missing" surprises.
5. Inspect the split¶
print(f"Active: {[t.name for t in active]}") # ['doc_read']
print(f"All: {[t.name for t in registry.all_tools()]}") # ['doc_read', 'skill_list']
excluded = [t for t in registry.all_tools() if t not in active]
print(f"Excluded: {[t.name for t in excluded]}") # ['skill_list']
all_tools() is everything registered; active is the satisfiable subset. The
difference is what got filtered and why.
Why this matters¶
Capability filtering keeps the model's tool surface honest: a tool that cannot run in the current deployment never appears in the prompt, so the model cannot hallucinate a call to it. Register the same tool set everywhere, and each environment (dev, staging, a specific tenant) automatically exposes only what it can back.
What to try next¶
- Write tools with the decorator → Writing Tools
- Guard or rewrite tool calls at runtime → Tool-Call Steering
- Manage reusable skills → skill_agent