Skip to content

Tools

A tool is a unit of side-effect work the LLM asks for and the agent delivers.

The LLM never executes code directly. It produces a tool call — a name and arguments. The agent validates the call, checks guardrails, asks the user if the tool is guarded, then runs tool.execute(). The result goes back into the conversation as a ToolExecutionResultMessage.


Tool call lifecycle

flowchart LR
    L["LLM produces\ntool_call {name, args}"] --> V["BaseTool.run()\n① validate input schema"]
    V -->|invalid| E(["ValueError"])
    V -->|valid| G["🛡 TOOL_CALL\nguardrail check"]
    G -->|tripwire| D(["Denied"])
    G -->|pass| H{HITL\nrequired?}
    H -->|"HitlMode.BLOCKING"| W["⏸ Wait for\nhuman approval"]
    H -->|no| EX["② tool.execute(**kwargs)"]
    W -->|approved| EX
    W -->|denied| D
    EX --> R["ToolResult\n{content, is_error, app_data}"]
    R --> M["ToolExecutionResultMessage\nadded to history"]

    style EX fill:#2b1a0d,stroke:#fb923c,color:#e2ecff
    style R  fill:#1a2b1a,stroke:#4ade80,color:#e2ecff
    style W  fill:#1a1a2e,stroke:#818cf8,color:#e2ecff

Subclassing BaseTool

Every tool subclasses BaseTool. Set risk and hitl_mode as class-level attributes — the agent reads them before executing.

from ravi.core.tools.base_tool import BaseTool, ToolResult, ToolRisk, HitlMode

class SendEmailTool(BaseTool):
    risk      = ToolRisk.CRITICAL       # Strategy: class-level
    hitl_mode = HitlMode.BLOCKING       # Agent suspends until user approves

    def __init__(self):
        super().__init__(
            name="send_email",
            description="Send an email to a recipient",
            input_schema={
                "type": "object",
                "properties": {
                    "to":      {"type": "string", "description": "Recipient email"},
                    "subject": {"type": "string"},
                    "body":    {"type": "string"},
                },
                "required": ["to", "subject", "body"],
            },
        )

    async def execute(self, *, to: str, subject: str, body: str) -> ToolResult:  # type: ignore[override]
        await email_service.send(to=to, subject=subject, body=body)
        return ToolResult(
            content=[{"type": "text", "text": f"Email sent to {to}"}],
            app_data={"recipients": [to]},   # ← use app_data, not metadata
        )

ToolRisk — the trust ladder

graph LR
    SAFE["🟢 SAFE\nread-only\nno side-effects"]
    SENSITIVE["🟡 SENSITIVE\nexternal reads\nnetwork calls"]
    CRITICAL["🔴 CRITICAL\nwrites / deletes\nreal-world effects"]

    SAFE --> SENSITIVE --> CRITICAL

    style SAFE      fill:#1a2b1a,stroke:#4ade80,color:#e2ecff
    style SENSITIVE fill:#2b2710,stroke:#facc15,color:#e2ecff
    style CRITICAL  fill:#3b1a1a,stroke:#f87171,color:#e2ecff
Risk Examples Default HITL
ToolRisk.SAFE web_search, read_file, calculator None
ToolRisk.SENSITIVE http_get, database_read, list_files None
ToolRisk.CRITICAL send_email, delete_file, run_sql_write HitlMode.BLOCKING

HitlMode — how approval works

flowchart TD
    T["Tool is called"] --> M{HitlMode?}

    M -->|BLOCKING| B["⏸ Agent suspends\nSSE → UI shows approval card\nWait for user response"]
    B -->|approved| OK(["Execute tool"])
    B -->|denied / timeout| NO(["Skip — return denial"])

    M -->|CONTINUE_ON_TIMEOUT| CT["⏸ Wait hitl_timeout_seconds\nthen auto-approve"]
    CT --> OK

    M -->|FIRE_AND_CONTINUE| FC["📨 Send approval event\nContinue immediately\n(no wait)"]
    FC --> OK

    style B  fill:#1a1a2e,stroke:#818cf8,color:#e2ecff
    style OK fill:#1a2b1a,stroke:#4ade80,color:#e2ecff
    style NO fill:#3b1a1a,stroke:#f87171,color:#e2ecff

Schema methods

A tool exposes three schema formats — use the right one for each consumer:

Method Returns Use for
tool.get_schema() Tool (framework) ReActAgent(tools=[...])
tool.get_openai_schema() dict (OpenAI format) client.generate(tools=[...])
tool.get_mcp_schema() dict (MCP wire) MCP protocol / debugging

CapabilityRegistry

The agent auto-discovers and searches tools through CapabilityRegistry. Register your tools and the agent uses fuzzy search to surface the right one.

graph TD
    REG["CapabilityRegistry"] --> CAT["Categories\n(system / data / communication / ...)"]
    REG --> ENT["CapabilityEntry\n{name, description, kind, tags}"]
    ENT --> TOOL["BaseTool instance"]

    SEARCH["registry.search('send notification')"] --> S["Multi-signal scorer\n(name match + tag + category boost)"]
    S --> TOP["Top-5 CapabilityEntry results"]

    style REG   fill:#0d2b2b,stroke:#2dd4bf,color:#e2ecff
    style SEARCH fill:#1a1a2e,stroke:#818cf8,color:#e2ecff
from ravi.core.tools.catalog import CapabilityRegistry

registry = CapabilityRegistry()

# Register
registry.register_tool(
    SendEmailTool(),
    category="communication",
    tags=["email", "notify"],
)

# Lookup by name or alias
tool = registry.get_tool("send_email")

# Fuzzy search
results = registry.search("send a notification", limit=5)

# Browse a category
tools = registry.browse("communication")

# All tools at a given risk level
risky = registry.by_risk(ToolRisk.CRITICAL)

Built-in categories

system · communication · data · data/visualization · data/exploration · data/management · development · development/execution · development/project · research · creative · media · productivity


Source

File What it owns
core/tools/base_tool.py BaseTool, ToolResult, ToolCall, ToolRisk, HitlMode
core/tools/catalog.py CapabilityRegistry, CapabilityEntry, CategoryNode
core/tools/registry.py ToolRegistry — global singleton
catalog/tools/ Built-in tool implementations