Skip to content
RasaGet a free licence
Menu

tutorial

Chapter 6 of 6

Check a Mantle transfer skill’s dependencies before reusing it

by Rod Rivera Published

Trace tool imports, shared identity fields and Python libraries before moving a bank-transfer skill to another agent.

You want to reuse Sterling’s transfer skill in another banking assistant. Copying its folder alone is not enough: it also needs the shared customer lookup, project identity fields and Python libraries used by its tools.

This chapter shows how to trace those dependencies before moving the skill. You will compare the skill’s declared tool imports and stored values with the Python imports it actually uses.

skills/transfer_money/
  skill.md      -> import_tools: what it borrows
  memory.yml    -> public / private: what it owns
  tools.py      -> what it brings with it

Read those three files and you know the skill’s entire contract with the rest of the project: it brings make_transfer, it borrows get_customer_info, it owns four memory fields, and it reads project.customer_id. Its Python tools also import lib.engine and lib.directory. Moving the skill therefore means retaining those libraries and their dependencies as well as supplying the tools named in import_tools.

Read both the skill imports and its Python imports before moving the folder. The frontmatter alone does not list every dependency.

Why it is built this way

Two forces, both from watching large teams build agents.

Bounded surface. An enterprise agent is usually several teams, each owning skills. If every team can reach every tool and every memory field, each change risks colliding with someone else’s. Local tools and private memory keep the blast radius inside one folder.

Discoverable sharing. What is shared is small, sits at the project root, and is named explicitly by whoever uses it. You can audit it in one directory listing.

Five things that will bite you

Every one of these came out of actually building this project.

Follow the companion’s memory convention. Its tools write context.memory.set("customer_id", …) and read project.customer_id. Trace those names when moving the skill so you can see which tool sets the shared identity and which skills depend on it.

Check the shared-value write rule for your runtime. In the pinned 3.21.0.dev5 memory manager, a project slot already holding a non-None value cannot be overwritten. The current field declaration has no immutable member; an old configuration note about that setting is not a current rule.

Distinguish project and skill memory declarations. The companion’s project identity fields have descriptions. Its tool-written skill fields use comments rather than model-facing descriptions. Follow the appropriate example instead of applying a skill-field rule to the project root.

Do not branch on memory tokens in prose you cannot guarantee. An @memory.project.authenticated reference resolves to nothing when the field is unset, and the model reads a broken sentence. Branch on a tool result instead — fetch_balance returning not_authenticated is unambiguous.

Say what to do with information already given. “Ask for the passphrase” makes the model ask even when the caller opened with it. Spell out the case:

If the caller has already given a passphrase in what they just said, call
`verify_passphrase` with it straight away. Do not ask them to repeat it.

Getting the decorator right

Four rules, all enforced by the engine rather than by convention. The real signature is def tool(*, description: str) — keyword-only, and the description is required:

@tool                      # TypeError: takes 0 positional arguments but 1 was given
@tool("does a thing")      # TypeError: same
@tool(description="…")     # correct

It must be async. The executor awaits the call, so a synchronous function fails at the await.

The parameter must be named exactly context. The runtime invokes tools as await func(context=ctx, **tool_args) — a keyword argument. Call it ctx and you get an unexpected-keyword error.

Every tool needs that parameter, even unused ones. There is no signature inspection; context=ctx is always passed. A tool declared as async def f(account_number: str) breaks. Give it context: ToolContext = None and ignore it — the default also keeps the function directly unit-testable.

Surviving the rename

Rasa renamed the engine package from rasa.calm_v2 to rasa.mantle in 3.20.0.dev1, and the old path is gone rather than aliased — so the same source cannot import both without help.

This project resolves it once, in lib/engine.py, and every tool imports from there:

from lib.engine import ToolContext, ToolResult, tool

The pinned companion’s lib/engine.py directly re-exports the rasa.mantle names. It has no old-package fallback. Keeping those imports in one module gives you one place to inspect when changing the tested engine version.

If you support several releases, check both the import paths and the API signatures. A successful import alone does not prove that an older tool interface still works.

Where to go next

Skills are the foundation, and more of Mantle is being built on top of them. The next things worth reading:

Two questions worth sending back to Rasa as you build: if nearly all your tools end up global, and if you find yourself wanting to widen private memory often. Both would say something about where the boundaries are drawn.