Where you are. You can size a unit of work against your planner’s latency. This lesson says what that unit actually is, and the answer is not the checkpoint you trained.
The confident grasp of empty air
Load your best checkpoint from Module 5. It is a file. You hand it an observation and it hands back seven numbers, thirty times a second, for as long as you keep calling it.
Now take the mug off the desk and run it anyway.
The arm reaches. The fingers close on nothing. The wrist lifts, carries nothing across the desk, opens above the bin, and returns home. No number was out of range. Nothing raised. The policy has no view on whether there was a mug, because “is there a mug” is not a question it was ever asked. Run it again and it will do the same thing with the same confidence.
Now put a language model in charge of that file. The model needs four things it cannot get from the weights: what this is for, what has to be true before calling it, when it has finished, and what happened. Those four are the subject of this lesson, and of most of the rest of this module.
The idea in one paragraph
A learned policy is a function from observations to actions. It has no beginning, no end, no arguments, no scope and no way to refuse. A skill is that policy wrapped in a contract: a name and typed arguments a planner can reason about, a precondition saying when calling it is legitimate, a termination rule saying when it stops, a postcondition report saying what the world looks like now, and failure semantics that separate “that call was nonsense” from “I tried, and here is what went wrong”. The checkpoint is the part you already have. The contract is the part you write, and everything above it - the planner loop, the MCP server, recovery, evaluation - consumes the contract, not the network.
Wider than the screen; scroll it sideways.
What the weights answer, and what they do not
| The question | Where the answer lives |
|---|---|
| What should the joints do in the next 200 ms? | the weights |
| Is this the right moment to run at all? | you |
| Is the object even there? | you |
| Has it finished? | you |
| Did it work? | you |
| What should happen next? | the planner, using what you reported |
One row is the checkpoint. Five are code you write. That ratio is the lesson, and it is why “we have a working policy” and “we have a skill” are months apart.
The five parts
Identity and signature. A name, a description, and typed arguments. The description is read by a model, not by a human, and it is the only place the planner learns what this does and when to reach for it. “Pick up an object” is not enough. “Grasps a single rigid object from an uncluttered flat surface within the arm’s reach; not for stacked, deformable or transparent items” is a description a planner can use to not call you.
Precondition. What must be true for the call to be legitimate: the object is visible, the gripper is empty, the arm is homed, nothing is in the workspace that should not be. Nobody checks this for you. No protocol in this module has a slot for it.
Termination. How the skill ends. There are four honest options, and most skills need two of them: a sensor event such as the gripper current spiking on contact, a goal check such as the object appearing inside the bin, running out of the action chunk, or a timeout. Always include the timeout, even when one of the others should fire, because the characteristic failure of a learned policy is not stopping.
This is a real design problem rather than plumbing. The hierarchical-orchestration study from the first lesson found that running each subgoal for a fixed duration, and letting the VLM decide when to switch subgoals, both degraded performance in some settings. There is no default that is safe to copy.
Postcondition report. What the world looks like now. Not whether the call returned.
Wider than the screen; scroll it sideways.
Failure semantics. Two kinds of bad news, and they belong in different channels. “There is no skill by that name” or “you passed a number where a string goes” is the caller’s mistake; retrying it identically is guaranteed to fail again. “I tried, and the object slipped out of the gripper” is a result about the world; the planner should reason about it and pick a different move. Collapsing the two into one error type is the most common way a skill API quietly disables recovery, and it is why exposing skills over MCP spends most of its time on the error model.
Three ways the field declares a skill
Wider than the screen; scroll it sideways.
A description plus a schema. Prose the model reads, arguments typed with JSON Schema, calls that block, errors whose meaning you define. This is what shipped robotics APIs look like and what MCP looks like. It is what you will build.
A library the model writes code against. Give the model perception calls and motion primitives, and let it emit Python that composes them with loops and conditionals. Code as Policies established this in 2022 and it is the ancestor of every “emit the plan as code” system since. It buys expressiveness - a loop over four objects is one program instead of four planner round trips - and it costs you the ability to check the plan before it runs.
Symbolic operators. Declare each skill as a symbol with explicit preconditions and effects so a classical planner can chain them, verify that a sequence is even feasible, and refuse compositions that are not. Research systems do this: one reads equipment manuals to propose operators, another treats the skill library as a maintained ecosystem that only stitches skills through compatible transitions. As of August 2026, nobody has shipped it in a product.
Why the report shape decides how well you recover
Look at the two reports in the figure again. Both are truthful about the same failed grasp. The difference is not honesty, it is what the planner is left able to do.
Given a bare error, a language model’s only reasonable move is to call the identical thing again, which will fail identically. Given the gripper state and where the mug was last seen, the same model can regrasp from the reported pose, open wider first, try a different approach angle, or hand off to a human with a reason attached. Nothing about the model changed. You changed what was in its context.
That is also why the skill, not the checkpoint, is the unit of measurement. A skill with a real postcondition can be run a hundred times and scored on its own, independently of whatever plan called it, and swapped for a better implementation behind the same contract. Evaluating an agentic system is built entirely on that property.
Without hardware
This module is already hardware-free; the exercise below is standard-library Python and the mock policy stands in for a checkpoint on either path.
- Instead of your Module 4 policy, wrap the simulated one from your Module 4 milestone. The contract does not change, because none of its five parts live in the weights.
- Measure this: which of your preconditions you can actually evaluate. On a simulated arm you can read ground truth, so every precondition is checkable and the contract looks easy.
- What you lose: the hedge that reveals a missing sensor. On hardware, a precondition you cannot state without hedging is a sensor you do not own. In simulation you own every sensor, so write each precondition twice: once against ground truth, once against only the signals a real rig would have, and keep the second one.
Check yourself
1. Your policy achieves a 94% grasp success rate in evaluation. Why is that not enough to hand it to a planner?
Because the number describes the policy’s behaviour, not its interface. The planner needs to know when calling it is legitimate, when it has finished, and what happened on the 6% of runs that failed. None of that comes out of the checkpoint. A 94% policy with no postcondition report gives the planner a system that silently does the wrong thing one time in sixteen, with no signal to react to, which is worse than a 70% policy that says clearly when it failed.
2. Why can an imitation-learned policy not simply return “failed” when it fails?
It was trained on successful demonstrations, so failure never appeared in its output space. Its outputs are action chunks that look like the demonstrations it saw, and there is no chunk that means “this went wrong”. Detecting failure requires a separate observation of the world after the fact, compared against what the skill was supposed to achieve. That comparison is code you write, and it is why re-perceiving is one of the five stages rather than an optimisation.
3. Give the four honest ways a skill can terminate, and say why a timeout is required even when another one is present.
A sensor event such as a current spike on contact, a goal check such as seeing the object in the bin, exhausting the action chunk, and a timeout. The timeout is required because the characteristic failure of a learned policy is not stopping: if the sensor event never fires because the grasp never happened, and the goal check never passes because the object is not there, the skill runs forever and the planner is blocked on a call that will not return. The timeout converts a hang into a reportable failure.
4. A caller passes pick(object=7). A grasp is attempted and the mug slips. Why should these two produce different kinds of error?
Because they license different next moves. The first is the caller’s mistake: the call was never valid, nothing was attempted, and retrying it unchanged is guaranteed to fail the same way, so the planner should correct the call itself. The second is a fact about the world: the call was valid, something happened, the world changed, and the planner should reason about the new state and pick a different move. Merging them into a single error type teaches the planner to treat both as “try again”, which is right for neither.
5. Why is a skill, rather than a policy checkpoint, the unit of reuse and the unit of measurement?
Because a contract is what makes something substitutable and scoreable. With a stated precondition, termination rule and postcondition, you can run the skill a hundred times on its own, score it against its own stated postcondition, and replace the implementation - a different checkpoint, a scripted motion, a human teleoperator - without touching anything that calls it. A bare checkpoint has no stated success criterion, so “did it work” is only answerable in the context of one particular task, and swapping it means retesting the whole system.
Do this
Thirty minutes, standard library only. Write the contract before you write anything else.
1. Write the declaration. For your pick skill, write out all five parts in plain text before touching code: the description a model will read, the preconditions, the termination rules, the fields in the postcondition report, and the two error kinds. Half a page. If you cannot state a precondition without hedging, that hedge is a sensor you do not have yet, and it is better to discover that now.
2. Wrap a deliberately unreliable policy.
import random
from dataclasses import dataclass
random.seed(0)
@dataclass
class World:
mug_at: tuple | None = (0.31, 0.10, 0.02)
holding: str | None = None
def fake_policy(world):
"""Stands in for your checkpoint: it acts, and it reports nothing at all."""
if world.mug_at is not None and random.random() < 0.7:
world.holding = "mug"
world.mug_at = None
return world
def pick(world, object_name):
if object_name != "mug" or world.mug_at is None:
return {"ok": False, "kind": "caller", "why": f"no visible {object_name}"}
world = fake_policy(world) # TODO(you): add a timeout branch
if world.holding == object_name:
return {"ok": True, "holding": world.holding, "mug_at": world.mug_at}
return {"ok": False, "kind": "world", "why": "gripper closed on nothing",
"holding": world.holding, "mug_at": world.mug_at}
counts = {}
for _ in range(40):
report = pick(World(), "mug")
key = (report["ok"], report.get("kind"))
counts[key] = counts.get(key, 0) + 1
print(counts)
print(pick(World(mug_at=None), "mug"))
print(pick(World(), "stapler"))
Then make three changes. Add the timeout branch where the marker is, and give it its own report. Add a second version of pick that returns only {"ok": False} on failure, and write one sentence on what the planner can no longer do. Finally, make the fake policy occasionally knock the mug to a new position instead of grasping it, and report that new position. That last case is the one the rest of this module is about.
What you can now do
You can say what separates a checkpoint from a skill, name the five parts of the contract, and explain why only one of the five is in the weights. You can state why an imitation policy cannot report its own failure, choose termination rules that do not hang, and write a postcondition report that describes the world rather than the call. You can place any skill interface you meet on the spectrum from prose to symbols, and you know which end of it has actually shipped.