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])

Reusing tools from other ecosystems

Wrap a tool built for LangChain or CrewAI as a native YAAB tool — duck-typed, so no extra package is installed. adapt_tool auto-detects the ecosystem (a tool exposing .invoke is treated as LangChain-style, one exposing .run/._run as CrewAI-style); from_langchain_tool / from_crewai_tool are the explicit forms.

from yaab import Agent
from yaab.tools import adapt_tool, from_langchain_tool, from_crewai_tool

agent = Agent("a", model="openai/gpt-4o", tools=[
    from_langchain_tool(some_langchain_tool),    # forwards to .invoke
    from_crewai_tool(some_crewai_tool),          # forwards to .run
    adapt_tool(some_tool),                        # auto-detected
])

Each returns a FunctionTool carrying the foreign tool's name/description, so it drops straight into tools=[…].

Browser use

browser_toolset() gives an agent a real (headless) browser — navigate, click, type, read, screenshot — for sites with no API. Needs the optional extra: pip install 'yaab-sdk[browser]' then playwright install chromium.

from yaab import Agent
from yaab.tools.builtin import browser_toolset

tools = browser_toolset(allow_domains=["example.com"])   # allowlist gates navigation
agent = Agent("scraper", tools=tools)
# ... after the run: await tools.session.aclose()

Safety is built in and composable: navigation is restricted to allow_domains (suffix match; None allows all — dev only; [] blocks all), the browser is headless by default, and because these are ordinary tools the existing approval and guardrail machinery applies unchanged — e.g. ToolApprovalPlugin(tools=["browser_navigate"]) pauses for human sign-off before each navigation. Playwright is imported lazily, so the SDK never needs it unless a browser tool actually runs.

Coercion

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