🤖lauren-ai
← Home
Export this page

Tools

Tools are functions or classes decorated with @tool() that agents can invoke during the agentic loop. The @tool() decorator inspects the function signature at decoration time to generate the JSON schema that is sent to the LLM.

Important: from __future__ import annotations is supported in tool files, but every type used in the tool signature must resolve when @tool() builds the schema. Avoid unresolved forward references and circular imports in function-form tool files. A safe reminder is:

python
# @tool() resolves parameter annotations when this module is imported.
# Keep tool signature types importable and avoid unresolved forward refs.

Function-form tools

The simplest form — a plain async function decorated with @tool():

python

from lauren_ai import tool

@tool()
async def get_weather(city: str, unit: str = "celsius") -> dict:
    """Return current weather for a city.

    Args:
        city: The city name to look up.
        unit: Temperature unit, either 'celsius' or 'fahrenheit'.
    """
    # ... call a real weather API
    return {"city": city, "temperature": 22, "unit": unit}

@tool() must be called with parentheses. Bare @tool raises DecoratorUsageError.


Class-form tools

Use the class form when the tool needs constructor-injected dependencies (a database client, an HTTP client, etc.):

python

from lauren_ai import tool
from lauren import Scope, injectable

@tool()
@injectable(scope=Scope.SINGLETON)
class SearchTool:
    """Search the product catalogue."""

    def __init__(self, db: DatabaseService) -> None:
        self._db = db

    async def run(self, query: str, max_results: int = 10) -> list[dict]:
        """Search for products matching query.

        Args:
            query: The search query.
            max_results: Maximum number of results (1-50).
        """
        return await self._db.search(query, limit=max_results)

Key rules for class-form tools:

  • The entry point must be an async def run(self, ...) method.
  • @tool() automatically applies @injectable(scope=Scope.SINGLETON) if not already present.
  • The schema is generated from run()'s parameters, not __init__.

ToolContext — injected context parameter

Declare a ctx: ToolContext parameter to receive the agent context at execution time. ToolContext is never included in the JSON schema sent to the LLM — it is injected internally by ToolExecutor.

python

from lauren_ai import tool
from lauren_ai._tools import ToolContext

@tool()
async def log_action(action: str, ctx: ToolContext) -> str:
    """Log an action taken by the agent.

    Args:
        action: Description of the action.
    """
    turn = ctx.turn
    run_id = ctx.agent_context.agent_run_id
    print(f"[{run_id}] turn={turn}: {action}")
    return "logged"

The parameter may be named anything (e.g. ctx, agent_ctx); @tool() detects it by its type annotation.

ToolContext fields:

FieldTypeDescription
agent_contextAgentContextThe owning agent's context
tool_use_idstrProvider-assigned identifier for this call
turnintAgentic loop iteration that triggered the call
requestAny \| NoneOriginating HTTP request, if any
statedict[str, Any]Mutable per-call state bag

Schema generation rules

The JSON schema is built from the function/run() signature:

Python typeJSON Schema type
str"string"
int, float"number"
bool"boolean"
dict"object"
list"array"
Optional[X] / X \| NoneX type, not required
  • Parameter descriptions are extracted from the Google-style Args: section of the docstring.
  • Parameters with a default value are marked as not required in the schema.
  • ctx: ToolContext parameters are excluded entirely.

Override the inferred name or description:

python
@tool(name="web_search", description="Search the web for current information.")
async def search(query: str) -> list[dict]: ...

@tool() parameters

ParameterTypeDefaultPurpose
namestr \| Nonefunction/class nameTool name sent to the LLM
descriptionstr \| Nonefirst docstring paragraphDescription sent to the LLM
requires_confirmationboolFalsePause for HITL approval before executing
pre_hookCallable \| NoneNoneCalled before the tool runs
post_hookCallable \| NoneNoneCalled after a successful run
error_hookCallable \| NoneNoneCalled when the tool raises
cache_ttlint \| NoneNoneCache successful results for N seconds
cache_key_fnCallable \| NoneNoneCustom cache key factory

Human-in-the-loop (HITL) approval

Set requires_confirmation=True to pause execution before the tool runs. The runner stores a pending future; resume it via AgentRunner.approve_tool() or AgentRunner.reject_tool():

