Skip to content
RasaGet a free licence
Menu

tutorial

Chapter 2 of 6

Rasa Mantle local tools: when a tool belongs to one skill

by Rod Rivera Published

A tool that belongs to exactly one skill lives in that skill folder, is auto-discovered, and travels with the skill.

In the banking assistant, signing in is one task, called a skill in Mantle. Its passphrase check is a Python function, called a tool. The balance and transfer tasks need the customer ID that sign-in stores; they do not need to check the passphrase themselves.

You will put that check inside the sign-in skill’s folder. This makes it a local tool, available to that skill. When you reuse the skill, you can see which code and supporting dependencies to carry with it.

skills/authenticate/
  skill.md
  memory.yml
  tools.py        <- local tools live here

The tool

# skills/authenticate/tools.py
from lib.engine import ToolContext, ToolResult, tool

from lib.directory import customer_by_passphrase


@tool(description="Check the caller's passphrase and sign them in if it matches.")
async def verify_passphrase(passphrase: str, context: ToolContext = None) -> ToolResult:
    """Verify the caller's passphrase.

    Args:
        passphrase: The secret word the caller gives to prove identity.
    """
    customer = customer_by_passphrase(passphrase)

    if customer is None:
        if context is not None:
            attempts = context.memory.get("passphrase_attempts") or 0
            context.memory.set("passphrase_attempts", int(attempts) + 1)
        return ToolResult(
            llm_response={"ok": False, "error": "passphrase_incorrect"}
        )

    # Writing to project memory is what lets later skills skip this work.
    if context is not None:
        # Bare names resolve against this skill, then the project. The pinned
        # runtime also accepts qualified project names; later overwrites fail.
        context.memory.set("customer_id", customer["customer_id"])
        context.memory.set("authenticated", True)
        context.memory.set("verification_method", "passphrase")

    return ToolResult(
        llm_response={
            "ok": True,
            "customer_id": customer["customer_id"],
            "name": customer["name"],
        }
    )

There is no registration step. Files in a skill’s own folder are discovered automatically — no import_tools, no manifest, nothing to keep in sync.

Why the import goes through lib.engine

The companion keeps its Rasa imports in lib/engine.py. Each tool imports from that module, so an SDK import change can be handled in one place. For the pinned release, the module imports rasa.mantle directly:

# lib/engine.py
from rasa.mantle.tools.decorator import ToolContext, tool
from rasa.mantle.tools.result import ToolResult

Older versions used a different package name. This pinned project does not include a fallback for those versions.

Discovery is unaffected. The loader finds tools by checking for the _tool_description attribute that @tool sets, so where you imported the decorator from does not matter; the re-exported objects are the real ones.

Chapter 6 covers the decorator’s own rules, which are easy to get wrong.

The signature is the schema. The function name is the tool name, the description is what the model reads when deciding whether to call it, and the type hints define the inputs. context is injected by the runtime and is never visible to the model, so it must be named exactly that.

Why this one is local

verify_passphrase is the authentication workflow. No other skill should ever call it — a balance lookup has no business checking passphrases. Keeping it in skills/authenticate/ means the boundary is structural rather than a matter of everyone remembering not to import it.

There is a second payoff. When you move skills/authenticate/ into another agent, the tool comes with the folder. You must also carry its Python dependencies: here, lib.engine and lib.directory, plus the memory fields the tool uses. Local discovery does not package those dependencies for you.

The rule

Make a tool local when any of these is true:

  • only one skill calls it
  • it encodes that skill’s workflow rather than a general capability
  • exposing it to other skills would be a mistake — it touches a system that should stay behind this skill

That last one matters in regulated domains. A tool that reaches a payment rail or an identity provider should not be quietly available to every skill in the project just because it was convenient to share.

What a tool returns

ToolResult(llm_response=...) is what the model sees. Return structured facts, including failures:

return ToolResult(llm_response={"ok": False, "error": "passphrase_incorrect"})

Naming the failure lets the instructions branch on it. You will see that pay off in Chapter 4, where a failed lookup is what tells the agent the caller is not verified yet.