- Give the task a tool the owner does not have - the one that decides entry
- Order steps with the prompt - two sentences, no gate
- Keep a read on the owner - and the write on the task
- Let the tool end the task -
finish:and its success values - Scope history down - what each scope shows
- Give every task an escape -
unserved_request, and reading it - Do not let two prompts disagree - owner and task together
Give the task a tool the owner does not have
If the owner already holds every tool the task holds, the owner can finish the job without entering the task. It usually will. The task’sassign: then never
runs and nothing is saved.
This is the shape that produces it. A scheduling desk declared:
agent.yaml
appointment stays empty. The prompt is not the fix. A workflow that says
“then run the booking task in the same turn” is a request to the model, and a
tool within reach beats a request.
The fix is to take the tool off the owner and leave it on the task:
agent.yaml
unmute validate warns when an agent holds every tool of one of its tasks, and
names the tool to move. It is a warning and not a refusal because whether the
model takes the short route depends on your prompt and your when:, neither of
which the compiler reads. Read the warning and decide.Order steps with the prompt
Step here means a task an agent runs on its own. Two tasks that must hold their order every time belong in a task group, which runs its steps in the order you list them. This section is for the looser case: a task that usually follows another one. A task that must run only after another one does not need a code gate for that. It needs two sentences: one in the owner’s own instructions, and one in the task’s ownwhen:.
Number the flow in the owner’s instructions, in the order the caller actually
moves through it:
instructions.md
when: that names the situation and, in one
clause, what has to be true first:
agent.yaml
instructions.md
inject: or a webhook path, still
refuses to run while that value is empty or unconfirmed. The model is told
which task supplies it. When nothing supplies it, the model is told to ask the
caller.
Check the order with a scripted text conversation before you check it on the
phone. It drives the agent through a fixed script with the real model and the
real tools, so you see which task ran, and in what order, without picking up a
phone. See
scripts/text_run_livekit.py.
Keep a read on the owner, keep the write on the task
The rule above has a natural shape once you apply it. Lookups that answer questions belong on the owner, because the caller can ask at any time and entering a task to answer a price question is slow. Anything that records, saves, or changes something belongs on the task, alone, because that is the action whose result you want saved. Split that way, the model has a real reason to enter: the task holds the only route to the thing the caller is asking for.Let the tool end the task
When a task’s work ends on one tool, name that tool underfinish: with the
result values that count as success. The task then ends on the tool’s own
success result, saves its assign: from that result, and never waits for the
model to make the finish call. A result that is not a success goes back to
the model and the task stays open.
agent.yaml
enum:, and every
tool under finish: has to return what assign: saves. See
Say what success looks like.
Scope history down and let saved values carry the facts
A task may not need the transcript if its prompt names every fact it needs. Start withmessages. Use reset only when the saved inputs cover the job;
otherwise the caller will have to repeat details that were only spoken.
reset is the strongest scope. A reset task never receives the turn that
triggered it. Save a fact before the boundary and reference it in the reset
prompt when the receiver needs it. If the fact was only spoken and never saved,
the reset task asks for it again.
Give every task an escape, and read it
A task offers its own tools, its declared handoffs, and thefinish call. When
the caller asks for something none of those cover, a task with no way out
refuses, the caller presses, and it refuses again.
Every generated task carries a reserved unserved_request field for this. The
task does its own work, then names the request it could not serve in its own
words, and makes the finish call. You do not write that rule yourself: the
compiler appends it to every task prompt.
The owner receives only an unserved status, not the private request text. It
asks what the caller needs and handles the new request with its own tools or
handoffs:
Do not let two prompts disagree
A task’s prompt and its owner’s prompt are written at different times and read together. When they disagree, the model picks one, and you cannot tell which. The pairing that bites most often is “do not run the same task again for a request that just finished” on one side and “act on the unserved request” on the other. A second booking is a new request, not a repeat of the last one, but nothing says so unless you write it. When you add a rule to a task, read the owner’s prompt in the same sitting and check it still agrees.Where the mechanics live
Tasks
Declaring a task, its saved values, and what success looks like.
Task groups
Tasks in a fixed order, and skipping a step whose work is already done.
Designing declared state
What to save, and who reads it next.
Choosing a context scope
What each scope costs, and what has to travel as a saved value.