Skip to content

Tools

A tool exposes a JSON schema (for the model) and an async execute (for the runtime). YAAB supports typed function tools, agent-as-tool, MCP tools, and remote A2A agents — all satisfying the same Tool protocol.

Typed function tools

Decorate a typed function with @tool. The parameter schema is generated from the type hints, arguments are validated by Pydantic, and the description comes from the docstring.

from yaab import tool

@tool
def search(query: str, limit: int = 5) -> list[str]:
    """Search the knowledge base."""
    return [...]

Tools may be sync or async, and may optionally take a RunContext first parameter (named ctx) for dependency injection — it is excluded from the model-facing schema:

from yaab import RunContext, tool

@tool
def charge(ctx: RunContext, amount: int) -> str:
    """Charge the current customer."""
    return ctx.deps.payments.charge(ctx.deps.customer_id, amount)

Bad arguments raise a ToolError, which the runtime feeds back to the model as a tool result so it can correct itself rather than crashing the run.

Custom Tool objects

Implement the protocol directly for full control:

from yaab.tools import Tool   # typing.Protocol

class MyTool:
    name = "my_tool"
    description = "Does a thing."
    def schema(self) -> dict: ...
    async def execute(self, ctx, **kwargs): ...

Controlling tool use (tool_choice)

Set tool_choice on the agent to steer whether/which tools the model calls:

Agent("a", model="openai/gpt-4o", tools=[...], tool_choice="auto")      # default
Agent("a", model="openai/gpt-4o", tools=[...], tool_choice="required")  # must call some tool
Agent("a", model="openai/gpt-4o", tools=[...], tool_choice="none")      # answer without tools
Agent("a", model="openai/gpt-4o", tools=[search], tool_choice="search") # force this tool

A bare tool name is expanded to the provider's {"type": "function", "function": {"name": ...}} form. Use "required" to force the agent to call a tool before producing a final answer.

A forcing choice ("required" or a pinned tool name) applies to the first model call only. After the agent has called a tool, the choice is relaxed to "auto" so the model can read the tool result and produce a final answer — otherwise every turn would be forced to call a tool and the run could never finish. This is the "force at least one tool call" semantics, not "force a tool call on every turn".

Repairing malformed tool args

When a model emits tool-call arguments that don't match the schema, a plugin can coerce/repair them before validation via the repair_tool_args hook:

from yaab.plugins import Plugin

class CoerceInts(Plugin):
    async def repair_tool_args(self, ctx, agent, tool, args):
        if tool == "add":
            return {k: int(v) for k, v in args.items()}   # "2" -> 2
        return None   # leave unchanged

runner = Runner(plugins=[CoerceInts()])

If repair isn't enough, invalid args still raise a ToolError that is fed back to the model as a tool result so it can retry.

Agent as a tool

sub = Agent("researcher", model="openai/gpt-4o")
main = Agent("writer", model="openai/gpt-4o", tools=[sub.as_tool(name="research")])

MCP tools

Import an MCP server's whole toolset (see Interop):

from yaab.tools.mcp_client import MCPClient

client = MCPClient.stdio(["python", "my_mcp_server.py"])
await client.start()
agent = Agent("a", model="openai/gpt-4o", tools=await client.list_tools())

Remote A2A agents as tools

A RemoteAgent is also a tool, so a local agent can delegate to a remote one:

from yaab.a2a import RemoteAgent

remote = RemoteAgent("https://other-service", name="billing")
agent = Agent("a", model="openai/gpt-4o", tools=[remote])

Coercion

Agent(tools=[...]) accepts a mix of plain functions and Tool objects; functions are wrapped in FunctionTool automatically.