Skip to content

symfonic.agent.backend.contributed_tools

contributed_tools

Bind capability-contributed tool descriptions at the native composition root.

bind_contributed_tool

bind_contributed_tool(offered: Any) -> Any

One contributed tool, in the shape the runtime binds.

A :class:DelegationToolSpec — anything carrying name, description and a coroutine — becomes a StructuredTool. Anything else is returned untouched, because a capability that already offers a runtime tool has nothing for this function to do and re-wrapping one would destroy it.

"Anything else" includes an already bound runtime tool, and the check for one has to come first. A StructuredTool carries a callable coroutine too, so a guard that asked only about that would rebuild it from StructuredTool.from_function(coroutine=...) — which discards the args_schema the binder that produced it derived and re-infers one from the wrapper's signature. That is exactly what happened to a ToolContribution bound by :func:~symfonic.agent.cutover.extension_tools.bind_extension_tool: a tool declaring query was re-advertised as taking a single free-form arguments object, and every call the model made arrived at ToolContribution.invoke wrapped in an extra dict level. See :func:_already_bound.

parameters is read, and read here rather than nowhere. The runtime infers the argument schema from the coroutine, so a spec whose declared parameter list disagreed with the coroutine it carries would bind the coroutine's signature and say nothing -- the tool would work, under a description of itself that was false, and the field would be documentation nobody could rely on. So the declaration is held against the signature and a divergence is a ConfigurationError raised at fold time, at the composition root, before any turn is admitted. A spec declaring no parameters declares nothing to check and is left alone.

The tool-library import stays local, so importing this binder alone does not load runtime tool types. Both native composition and compatibility composition use this implementation.

Source code in src/symfonic/agent/backend/contributed_tools.py
def bind_contributed_tool(offered: Any) -> Any:
    """One contributed tool, in the shape the runtime binds.

    A :class:`DelegationToolSpec` — anything carrying ``name``, ``description``
    and a ``coroutine`` — becomes a ``StructuredTool``. Anything else is
    returned untouched, because a capability that already offers a runtime tool
    has nothing for this function to do and re-wrapping one would destroy it.

    "Anything else" includes an *already bound* runtime tool, and the check for
    one has to come first. A ``StructuredTool`` carries a callable
    ``coroutine`` too, so a guard that asked only about that would rebuild it
    from ``StructuredTool.from_function(coroutine=...)`` — which discards the
    ``args_schema`` the binder that produced it derived and re-infers one from
    the wrapper's signature. That is exactly what happened to a
    ``ToolContribution`` bound by
    :func:`~symfonic.agent.cutover.extension_tools.bind_extension_tool`: a tool
    declaring ``query`` was re-advertised as taking a single free-form
    ``arguments`` object, and every call the model made arrived at
    ``ToolContribution.invoke`` wrapped in an extra dict level. See
    :func:`_already_bound`.

    ``parameters`` is **read**, and read here rather than nowhere. The runtime
    infers the argument schema from the coroutine, so a spec whose declared
    parameter list disagreed with the coroutine it carries would bind the
    coroutine's signature and say nothing -- the tool would work, under a
    description of itself that was false, and the field would be documentation
    nobody could rely on. So the declaration is held against the signature and
    a divergence is a ``ConfigurationError`` raised at *fold* time, at the
    composition root, before any turn is admitted. A spec declaring no
    parameters declares nothing to check and is left alone.

    The tool-library import stays local, so importing this binder alone does
    not load runtime tool types. Both native composition and compatibility
    composition use this implementation.
    """
    if _already_bound(offered):
        return offered

    coroutine = getattr(offered, "coroutine", None)
    if not callable(coroutine):
        return offered

    _check_declared_parameters(offered, coroutine)

    from langchain_core.tools import StructuredTool

    return StructuredTool.from_function(
        coroutine=coroutine,
        name=offered.name,
        description=offered.description,
    )

bind_contributed_tools

bind_contributed_tools(contributed: Sequence[tuple[str, Any]]) -> tuple[tuple[str, Any], ...]

Bind every (capability, tool) pair the fold produced.

The pairing survives the binding. merge_capability_tools reports a name collision by capability, and a composition root that flattened the pairs here would turn "delegation contributed a tool named 'run_agent', which is already registered by a tool passed to Agent(tools=[...])" into "two tools called run_agent" — true, and useless to whoever has to fix it.

Source code in src/symfonic/agent/backend/contributed_tools.py
def bind_contributed_tools(
    contributed: Sequence[tuple[str, Any]],
) -> tuple[tuple[str, Any], ...]:
    """Bind every ``(capability, tool)`` pair the fold produced.

    The pairing survives the binding. ``merge_capability_tools`` reports a name
    collision *by capability*, and a composition root that flattened the pairs
    here would turn "delegation contributed a tool named 'run_agent', which is
    already registered by a tool passed to Agent(tools=[...])" into "two tools
    called run_agent" — true, and useless to whoever has to fix it.
    """
    return tuple(
        (capability, bind_contributed_tool(offered)) for capability, offered in contributed
    )