Skip to content

tutorial

Chapter 6 of 6

Chapter 6 — Portability, and what it cost

by Rod Rivera Published

Why explicit tools and explicit memory make a skill folder movable, plus the mistakes worth knowing before you hit them.

Every rule in this tutorial serves one goal: a skill folder you can pick up and drop into another agent.

That works when three things are explicit.

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. Moving it is a copy plus whatever import_tools names.

Nothing has to be inferred by reading code, which is the difference between a skill you can move and a skill you can only rewrite.

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.

Write bare, read qualified. context.memory.set("customer_id", …) works; set("project.customer_id", …) fails validation. Reads accept both.

immutable is not write-once. It blocks all runtime writes. On customer_id it silently breaks authentication — the write is denied and only the server log says so.

No description on tool-written fields. Validation rejects a description unless the field is llm_settable or collect-owned. Use a YAML comment.

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

One file to change instead of four, and nothing to change at all once the rename lands. Two things worth knowing if you copy the pattern:

  • It assumes the rename is path-only. If the API also changes shape, the try branch will import cleanly and then fail somewhere less obvious.
  • Delete the fallback once the old path is gone, rather than leaving it indefinitely — a shim that outlives its reason becomes a puzzle for whoever reads it next.

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.