State when the skill is finished
Say when the skill is done in two places.complete_when in the skill
frontmatter is the gate the engine checks. A sentence in the body, at the step
where the work ends, tells the model when to call complete_skill.
Put the facts that must be true before the skill can end in
complete_when. Use condition for what
memory can show, and criteria for what needs reading the conversation. For
example, a SIM skill can end only once a replacement is ordered or the user
declines one, and a tool sets replacement_settled when either happens:
skills/sim_management/skill.md
condition with no criteria also completes the skill on its own once that
condition is true.
Sometimes the model calls complete_skill too early. complete_when refuses
to complete the skill, and the skill stays open. It does not tell the model
when to try, so the body sentence is what cuts the number of refused calls. Add
it next to the step that looks like the end but is not. For example:
skills/sim_management/skill.md
complete_when needs the body sentence alone. Write it at the
step that finishes the job. For example, a status skill completes after it has
given the facts from the tool and the next step. An advice skill completes
after the answer, so it does not stay open until something cancels it:
skills/card_advisory/skill.md
criteria item so a later turn cannot undo it. The judge reads the
whole conversation and fails closed, so a refused complete_skill leaves the
skill open and the model writes its own ending. For example, “states the
answer to the user’s question” fails when the user’s last message is “thanks,”
because that message has no answer. Write “answered the user’s question at
least once; a later thank-you or decline does not undo that.”
Describe every memory field the model can set
A name and a type are not enough for a field the model writes. The description says what the memory item is about. When a similar answer could be mistaken for it, also say which user answer sets the field and which answers leave it unset. For example, a skill asks whether to lock a card after it is reported lost or stolen:skills/card_replace/memory.yml
llm_settable field can be written
on any turn of the skill, so the model can still fill it from a remark before
the question is asked. When a wrong early write is costly, reject it in a
memory validation tool, not in the
description. The tool returns an error while the question has not been asked,
and the engine rolls the write back. See
Close a side effect until its facts exist
for why this matters to a gate.
Write a value to memory when a later turn needs it
Passing a value into a tool does not store it. The result of a one-time check is not kept either. If a later turn needs the fact, write it to memory when you learn it. For a value the user gives, make the fieldllm_settable and tell
the model to record it with @tool.set_fields. For a result, have the tool
write it to memory.
Keep one copy. A tool that takes the same value as an argument gives the model
two places to write it, and they can differ. Let the tool read the field from
memory and gate it on that field with requires.
For example, the user chooses an eSIM or a physical SIM. A later response still
says which SIM was ordered, so record the choice first, and let the order tool
read it:
skills/sim_management/skill.md
Close a side effect until its facts exist
Addrequires on the tool that places an order,
a lock, or another side effect, so the tool stays unavailable until the facts
it depends on exist.
For example, the tool that orders a replacement card stays hidden until the
user has picked a shipping speed and the rush check has run:
skills/card_replace/skill.md
requires or a precondition, list how each field it reads can be set:
by the model through set_fields, by a collect: step, by tool code, or by a
set: button. Close the paths that should not open the gate. In the example,
shipping_type is llm_settable, so the model can set it from a remark and
open the gate. If that is not acceptable, reject the early write in a
memory validation tool, or write
the field from tool code only.
Use a response when the words have to be exact
When the user has to hear a specific fact, put the line inresponses.yml and attach it to the tool with
on_success. Do not leave that sentence for the model to restate. The model
may summarize a result and drop the detail the user needs.
For example, after a device lookup the user needs the exact serial number for a
police report:
responses.yml
skills/sim_management/skill.md
card_locked says nothing to the user. Attach a
response if the user has to hear that the card is locked.
on_success delivers the line, and then the turn goes on. The model gets
another call in the same turn and can add its own message behind the exact
line: a question it already knows the answer to, a restated duration, or an
offer nobody asked for. When nothing may follow the line, deliver it from an
ordered block that ends with action_listen.
That ends the turn without a model call:
skills/sim_management/skill.md
Keep a second fact out of a tool result
The model speaks what a tool returns. If a payload mixes the fact the user asked for with a fact that belongs on another path, split the tools or drop the extra field. For example, a device lookup returns the serial number and also instructions for finding a lost phone. A user whose phone was stolen asks only for the serial number, and the model reads out the location instructions too. Return only the serial number from the lookup tool. Move the location instructions to a separate tool that only the lost-phone path offers. The other side holds too: a fact that is not in the result is not said. Put every fact the answer needs in the result. For example, a coverage lookup that returns the rebate but not the remaining limit makes the model say “covered, 50% back” to a user who has nothing left to claim. When an instruction applies only to the turn a tool ran, put it in the result as well. Writing prose shows this with anext_step field.
For example, “this list is complete, so a service not in it is not covered”
in the tool description was not enough. The same sentence in the result was.
Put a limit in the skill description
If a skill must not carry out a change, say so in the skill description. A line in the body alone is not enough: the model still tries to make the change while the skill is active. For example, a skill that explains card options should not change the user’s card:skills/card_advisory/skill.md
requires, or leave it
out of the skill. If the skill has no such tool, the risk is a claim: the model
says the change happened when nothing did. The description is what stops that
claim.
Continue from a choice the user already made
Record the choice in memory, and have the next step read that field. Ask only when it is still unset. This is for preferences: a shipping speed, a SIM type, a reason. Do not use it for a field that arequires gate or a precondition reads. Letting the model
fill that field from an earlier remark opens the gate without the question it
was meant to protect. See
Close a side effect until its facts exist.
Make the field llm_settable. At the step that would ask, tell the model to
write a value the user already gave with @tool.set_fields and continue. Point
the steps that follow at that field with
if:, so the skill follows the choice
instead of offering it again.
For example, if the user has already said why they need a replacement, record
the reason and follow that path:
skills/card_replace/skill.md
collect: step asks only while its field is unset, unless you set
ask_before_filling. Record the value before
that step.
For example, if the user already named a shipping speed, record
shipping_type before the shipping collect step, so the step does not offer
both speeds.
When the named choice is the consent and the action is cheap to undo, record
it and call the tool. Do not add a separate yes or no question.
For example, naming a wallet is the consent to add it. Record the provider and
provision the card. Do not ask whether they want a wallet first.
For an action that is hard to undo, such as a transfer to a human agent or a
payment, ask. Otherwise the model decides what counts as consent, and a remark
becomes a yes.
Tie a step to the fact that makes it true
Place a step right after the fact it depends on, not after a step the user might skip or reorder. Scope it withif: on that fact, so it appears as soon
as the fact is known.
For example, a user whose phone was stolen should always be told to report the
theft to the police. If that advice sits after the replacement order, a user
who asks only to block the SIM never hears it. Put it under the stolen branch,
before blocking or ordering:
skills/sim_management/skill.md
utter with
when::
skills/sim_management/skill.md
Repeat a rule in the section it governs
A field description, or a rule inagent.yml, is not
always enough. Restate the instruction in the section of the skill it governs.
The model follows it more thoroughly there than from a line it read earlier.
Writing prose
already says this is the one place repeating yourself pays: a rule at the top
of a long body is far from the section it governs. The same holds for a memory
description and for an agent-wide rule.
For example, a memory description says a lock flag is set only after the user
answers the lock question, and that “my card was stolen” does not set it. Say
that again in the section for a stolen card, where the model decides whether
to ask. If agent.yml says to speak an identifier only as a tool returned it,
say that again in the section that reads the lookup back.
Write that instruction in one place. When a collect: step collects the field,
its description is the one the model sees, and it takes preference over the
memory field’s description. Do not set both.
Put a required call in an ordered block
A prose instruction that names a tool does not make the model call it. When a call must happen, put it in an ordered block and point at it with@block.
For example, after a SIM is blocked, the device lookup must always run. A
sentence like “then look up the device” is sometimes skipped. An ordered block
runs the lookup without asking the model:
skills/sim_management/skill.md
@block.main first” is sometimes ignored:
the model answers on its own and the block never runs. If the call must happen
whenever the skill runs, make the skill blocks-only. The body is one block and
nothing else, so activating the skill runs it. See
Fully controlled.
For example, a handoff skill has to check that a human agent is available and
ask for consent before every transfer. As a blocks-only skill, both steps run
on every activation:
skills/handoff/skill.md
Reference
complete_whenfor the completion gate- Memory for
llm_settableand field descriptions - Memory validation tools for rejecting a write
- Tool constraints for
requiresandon_success - Responses for lines the user has to hear verbatim
- Scoped instructions for
if: - Ordered blocks for a call that must happen