What it gives you
CallState
unmute_core.state holds what one call knows. It keeps typed values, whether the caller confirmed each one, and how each value reads in a prompt. It works on any Pydantic model.
state.py
TypeError when you build the CallState.
state.names lists the value names in the model’s order. state.name_set gives the same names as a frozenset, for a quick membership test:
Every argument CallState takes
type[BaseModel]
default:"NoValues"
The Pydantic model class of the state. Every field needs a default. Anything else raises
TypeError. The default, NoValues, is for an agent with no values.Mapping[str, str] | None
default:"None"
Value name to the task step that must hear the caller agree to it.
Mapping[str, Sequence[str]] | None
default:"None"
A looked-up value name to the value names it was looked up from.
Mapping[str, str] | None
default:"None"
Value name to
required or optional, for the values a dispatch sets.CallInfo | None
default:"None"
What the call carries, as
state.call. None gives a web call with a new session_id. Build one with call_info, sip_call_info or telephony_call_info.agent_id to CallInfo, call_info, sip_call_info or telephony_call_info, or set UNMUTE_AGENT_ID. With neither, building the call state raises SettingsError. See Settings.
How a save works
state.save(values)checks the whole batch against the model first.- If one value is unknown, does not fit its type, or is longer than 64,000 characters,
saveraisesStateRefused. Nothing from the batch is saved. StateRefused.messageis worded for the model, likecaller_phone: expected a phone number with its country code. A task step shows it to the model, so the model can ask the caller again.- Otherwise every value is saved. Phone numbers, dates and the like are saved in their clean form.
- Read
state.valuesif you like, but never write to it.saveis the one way in.
save_values tool, or by your own code calling state.save.
A task step may try to save before the caller said anything in it. The state keeps that save aside. It is a held save. When the call ends, the library decides whether to keep it. That decision is to settle it. Held saves explains how.
Confirmation
confirm maps a value name to the task step that must hear the caller agree to it.
- A value in
confirmstarts unconfirmed. - It becomes confirmed when its own step saves it with something in it.
- Any other save of it makes it unconfirmed again. The caller must agree again.
state.withdraw("verify_phone")takes back every confirmation that step gave. The step runs again.state.is_confirmed(name)is true when the value is filled and its confirmation stands.
dependencies covers a value that was looked up from other values: CallState(State, dependencies={"customer": ["caller_phone"]}). It is confirmed only while all of those values are filled and confirmed.
A dependent value that no step confirms reads empty in every prompt until its sources are confirmed. One that a step confirms reads in that step’s prompt too.
A value saved by a prefetch Lookup also remembers what it was read from. When one of those inputs changes, the value goes back to its default. See Prefetch.
How a value reads in a prompt
state.render(path, site="") gives the text a prompt shows.
A value that waits for confirmation reads as empty. The one exception is the step that confirms it. Pass
site="task:verify_phone" and that step sees the value, so it can read it back to the caller.
state.plain(path) gives JSON data instead of text. A date reads "2026-10-02". A dotted path reads into a shape: state.plain("customer.status"). A path through an absent value gives None. A name the state does not hold raises KeyError.
state.snapshot(site="") gives every value as render does. The model call uses it to fill the prompt.
Filling a prompt
{{caller_phone}}. A path into a shape is {{customer.status}}. The same path also reads {{customer__status}}, the form the SLNG Context Router reads.
- Each
{{name}}is found. Spaces inside the braces are fine:{{ name }}. - Each name is rendered with
state.render. So a value that waits for confirmation readsnone recorded yet. - A task step passes its own site, like
task:verify_phone. A site is the place in the agent where a prompt is used:task:<name>for a task step, none for the main prompt. At its own site, the step sees the unconfirmed value. - The fill runs in one pass. A value that holds
{{x}}is not filled again. - A saved value reaches the model whole. A native model call never cuts it.
- A name the state does not hold raises
MissingVariablesError. Itsnameslist every missing name.
render_prompt
str
required
The prompt, with
{{name}} placeholders.CallState | None
required
The call state. None works for a prompt with no placeholders.
str
default:"\"\""
The prompt reading the values, like
task:verify_phone.render_prompt again after a save, so the prompt shows the new values. Two lower-level helpers, template_variables(messages, state, site) and fill_messages(messages, variables), work on chat messages. They fill only system and developer messages.
Which one to use
ModelCall, the prompt stays unfilled in the request. The model call fills it, natively or through the router. The model reads the same final prompt either way. See Model call.
Values the call starts with
call_start= names them, like confirm= does.
requiredmeans the call cannot start without it.optionalmeans it may be missing.save_call_start(state, dispatched, *, env=True)readsstate.call_start.dispatchedis thecall_startobject a call is dispatched with.UNMUTE_CALL_STARTgives the same for a local run, and the dispatch wins. Passenv=Falseto read only the dispatch.- With a mapping, only those names are taken. A missing required name raises
CallStartError. - Without a mapping, any State value may be set and none is required.
- Other names are dropped, and the log names them. A value that does not fit raises
CallStartError, and nothing is saved. - The origin kind of these saves is
call_start. - A name in
call_startthat the model does not have raisesValueError. So does a rule other thanrequiredoroptional, and a name that is also inconfirm. The caller is never asked for a value the call starts with.
save_call_start runs.
How they read
- An empty call-start value reads
""in a prompt. Other empty values readnone recorded yet.. - An optional call-start value never refuses a tool. A
{{brand}}in an argument, a locked argument or an announce line reads as empty text. - The save tool (
values_tool) leaves call-start values out. A call that names one is refused withbrand is set when the call starts. It cannot change. values_blockshows each one as “set when the call starts”.
In a schema
StateSchema row has a call_start field: "required", "optional" or none. schema.call_start gives the mapping, like schema.confirm does. Pass it on to CallState.
Values the call carries: state.call
CallState holds one more value. Its name is call. It holds what the call itself carries: who is on it and how it came in. The library fills it. Your State class does not declare it. State lists its fields.
CallState() with no model works too. It uses NoValues, for an agent that has no values of its own.
More on the fields:
session_idis new for each call and never changes. It is the same on every turn, task and handoff, on LiveKit and on Pipecat.carrier_call_idis notsession_id.- The library saves
first_user_messagefrom what the caller said first, andend_reasonas the call ends. - A withheld or malformed number reads as
None. It is never an error.
The three cases
- Inbound phone call. The caller is
calling_number. The agent’s own number iscalled_number.remote_numberis the caller. - Outbound phone call. The agent’s own number is
calling_number. The customer iscalled_number.remote_numberis the customer. - Web call.
channelisweb. There are no numbers and nodirection. Thesession_idis still set.
call object in the call-start dispatch. save_call_start(state, dispatched) reads it, so your agent must call it:
- On a web call, every field you give applies.
- On a phone call, the line wins. Its own
channel,direction,calling_number,called_numberandcarrier_call_idstay. The dispatch fills only the empty ones. agent_idandsession_idalways apply.
Where it reads
- Python reads
state.call.calling_number. - A prompt, a task text, a greeting, an announce line and a locked tool argument read
{{call.calling_number}}and{{call.session_id}}. - An empty call value reads
""in a prompt. - A tool that locks an empty call value is refused:
this tool reads call.carrier_call_id, which this call does not carry. state.names,values_block,snapshot,save_valuesandstate.dump()leavecallout.
Read-only
Only the library writescall. state.save({"call": ...}) is refused unless the save comes from the library.
A State value named call raises ReservedNameError. It is a ValueError. The same holds for a StateSchema variable, an alias, a confirm, dependencies or call_start rule, a task assign and a prefetch assign. The message says:
caller_phone.
Confirming the caller’s number
state.call is read-only, so the caller cannot confirm it there. Copy it into your own value with a CallValue prefetch entry. Then confirm that value in confirm=.
verify_phone then asks the caller to agree. See Prefetch.
Saving values no task saves
Some values belong to no task. The model learns them in free talk.values_tool gives the model a save_values tool for those values. values_block writes the values block: a short system message that lists each value and what it holds now.
values_block gives text like this:
values_toolbuilds thesave_valuestool. Each free value is an optional argument that may be null.nullputs a value back to its default.ownersmaps a value to the task that saves it. UseTasksSpec.owners().- A value a task saves is refused with
<name> is saved by the task <task>. Start that task to change it. - A call-start value is left out and refused too, with
<name> is set when the call starts. It cannot change. - The tool returns
{"ok": true, "saved": {...}}. - The tool saves the batch as a whole, like
state.save. A value that does not fit refuses all of them. - If every value belongs to a task or to the call’s start, there is nothing to save.
values_toolraisesValueError.free_values(state, owners)lists the values left to save, so you can check first. AgentSpec(save_values=True), on either adapter, shows the block whenever State has values. It offers thesave_valuestool only when a value is free. With none free, the block’s first line names no tool.
values_tool
CallState
required
The call state.
Mapping[str, str] | None
default:"None"
Value name to the task that saves it.
str
default:"save_values"
The tool name.
values_block
CallState
required
The call state.
Mapping[str, str] | None
default:"None"
Value name to the task that saves it.
str
default:"save_values"
The save tool’s name, as the block names it.
Turn it on
State showssave_values=True on UnmuteAgent and UnmuteFlows. On a plain LiveKit agent:
Plain LiveKit agent
save_values=Trueon the spec gives that agent the tool. An agent the call is handed to has its ownsave_values. Each main-agent model request ends with the block as its last system message. A step’s request gets none.save_values=Trueneeds a state. Without one,UnmuteAgentandUnmuteFlowsraiseValueError.- On a plain LiveKit agent,
filled_llmruns the agent’s own LLM plugin. It fills the prompt from the state on each request. Withsave_values=Trueit ends the request with the block. - On a plain Pipecat pipeline, give
tools_schema(registry, state)to the context. Thenawait fill_prompt(llm, worker, PROMPT, state, save_values=True)before the pipeline runs. It sends the filled prompt as the service’s system instruction, and sends it again after each save. See Pipecat. is_values(role, text)fromunmute_core.toolstells whether a message is that block. It is asystemmessage that starts with the block’s header. Use it to drop the old block from a chat before you add a new one.
Building a State from type text
StateSchema(variables, shapes) checks the rows and builds the Pydantic class. schema.model is that class. schema.confirm and schema.call_start are the maps for CallState. The class and a hand-written one run through the same CallState.
- Every value starts empty. A list starts as
[]. Anything else starts asNone. - Empty text counts as no value.
- A
Variableholdsname,description,type(text by default),confirmandcall_start. - A
Shapeis a named group of fields, likeContact. Name a shape in the type text. - A field of a shape is required unless its type ends in
| None. - A bad row raises
StateSchemaErroror a Pydantic error. It names the row and the fix.
The type grammar
A type is plain Python type text.parse_type("Phone | None").canonical gives the one canonical spelling.
A type the grammar does not take raises
TypeTextError. Its message says what to write instead. For example, datetime is refused. Write Date and Time as two fields.
Value types
A model may also use these classes fromunmute_core.state directly, as State does in the CallState example.
Phonedrops spaces, dashes, dots and brackets. A leading00becomes+.- A phone number with no plus is saved only when one valid reading exists. The call’s country can help. Enter
use_phone_region(...)with the country the call is in. Prefetch shows how. - A bad number is refused with a reason the model can act on. It may be too short, too long, or missing its country code.
NameEmailreadsJo Bloggs <jo@example.com>or a bare address. Paths read its parts:{{contact.email}}.- The
formatlets a tool schema or another reader name the type. - A bad
Idis refused with the words: “expected an identifier: letters, digits, and then any of dot, dash, underscore or colon”. AnIdhas at most 64 characters. The refusal shows no pattern. - A number sent to a text value is saved as its text. NaN and infinity are refused there too.
- A whole float reads as an integer in a prompt, a tool result, a webhook URL placeholder and
CallState.snapshot():3.0renders as3. A locked argument that is a whole"{{price}}"still sends the float. - A choice (
Literal[...]) is matched with the spaces around the answer and the choices ignored, so a padded answer still fits. The value saved is the choice as declared," small"included. - Code says a
BirthDatewith its year and no weekday, like “June 5, 1979”, and aSpelledcode a character at a time, in groups, like “K P, 4 8 2, 9 1 3”. A voice then reads each character on its own. Check a code’s pattern with your own validator. - A variable name may start with an underscore or be a
model_*name. It works as any other name, in the prompt, in a tool’s arguments and in a task’s saves.
Listening to changes
- A listener gets one
StateEventper save that changed something. A save that changed nothing sends nothing. event.changeslists each value that changed. A change holdsfield,valueas JSON,confirmed, andby.confirmedisNonefor a value that never waits for the caller.byis aStateOrigin. Itskindis one of nine:step,prefetch,call,call_start,tool,event,withdraw,startorreset. Any other kind raises aValidationErrorwhen you build the origin.listen(listener, replay=True)first sends the values already set, as one event of kindstart.- A listener that raises is logged and removed. It never fails the save.
Who supplies a value
When a tool needs a value that is not set, the refusal tells the model which task saves it. See Tools. The state keeps that map instate.suppliers.
set_suppliersreplaces the earlier map. It does not merge.- The adapters call it when a task plan runs. You call it only when you build your own task host.
- Write to the map only through these two methods.
Troubleshooting
TypeError when you build the CallState
The first argument is not a Pydantic model class, likeCallState(int). Or a field has no default. Every value starts the call empty.
Fix: pass your State class, or schema.model. Give each field a default.
StateRefused from a save
A value is unknown, does not fit its type, or is too long. Nothing from the batch was saved.StateRefused.message says which value and why.
Fix: correct the value and save the batch again. In a task step, the model reads the message and asks the caller again.
MissingVariablesError when a prompt fills
The prompt names a{{name}} the state does not hold. Its names list every missing name.
Fix: add the value to the model, or correct the name in the prompt.
ValueError from values_tool
Every State value belongs to a task or to the call’s start, so there is nothing to save. Only a directvalues_tool call raises it. AgentSpec(save_values=True) shows the block and offers no tool instead.
Fix: build the tool only when a value is free, or use save_values=True.
Where to go next
State
Define, save, confirm and read a call’s values.
Prefetch
Fill values before the greeting.