python
@tool(requires_confirmation=True)
async def send_email(to: str, subject: str, body: str) -> dict:
    """Send an email.

    Args:
        to: Recipient email address.
        subject: Email subject line.
        body: Email body text.
    """
    ...

# In a separate handler (e.g. a webhook from your UI):
await runner.approve_tool(agent_run_id="...", tool_use_id="toolu_abc123")
# or:
await runner.reject_tool(agent_run_id="...", tool_use_id="toolu_abc123", reason="Not approved")

Tool result caching

Cache repeated calls with the same inputs using cache_ttl:

python
@tool(cache_ttl=300)   # cache for 5 minutes
async def fetch_exchange_rate(base: str, quote: str) -> float:
    """Fetch the current exchange rate between two currencies.

    Args:
        base: Base currency code (e.g. USD).
        quote: Quote currency code (e.g. EUR).
    """
    ...

Caching requires a CacheBackend to be passed to AgentModule.for_root() via the tool_cache= argument. Without a backend, results are not cached and no error is raised.

Provide a custom cache key to control how inputs are normalised:

python
@tool(cache_ttl=60, cache_key_fn=lambda inp: inp["city"].lower())
async def get_weather(city: str) -> dict: ...

ToolResult

Tools return plain Python values. ToolExecutor wraps them in a ToolResult automatically. You can also return a ToolResult directly for fine-grained control:

python
from lauren_ai._tools import ToolResult

@tool()
async def risky_operation(param: str) -> ToolResult:
    """Attempt a risky operation."""
    try:
        result = do_thing(param)
        return ToolResult.ok(result, tool_use_id="")
    except Exception as exc:
        return ToolResult.error(str(exc), tool_use_id="")

tool_use_id is filled in by the executor; you can leave it as "" when returning from a tool function — the executor will patch it.


Sharing a tool across multiple AgentModules

When the same @tool() class is listed in @use_tools() on agents in more than one AgentModule, the DI container raises ModuleExportViolation at startup because the same class is declared as a provider in multiple modules.

Solution: create a dedicated ownership module that provides and exports the tool once, then pass the class via shared_tools= in every AgentModule that imports it. shared_tools suppresses the re-declaration without removing the tool from the agent's callable set.

python
# 1. Ownership module — declares the tool singleton exactly once.
from lauren import module
from app.ai.check_auth_tool import CheckAuthenticationTool

@module(providers=[CheckAuthenticationTool], exports=[CheckAuthenticationTool])
class CheckAuthModule: ...

# 2. Every AgentModule that uses the tool imports CheckAuthModule and
#    lists the tool in shared_tools= to skip re-registration.
from lauren_ai._module import AgentModule

UnauthMod = AgentModule.for_root(
    agents=[UnauthenticatedCRMAgent],
    imports=[LLMProvider, CheckAuthModule],
    shared_tools=[CheckAuthenticationTool],   # owned by CheckAuthModule
)

AuthMod = AgentModule.for_root(
    agents=[AuthenticatedCRMAgent],
    imports=[LLMProvider, CheckAuthModule],
    shared_tools=[CheckAuthenticationTool],   # owned by CheckAuthModule
)

Rules:

  • The tool must be exported by a module in imports. The DI container will raise MissingProviderError at startup if it cannot be resolved.
  • shared_tools only affects provider registration; the tool's scope, lifecycle hooks, and singleton identity are all controlled by its owning module.
  • Function-form tools (plain async def) do not need this treatment — they are added directly to the runner's tool map, not registered as DI providers.

Built-in skills

lauren_ai.skills ships ready-to-use tools:

python
from lauren_ai.skills import WebSearchTool, HttpFetchTool, CodeExecutionTool
ToolDescription
WebSearchToolWeb search (stub — wire up a real API in production)
HttpFetchToolFetch a URL via HTTP (requires httpx)
CodeExecutionToolExecute Python in a subprocess sandbox

Attach them to agents via @use_tools():

python
from lauren_ai import agent, use_tools
from lauren_ai.skills import WebSearchTool, HttpFetchTool

@agent(model="claude-opus-4-6", system="You are a research assistant.")
@use_tools(WebSearchTool, HttpFetchTool)
class ResearchAgent: ...