> ## Documentation Index
> Fetch the complete documentation index at: https://unmute.ai/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Unmute compiles to exactly three targets. Pipecat and LiveKit are code targets: compile writes a Python project you run. SLNG is a hosted target: compile writes a deployment body and SLNG runs the agent, so it has no `unmute dev`. Those three are the only values `provider` accepts in `targets.yaml`. Deepgram and ElevenLabs appear in these docs as model vendors, which is not the same thing as a target, and `slng` is both.
> The Go structs in `internal/spec` and `internal/ir` are the schema truth. Check a field against them, or run `unmute validate`, rather than against what you remember.

# The dev loop

> What unmute dev actually does, how to pick a target, and where the logs are.

`unmute dev` is the loop you live in while building an agent. One command
compiles the package, starts the target's local runtime, and serves a web page
you can talk through.

```sh theme={null}
unmute dev examples/salon-concierge --target pipecat
```

```text theme={null}
compiled examples/salon-concierge/build/pipecat
starting the local Pipecat agent...

  ▸ http://localhost:8765/?agent=pipecat
    ctrl-c to stop  ·  logs: examples/salon-concierge/build/pipecat/dev.log
```

That names a package sitting somewhere else, which is why it carries a path.
The argument is optional: inside a package, `unmute dev` on its own runs the
directory you are standing in, and every path it prints is relative to there.

On this page:

