Skip to content

Writing Tools

The @symfonic_tool decorator turns a plain Python function into a framework-native tool — schema, description, and symfonic metadata included — that ToolRegistry accepts with no extra plumbing.

Quick start

from symfonic.core import symfonic_tool

@symfonic_tool()
async def get_weather(city: str) -> str:
    """Return the current weather for a city.

    Args:
        city: Name of the city to look up.
    """
    return f"Sunny in {city}."

That's it. The decorated object is a LangChain StructuredTool, so it works anywhere a tool is expected:

from symfonic.core import ToolRegistry

registry = ToolRegistry()
registry.register(get_weather)

No feature flag is required — @symfonic_tool is GA.

What the decorator does for you

  • Derives the JSON schema from your type hints and docstring (delegated to LangChain's StructuredTool.from_function(parse_docstring=True)). Primitives, Pydantic BaseModel parameters, Optional/Union, and Annotated[T, "description"] all work.
  • Uses the docstring as the tool description and lifts Args: entries into per-field descriptions — the text the model reads to decide how to call it.
  • Preserves async / sync / async-generator semantics. Async generators remain detectable by inspect.isasyncgenfunction, so streaming tools keep working.
from typing import Annotated

@symfonic_tool()
async def search(
    topic: Annotated[str, "What to search for."],
    top_k: int = 5,
) -> list[str]:
    """Search the knowledge base."""
    ...

Symfonic metadata

Beyond the schema, the decorator attaches symfonic-specific metadata that the registry lifts into the tool's ToolRegistration:

@symfonic_tool(
    scope="admin",          # "public" (default) | "internal" | "admin"
    routing_mode="enforce", # "observe" (default) | "enforce" | "off"
    visibility="hidden",    # "visible" (default) | "hidden"
    name="rotate_keys",     # defaults to the function name
    description="...",       # defaults to the docstring
)
async def rotate_keys() -> None:
    """Rotate the tenant's API keys."""
    ...
Field Effect
scope Logical visibility scope. Captured for policy code; not yet enforced at the invocation tier.
routing_mode Per-tool override for dynamic tool routing. enforce lets the router narrow the manifest; off opts the tool out of routing. See Intent & Tool Routing.
visibility hidden sets visible_to_agents=False — the tool is bound to the runtime and callable by orchestration code but never offered to the model. Useful for telemetry emitters or feature-flag readers.

Introspect metadata on any decorated tool:

from symfonic.core.tools import is_symfonic_tool, get_symfonic_metadata

is_symfonic_tool(rotate_keys)          # True
get_symfonic_metadata(rotate_keys).scope   # "admin"

Relationship to plain LangChain tools

@symfonic_tool is additive, not a replacement. Vanilla LangChain @tool functions still register and run unchanged — the registry only lifts extra metadata when it is present. Reach for @symfonic_tool when you want symfonic's scope/routing/visibility controls; use plain @tool when you don't.

Deprecated flag

Earlier releases gated the decorator behind FrameworkConfig.experimental_tool_decorator. That flag is now a deprecated no-op — its value is ignored and the decorator is always available. It remains on the config only so existing constructions do not error, and will be removed in a future major version.