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:
- Tool constraints —
requires:and confirmation gates, used here onmake_transfer - Scoped instructions — narrowing what the model may do inside a skill
- The Mantle documentation, which is a living document updated with every release, alongside a per-version changelog
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.