* [The one way to run](#the-one-way-to-run) - one command
* [What it does, in order](#what-it-does-in-order) - the five things it runs
* [Every flag](#every-flag) - targets, ports, seeding
* [The two views](#the-two-views) - the transcript and the timings
* [Advanced](#advanced) - environment, seeding, more than one session

## The one way to run

A phone call reaches an agent that is deployed, so telephony is verified after
you deploy. [Phone calls](/telephony/overview) covers what a route is.
Deploying to [LiveKit Cloud](/deploy/livekit-cloud) or [Pipecat
Cloud](/deploy/pipecat-cloud) puts an agent where a real call can reach it.

## What it does, in order

<Steps>
  <Step title="Compile">
    The package is generated into `build/<target>/`. You do not run `unmute
            compile` first.
  </Step>

  <Step title="Start the runtime">
    Pipecat starts the generated `bot.py` directly with `uv`. This keeps its
    WebRTC ICE candidates reachable from the host browser. LiveKit uses Docker
    Compose because its stack also includes a local LiveKit server.
  </Step>

  <Step title="Serve the page">
    A small local web server opens your browser straight away, before the
    runtime is up. The page shows the startup output while it waits, so a build
    that fails is something you read rather than guess at.
  </Step>

  <Step title="Wait for ready">
    Pipecat waits for its `/status` response. LiveKit waits for the worker to
    register, up to three minutes. The call button stays unavailable until then,
    and the page moves to the conversation view once a call would be answered.
  </Step>

  <Step title="Clean up">
    Ctrl-c stops the local Pipecat process or removes the LiveKit stack. LiveKit
    data volumes are kept.
  </Step>
</Steps>

Pipecat browser development needs `uv`. LiveKit browser development needs
Docker with Compose.

## Picking a target

A package with one target needs no flag. With more than one:

* In a terminal, `unmute dev` asks which target to run.
* Without a terminal, for example in a script, it refuses and tells you the
  choices:

```text theme={null}
unmute: dev examples/salon-concierge: multiple targets declared; pass --target <name>: livekit (livekit), pipecat (pipecat)
```

`--target` takes exactly one name here, unlike `validate` and `compile`, which
take a repeatable list. A name that is not declared is an error:

```text theme={null}
unmute: dev examples/salon-concierge: target instance "nope" is not declared
```

## Every flag

<ParamField path="--target" type="target name">
  Which target to run. Needed when `targets.yaml` declares more than one. See
  [Picking a target](#picking-a-target).
</ParamField>

<ParamField path="--port" type="number" default="8765">
  The local web page you talk through.
</ParamField>

<ParamField path="--bot-port" type="number" default="7860">
  The host port the local agent runtime uses.
</ParamField>

<ParamField path="--var" type="name=value">
  Seed a declared variable before the call. See
  [Seeding variables](#seeding-variables).
</ParamField>

<ParamField path="--source" type="fact=value">
  Stand in for a fact the phone network would supply, such as
  `from_number`. See [Seeding call facts](#seeding-call-facts).
</ParamField>

<ParamField path="--no-open" type="switch, no value">
  Do not open a browser. See [Not opening the browser](#not-opening-the-browser).
</ParamField>

<ParamField path="--verbose" type="switch, no value">
  Print the runtime's own logs as well as the run's. See [Logs](#logs).
</ParamField>

### Ports

When a default port is busy, most often because another session holds it, a
free one is chosen and the printed URL names it. Pass the flag to pin a port; a
pinned port that is busy is a refusal:

```sh theme={null}
unmute dev examples/salon-concierge --target pipecat --port 8790 --bot-port 7890
```

The web page URL always carries the target name, for example
`http://localhost:8765/?agent=pipecat`.

## The two views

The page has two views and one set of call controls that stays put across both,
so switching never interrupts a call.

| View         | What it is for                                                      |
| ------------ | ------------------------------------------------------------------- |
| Conversation | streaming words, current activity, and expandable measurements      |
| Logs         | runtime output and activity or measurement updates labelled by call |

Recognized words appear as they arrive. Each finalized part becomes normal text
immediately, even while the model is still silent. The agent's text also grows
as it is generated. Generated words can lead audio playback; the page keeps
those states separate and marks interruptions or incomplete replies.

Model and tool activity appears before answer text. Each tool call has its own
row, even when two calls use the same name. Reported outcomes update that row;
`returned` means a result arrived, not that the business action succeeded.
A call into a task, or a handoff to another agent, gets its own row too,
labelled `HANDOFF`, and carries no duration: it hands control over rather than
returning a result. That row is what accounts for the model call that follows
it.
A reply summary shows its reported latency and model-call count. Each `LLM 1`,
`LLM 2`, and later row shows its own first-response and full-duration values
without expanding anything. TTS first-audio time and tool duration stay visible
too. **Debug details** holds secondary timings and source/model details. See
[Reading the latency numbers](/optimization/latency) for definitions. Counts appear only after
identified model calls arrive.

Only captured measurements appear. Missing and pending values have no
placeholder; a measured zero is `0ms`. Small positive values stay positive:
`1ms`, or `<1ms` below one millisecond. Timing values can overlap, so adding
stage values does not reconstruct reply latency.

The conversation follows new content until you scroll back. **Latest** returns
to the newest content with one action. Details keep their open state and work
with Enter or Space. Ending a call retains its words and marks unfinished text;
starting another call clears that history.

Live-data status is separate from audio status. Reconnecting the event feed
does not restart the microphone. If some history cannot be recovered, the page
keeps the incomplete-history label until the next call and shows observed model
calls labelled as observed. Late updates stay with their known exchange;
unassignable values remain labelled as unassigned. **Call diagnostics**, below
the conversation, holds those values and call-level first-speech measurements.
It starts collapsed and appears only when there is data to inspect; it does not
insert diagnostic sections between turns.

The `HANDOFF` row carries no duration on purpose. A task, a task group, or a
handoff to another agent reaches the framework as a function call, so it could
be timed like a tool. But a task or a task group does not return until
everything it started has finished, and a handoff never returns at all. Timed
that way, a three-turn task would appear as one very slow tool, which is the
opposite of what the numbers are for. The row marks the moment control moved,
and the model call that follows it is timed on its own.

[Reading the latency numbers](/optimization/latency) covers what each number means, where
the time goes in a voice turn, and how to tell which part is slow.

The logs view is in front while the target is starting, and the conversation
view takes over once the agent is ready. If anything in the startup output looked
like a failure, the logs view stays in front instead, and the count of those
lines sits on the tab so it is visible from either view. Pick a view yourself and
the page stops moving it for you.

Filter the output by typing, or narrow it to output, measurements, or problems.
There is no log-level parsing behind those buttons, just a match on the text,
which is why it does not break when a provider reformats a line.

**A failed start is now visible in the browser.** The page stays up so you can
read the output, the terminal still prints the error and the log path, and the
command still exits non-zero.

## Advanced

### Logs

**Every run writes its log into the build directory**, and prints the path when it
starts. That file is the first place to look when the page loads but nothing
speaks, and it is where a failure to start explains itself. It holds everything
the process printed, measurement lines included, so it always matches the run.

The log file is always `build/<target>/dev.log`. The path is printed with the
ready line, exactly as you will see it. From inside a scaffolded package, run
with no argument, that reads:

```text theme={null}
  ▸ http://localhost:8765/?agent=livekit
    ctrl-c to stop  ·  logs: build/livekit/dev.log
```

The file holds the local runtime's output and the reason a start failed. Raw
dev records include recognized and generated transcript text. The visible
measurement filter leaves out repeated transcript fragments. The file is
rewritten on every run, so the file you are looking at is
always this run.

To follow the same output in your terminal while it runs, add `--verbose`. The flag
means "follow container/agent logs on stderr", and without it the log goes to the
file only:

```sh theme={null}
unmute dev examples/salon-concierge --target pipecat --verbose
```

### Not opening the browser

```sh theme={null}
unmute dev examples/salon-concierge --target pipecat --no-open
```

The URL is still printed. Useful over SSH, and in scripts.

### Environment

`unmute dev` builds the run's environment in this order, with later files
winning:

1. your shell environment
2. `.env` in the directory you run the command from
3. `.env.local` in the directory you run the command from
4. `.env` in the package directory
5. `.env.local` in the package directory

So a repository wide `.env` can hold shared keys while `.env.local` overrides
it, and one package can override both. The generated
`build/<target>/.env.example` lists exactly what you supply.

### Seeding variables

`--var name=value` stands in for the values a production call would arrive
with. It is repeatable, and it only accepts variables declared with
`source: call_start`:

```yaml agent.yaml theme={null}
variables:
  customer_id:
    type: string
    source: call_start
    default: cus_1001
    description: CRM id of the caller.
```

```sh theme={null}
unmute dev my-agent --var customer_id=cus_2002
```

Leave the flag out and the `default` is used instead. See
[variables](/build/variables) for what happens when you try to seed something
else.

### Seeding call facts

`--source name=value` stands in for a fact the call itself carries, such as
the caller's number. It is repeatable, and it only accepts the eight facts a
call carries: `from_number`, `to_number`, `call_id`, `direction`, `carrier`,
`connection`, `session_id`, `stream_id`.

```sh theme={null}
unmute dev my-agent --source from_number=+34600111222
```

This seeds the fact, which a `prefetch:` entry then reads, so the run
exercises the pre-fetch, the confirmation marking, and the read back. On a
real call the carrier's own value wins: a seed only fills in what the route
gave nothing for.

Which facts a real call actually carries depends on the route: LiveKit's two
routes grant the most, and Pipecat's two Twilio routes grant a smaller set,
one direction only for a phone number. Seed whichever your target's route
grants; [Where it works](/build/prefetch#where-it-works) has the full grid.

Do not use `--var` to seed a caller's number. `--var` writes the variable
directly, so it skips the pre-fetch, marks nothing as awaiting confirmation,
and lets a local run act on a number it never read back. That is a path a
real call cannot take.

### More than one session at a time

Each `unmute dev` run of a LiveKit package is its own Compose project, named
from the package path and the target. Two runs from the same path share one
project, so the second replaces the first. Two runs from different paths, such
as two checkouts of the same agent, run side by side: each picks a free set of
LiveKit server ports and a free page port.

To pin the LiveKit ports yourself, move signaling, TCP fallback, and UDP media
as one set:

```sh theme={null}
LIVEKIT_HOST_PORT=7890 LIVEKIT_TCP_HOST_PORT=7891 LIVEKIT_UDP_HOST_PORT=7892 \
  unmute dev my-other-agent --target livekit --port 8766
```

A run stops its own stack when it ends, including on ctrl-c and on a closed
terminal. A stack whose `unmute dev` process was killed outright is stopped by
the next run, which says so. When a stack cannot be stopped, the run prints the
`docker compose` command to run by hand.

A second Pipecat run picks a free agent port the same way when `--bot-port` is
not passed.

## Where to go next

<Columns cols={2}>
  <Card title="Tracing" icon="chart-line" href="/tracing/overview">
    See the whole call after it ends: transcript, tool calls, and per-span
    timing.
  </Card>

  <Card title="Phone calls" icon="phone" href="/telephony/overview">
    What each route means, and how a call reaches a deployed agent.
  </Card>

  <Card title="dev reference" icon="book" href="/reference/cli/dev">
    Every flag, with defaults and requirements.
  </Card>

  <Card title="Latency" icon="gauge" href="/optimization/latency">
    What each number under a turn means, and which part to fix.
  </Card>
</Columns>
