Not everything belongs in project memory. Most of what a skill tracks is its own working state: the account being asked about, the amount being sent, how many passphrase attempts have failed.
That is skill memory, declared in the skill’s own folder and split in two.
# skills/transfer_money/memory.yml
schema:
public:
amount:
type: float
description: Amount the caller asked to send.
llm_settable: true
payee_name:
type: text
description: Who the money is going to.
llm_settable: true
transfer_confirmed:
type: bool
description: Caller explicitly confirmed the transfer details.
initial_value: false
llm_settable: true
private:
transfer_reason:
type: text
description: Free-text reason the caller gave for the payment.
pii: true
llm_settable: true
The top-level key is schema:, not memory:.
Public fields are readable by every other skill. Private fields are
readable only by the skill that owns them. transfer_reason is free text the
caller volunteered, flagged pii: true, and it stays inside Transfer Money.
The boundary is enforced by the runtime. It is not a convention or a lint rule, and the model cannot talk its way past it — a denied read simply returns nothing.
Two different writers
Two actors write memory, and they are governed separately:
- Tools write whatever the skill declares, through
context.memory.set(). - The model may only write fields marked
llm_settable: true, through the built-inset_fieldstool.
Mark the caller’s own decisions settable — the amount, the payee, the
confirmation. Leave anything the system derives to tools. passphrase_attempts
is not settable by the model, because the model has no business deciding how
many times you failed.
Everything must be declared
A tool that writes an undeclared field fails at rasa train:
A tool in skills/authenticate/tools.py writes undeclared memory entry
'project.customer_id' via memory.set(...) at line 38. Declare it under the
skill's schema (public/private) or the project memory.yml, or it will be
rejected at runtime.
This is a good failure. It happens at build time rather than three turns into a production conversation.
The description rule that will catch you
Validation rejects a description on a field that is neither llm_settable
nor owned by a collect: step:
Memory field 'verification_method' in skill 'authenticate' declares a
description but is neither llm_settable nor collect-owned. Descriptions are
only surfaced on set_fields for LLM-facing entries — remove the description or
mark the field settable.
The reasoning: a description exists so the model knows what to put in a field. If the model can never write it, the description is documentation aimed at nobody, and Mantle would rather you say so in a comment. Use a YAML comment for tool-written fields.
Choosing a scope
| Ask | Scope |
|---|---|
| Is it about the session or the end user? | Project |
| Is it this skill’s working state that others may need to see? | Public |
| Is it internal, sensitive, or meaningless outside this skill? | Private |
When in doubt, start private. Widening a field later is a one-line change; discovering that three skills quietly depend on something you meant to keep internal is not.
