unmute validate, and
every change leaves you a package that runs.
The example is a parcel tracking line. A caller reads out a tracking number,
the agent looks it up and says when the parcel is due. It is small on purpose.
The shape is what matters, and you will swap the parcel for your own thing.
Tasks need a code target: Pipecat or LiveKit. The package
unmute init
writes targets LiveKit, so you are ready.SLNG compiles one agent with one prompt, so it refuses a task today and tells
you to fold the step into the agent’s instructions instead. Tasks on SLNG are
coming.What a task is
A task is a smaller conversation inside the call. It has its own prompt and its own tools. To the model, a task is one more entry in its function list, right next to the agent’s tools. When the model calls it, the task’s prompt takes over. When the task is done, the agent that started it, the owner, gets the caller back, along with whatever the task saved. You reach for a task when part of the call needs a narrower prompt, a smaller tool list, or an answer you want to keep. If none of those is true, stay with one agent and its tools. Most agents should. One word to settle before we start. A task is one step of the call. Some of the compiler’s messages say “step” for that reason. On this page the thing you write is always a task. On this page: nine steps, each ending with a package that validates, so you can stop anywhere and come back.- Start with no task - confirm the package you already have still works
- Give the agent a tool - a lookup, so there is something to move
- Say what you want to keep - declare the variables
- Add the task - move the tool onto it
- Save the answer -
assign: - Read it back - a placeholder in the owner’s prompt
- Say what success looks like -
finish:, so the tool ends the step - Open with the question -
announce:andopening: listen - Run it - talk to it in the browser
1
Start with no task
This is the package from the last page. Make sure it still validates:One agent, one prompt, one built-in tool so it can hang up. Keep this
picture in mind: each change below adds one thing to it.
2
Give the agent a tool
A tool does real work and comes back with a result. This one is a small
Python function that stands in for your delivery system. It lives in the
package, in two files.The tool file says what the tool takes and what it returns:The handler is a function with the same name as the tool:Look at the And tell the agent what to do with it, at the end of This already works, and for many agents it is enough. Stop here if the job
is one lookup and one answer. The rest of this page is for when you want the
lookup to be its own small conversation, and you want to keep what it
found.
tools/look_up_parcel.yaml
tools/look_up_parcel.py
status field in output:. It has an enum:, a fixed list
of the values it can hold. That list is what will let the task know, later,
whether the lookup worked. Write one for any tool a task will end on.Now name the tool in two places in agent.yaml. The agent’s own list says
the agent may call it. The top-level list says the tool exists.agent.yaml
instructions.md:instructions.md
3
Say what you want to keep
A task can save what it learns. You say what that is by declaring a
variable, once, with a type. Add this block to
agent.yaml, anywhere at
the top level:agent.yaml
Date is a built-in type. A value that is not a date in that form is
refused where it enters, and the model is told the form, so it can try
again. The description is read by the model, so write it for the model.Nothing reads these yet. Declaring a value and using it are two separate
things, and that is on purpose.4
Add the task
A task is written inside the agent that runs it, under Then add the task to the agent:It still validates, and it warns:Read that warning, because it is the most common mistake with a first
task. The model sees one list with the agent’s tools and the task in it,
and it takes the shortest route. If the agent can call The top-level The warning is gone. The model now has one way to look up a parcel, and it
goes through the task.
tasks:. It has a
name, a when: that tells the model when to run it, its own prompt file,
and its own tools.Make a tasks/ folder next to agent.yaml and put the task’s prompt in it:tasks/find-parcel.md
agent.yaml
look_up_parcel
itself, it will, and the task never runs. The fix is to move the tool: take
it off the agent and leave it on the task.agent.yaml
tools: list keeps look_up_parcel. That list says the
tool exists; the task now says who may call it.Change the agent’s prompt to match. It no longer looks parcels up itself:instructions.md
5
Save the answer
Right now the task can find the parcel, but when it ends, what it found is
gone. Here is what that line did. Every task gets a
assign: says which values the task saves, and where. Each line is
one variable you declared, and the tool result field it takes:agent.yaml
finish call, a function
the model calls to say the task is done. assign: gives that call two
arguments, tracking_number and delivery_date, typed from the variables
they land in. When the model calls finish, each value is checked against
its type. If one is wrong, nothing is saved and the task stays open, so a
bad date never gets written down.You do not write “call finish when you are done” in the task’s prompt.
The compiler adds that rule to the prompt for you. You will see the exact
words when you add finish:.A wrong name on the left is refused with the line it is on:6
Read it back
Saving a value does not show it to anybody. A prompt sees a saved value only
when it names it, with the variable in double braces. Add this to the end
of Before the task has run, an empty value renders as
instructions.md:instructions.md
none recorded yet., so
write the sentence so it still makes sense with those words in it. After
the task has run, the owner reads the real values on its next turn and can
answer without looking the parcel up again.7
Say what success looks like
So far the task ends when the model decides to call Read it as a sentence: “this task is finished when And every This is also where you can see what the compiler adds to the task’s
prompt. Run So the
finish. That is one
more model request after the lookup, and one more chance for the model to
do something else first. The tool already knows whether it worked: its
status is found or not_found. finish: lets that result end the
task:agent.yaml
look_up_parcel returns
status: found”. When that happens, the task saves its assign: straight
from the tool result and ends. The model is not asked, and the result never
reaches it. When the status is not_found, the result goes back to the
model as normal and the task stays open, which is exactly when you want the
prompt’s “ask the caller to check the number” line to do its job.Two rules keep this honest, and the compiler holds both. A
finish: entry
needs a success: check:success: value has to be one the tool declares in its enum:,
so a typo is caught here and not on a call that never ends:unmute compile my-agent and open build/livekit/agent.py.
Under your own instructions you will find these lines:finish call is still there. The model uses it when the caller asks
for something the task cannot do, and for a value the task already holds.
Your own prompt should not argue with those lines.8
Open with the question
A task’s first turn is normally a model request that writes an opening
line. This task always opens the same way: it asks for the tracking
number. Say that line yourself and skip the request:
agent.yaml
announce: is one fixed line, spoken exactly as written. opening: listen
says: speak it, then wait for the caller. The line is recorded as the
task’s own first turn, so the caller’s answer lands in a conversation the
task can see.Update the task’s prompt so it does not ask the question twice:tasks/find-parcel.md
9
Run it
.env. When the browser opens,
ask where your parcel is and read out one of the two numbers in the
handler, “A B one two three four five six”.Watch the dev page while you talk. The task shows up as its own row when
the model enters it, then look_up_parcel gets a row of its own, and then
the agent answers. If you never see the task’s row, the model went round
it. Check two things: that the agent does not hold the task’s tool, and
that the when: line describes what the caller actually said. Running it
locally explains every row on that page.The whole file
This isagent.yaml at the end, with the scaffold’s comments taken out. The
three lists near the top are the whole change: one tool on the agent, one task
nested under it, and the task’s own tool, finish: and assign:.
Complete agent.yaml
Complete agent.yaml
What you have now
Where to go next
Tasks
Every task key in full: history, what returns, sharing a task between agents.
Task groups
Two or more tasks that have to run in a fixed order.
Making a task actually run
Why the model skips a task, and what to give it so it does not.
Designing declared state
Choosing what to save, and writing prompts that read well when it is empty.