Where you are. You can cut a robot into skills at a grain a planner can use, and type them so the arguments cannot be guessed wrong. This lesson is about what comes back. A skill that returns the wrong answer is worse than one that refuses, and returning the wrong answer is the default.
The desk that got tidied perfectly
Watch a run that goes wrong in the only way that really matters.
The planner says: pick up the mug. Your pick skill runs the policy. The arm reaches, the gripper closes, and it closes on air, because the mug was two centimetres left of where the detector said. The policy does not notice. It was trained to produce a fixed-length action chunk and it produced one, exactly as it always does. The wrapper waits for the motion to finish, sees no exception, and returns {"status": "success"}.
The planner reads success and moves on. Put it on the shelf. The arm travels to the shelf with an empty gripper, executes a beautiful placing motion, and releases nothing. That skill also reports success, for the same reason.
Two skills done. The planner checks its list, finds nothing left, and reports: the desk is clear.
Every component did its job. The policy ran, the wrapper returned, the planner sequenced correctly, and the final answer is a confident lie about a mug that is still sitting on the desk.
The idea in one paragraph
Every skill has three separable parts: what must be true before it can run, what it does, and what must be true afterwards for it to have worked. Only the middle one is code you already have. The other two are a precondition and a postcondition, and no protocol will supply them for you: the tool schema has nowhere to put them, and the model cannot infer them from a name. Writing them down is what converts a policy that produces motion into a skill a planner can trust. The precondition catches the call that was never going to work. The postcondition catches the call that thought it did. And when either fails, what you send back is not a status code but a short description of the world, because that description is the only thing the planner has to plan from next.
Three different questions answer to “success”
Wider than the screen; scroll it sideways.
The word success is doing three jobs at once, and almost every wrapper answers only the first two.
| The question | What answers it | What it misses |
|---|---|---|
| Did the call return? | no exception, no timeout | everything physical |
| Did the motion complete? | the policy emitted its chunks, the controller settled | whether the motion did anything |
| Did the world change as promised? | a measurement taken after the motion | nothing, when you actually take it |
The grasp that closed on air passes the first two gates cleanly. It is not a bug in the policy and not a bug in the wrapper. It is a missing third gate.
Measure the postcondition, do not assert it
A postcondition is only worth writing if something measures it. And measurements differ enormously in what they cost and what they can see.
Wider than the screen; scroll it sideways.
| Check | Cost | Sees | Blind to |
|---|---|---|---|
| Gripper width or joint position | about 1 ms | whether the fingers closed on something | what that something is |
| Motor current or force | about 1 ms | contact, load, whether it is still held | position, identity |
| Detector run on one frame | tens of ms | the object is where you left it | occlusion, wrong label |
| Vision-language judgement | one planner round trip | almost anything you can phrase | it is a judgement, and it is wrong sometimes |
Start at the top of that ladder, always. The gripper-width check is close to free and catches the failure that opened this lesson: command the gripper closed, read the width, and if it matches the empty-close width to within a millimetre or two, there is nothing in there. That is proprioception, the cheap and reliable half of sensing from Module 0, doing work that people routinely reach for a camera to do.
Preconditions belong in the skill, not the prompt
You can describe a precondition in the tool description and hope the planner respects it. Sometimes it will. It will also call pick on an object it saw thirty seconds and two motions ago, because its world model is a transcript and transcripts do not decay.
So check inside the skill and refuse. Three practical properties make the refusal useful:
- Check against fresh state, not the plan. Re-run the detection, read the gripper, ask where the arm is. A precondition validated against the same stale belief that produced the call proves nothing.
- Refuse before moving. A refusal that costs 40 ms and no motion is the cheapest possible failure, and it is the only failure mode that is fully reversible.
- Say which precondition failed. “Gripper is not empty” and “object obj_3 is no longer detected” send the planner in completely different directions. “Precondition failed” sends it nowhere.
Failure is a description of the world
Here is the part that people who have written a hundred tool servers still get wrong on their first robot.
In MCP, two kinds of failure look completely different to the model. A protocol error is a JSON-RPC error: unknown tool, malformed request, server broken. The spec notes that clients may show these to the model but they are less likely to lead to successful recovery. A tool execution error is a normal, successful result carrying isError: true plus human-readable text, and the spec says clients should give these to the model precisely so it can correct itself.
That maps onto robotics exactly:
- “No skill named
grab” is a protocol error. The planner used the wrong name; there is nothing about the world to reason over. - “Grasp failed. The gripper closed to 2 mm, which is empty. obj_3 is still detected at the same place. The arm has returned to the pre-grasp pose.” is a tool execution error, and every clause in it is load-bearing.
Wider than the screen; scroll it sideways.
Four fields carry a failure well.
- What was attempted. The skill and its arguments, echoed back, so the planner is not reasoning from memory.
- What was measured. The number that decided this was a failure. Not “grasp failed” but “gripper width 2 mm, empty threshold 3 mm.”
- What the world looks like now. Where the arm ended up, what the gripper holds, what is still detected.
- What is still true. Which parts of the plan survive. If the object is still there and still reachable, a retry is sensible. If it fell on the floor, the rest of the plan is void.
The fourth field is the one people leave out and the one that makes replanning possible. Without it the planner’s only options are retry blindly or give up.
What neither condition catches
Preconditions and postconditions bound one skill against what it promised. They say nothing about what it did that nobody wrote down.
The arm picks the mug successfully. Its elbow catches the pen on the way up and the pen rolls off the desk. Precondition held, postcondition held, the report says success, and it is true. The world is now different from the planner’s model in a way no check was looking for, and every later decision inherits the error.
Review
The two halves no protocol supplies
Every skill has three separable parts: what must be true before it can run, what it does, and what must be true afterwards for it to have worked. Only the middle one is code you already have. The other two are a precondition and a postcondition, and no protocol will supply them for you: the tool schema has nowhere to put them, and the model cannot infer them from a name. Writing them down is what converts a policy that produces motion into a skill a planner can trust. The precondition catches the call that was never going to work. The postcondition catches the call that thought it did.
Measure the postcondition, do not assert it
A skill that reports success because the motion finished is the most expensive lie in the stack, because the planner believes it and never checks again. For a pick, the postcondition is that the object is now in the gripper, not that the trajectory ran to completion; those are different claims and only one of them is about the world. So measure it against a sensor rather than asserting it from the fact that the code returned. And when either condition fails, what you send back is not a status code but a short description of the world, because that description is the only thing the planner has to plan from next.
Check yourself
1. A pick skill runs the policy, waits for the motion to finish, catches no exception, and returns success. Which gate is missing and what does it cost?
The third: whether the world changed as the name promised. The first two gates, the call returning and the motion completing, are both satisfied by a gripper that closed on air, because the policy emits its action chunk whether or not anything is there. The cost is that the planner now believes the object is held, so every downstream step is planned against a false state and none of them will notice, up to and including the final report that the task succeeded.
2. Why check the gripper width before asking a vision model whether the grasp worked?
Cost and reliability both point the same way. Reading gripper width is proprioception: about a millisecond, and it directly measures the thing you care about, because fingers that closed to their empty width are holding nothing. A vision-language judgement costs a full planner round trip and is itself sometimes wrong. Reserve the expensive check for questions the cheap one genuinely cannot answer, such as whether you picked up the right object.
3. What is wrong with letting the planner track whether the gripper is full, based on the successes it has been told about?
It turns one silent failure into permanent corruption. The planner’s state is a transcript, and transcripts do not decay or get corrected by the world. One unnoticed failed grasp, one bump, one human lifting the object out of the gripper, and every precondition the planner evaluates afterwards is evaluated against fiction, while each individual call still looks reasonable. Robot state is measured, not remembered.
4. Distinguish a protocol error from a tool execution error, and give the robotics version of each.
A protocol error means the request itself was invalid: unknown tool, malformed arguments, server failure. In robotics that is “no skill named grab”, and there is nothing about the physical world for the model to reason over. A tool execution error is a successful response that reports a failed operation, flagged as an error and carrying readable text. In robotics that is “grasp failed, gripper closed to 2 mm, object still detected at the same place.” The second kind is what the model can actually recover from, which is why the spec says to pass it through.
5. Of the four fields in a failure report, which one do people leave out, and what does its absence cost?
“What is still true.” Reports usually carry the attempt, the measurement and sometimes the current state, then stop. Without an explicit statement of which parts of the plan survive, the planner cannot tell a retryable failure from a fatal one, so it either retries something impossible or abandons a task that only needed one step repeated. The object still being detected and still reachable is the difference between those two responses.
6. The arm picks the mug successfully and knocks a pen off the desk on the way up. Which condition caught it?
Neither. The precondition held, the postcondition held, and the report is honest, because nobody wrote down that the elbow sweeps through the space where the pen was. This is a latent failure: it satisfies its contract and still leaves the world different from the planner’s model. Replanning does not fix it, because the replan starts from the same wrong state. The only defences are naming likely side effects in advance and re-grounding the scene before anything irreversible.
Do this
About forty minutes, standard library only.
1. Build a dishonest skill. Write fake_pick that runs a pretend motion and returns {"status": "success"} unconditionally, with an internal coin flip deciding whether the object is really in the gripper:
import random
EMPTY_MM, HELD_MM = 2.0, 24.0
def fake_grasp(p_slip=0.4):
"""Returns (truth, measured_gripper_width_mm)."""
held = random.random() > p_slip
return held, (HELD_MM if held else EMPTY_MM) + random.gauss(0, 0.3)
def fake_pick(object_id):
held, width = fake_grasp()
return {"status": "success"}, held # second value is ground truth
Run 50 trials and print reported successes against real successes. You should see 50 versus roughly 30. That gap is the whole lesson, and it is the gap the planner never sees.
2. Add the third gate. Rewrite pick so it reads the width, compares it against a threshold between the empty and held values, and returns a structured result rather than a status string. Re-run 50 trials. Reported successes should now match real successes every time. Print any disagreements; there should be none, and if there are, your threshold is wrong rather than your logic.
3. Write the failure report. On failure, return a dict with all four fields: attempted, measured, world_now, still_true. Make still_true genuinely informative, for example that the object is still detected and reachable so a retry is sensible. Then delete that field, read the remaining report as if you were the planner, and write one sentence on what you would do next. The difference between those two sentences is why the field exists.
4. Add a precondition. Give your module a gripper_full flag that pick sets and place_on clears. Have pick refuse when it is already set, returning which precondition failed and what it measured. Now call pick twice in a row and confirm the second call refuses in under a millisecond without moving.
5. Break it on purpose. Have an outside function clear the object from the detection set between the precondition check and the grasp, and watch a correct precondition produce a failed skill anyway. That is a world change rather than a skill failure, and telling those two apart is the subject of the failure-taxonomy lesson. Before that, lesson 6 asks the harder question: how did the object get into the detection set in the first place, and how much should you believe it.
What you can now do
You can separate the three questions that hide inside the word success, and add the missing third gate to a skill so that a grasp on air reports as a failure rather than as a finished motion. You can pick the cheapest measurement that actually settles a postcondition, put preconditions inside the skill where they get checked against fresh state, and write a failure report with the four fields a planner needs to recover, including the one that tells it whether the rest of the plan survived. And you can name the class of failure none of that catches.