# Per-call agent usage metrics

> Where the Metrics panel's usage rows come from, how they differ from the live event ring, and what an externally hosted agent has to report.

The Metrics panel on an agent's Test drive screen shows what one call cost
the agent: tokens, audio seconds, TTS characters, and tool calls. These
numbers do not come from the live event feed next to them. They are a
separate, durable per-call record, fetched by call sid.

This page explains where those rows come from, why the panel can be empty
while the transcript is filling in, and what an agent you host yourself has
to report for the panel to have anything in it.

## Where usage rows are written

The panel is one query, keyed by the call sid of the drive that is running:

```ts
const metrics = await trpc.cdr.agentEvents.query({
  orgId,
  callId: callSid,
});
// → { rows: [ … ], totals: { prompt_tokens, completion_tokens,
//                            audio_seconds, characters, tool_calls } }
```

Two consequences of the key:

- **The query does not run until a call exists.** Test drive starts a call
  with `telephony.originate` (trunk `__test_drive__`, destination
  `agent:<agentId>@<orgId>`, app `AI_BIDIRECTIONAL_STREAM`) and only then has
  a `call_sid` to ask about. Before Start, the panel has nothing to show
  because nothing has been asked for.
- **One call's numbers can never leak into another call's panel.** Switching
  agents in the left rail keeps the screen mounted, so the screen clears the
  call sid explicitly; starting a second drive mints a new sid. Either way the
  query re-keys and the previous call's totals disappear rather than being
  carried forward.

`rows` holds the individual usage records; `totals` is their sum. A row lands
as a turn completes, so the panel grows during the call rather than appearing
all at once at the end. The console polls it while the call is live and
re-reads it after Stop, so the totals settle on the final turn instead of
freezing wherever the last poll happened to land.

A Test drive call has no call row and no SIP dialog behind it — the only
server-side record of ownership is the `voice:owner:<call_sid>` marker
`telephony.originate` writes for the test-drive trunk. Usage is keyed by the
sid itself, which is why a test drive still produces a metrics panel even
though there is no CDR-style call record to hang it off.

## The four totals

| Field               | What it counts                                                  |
| ------------------- | --------------------------------------------------------------- |
| `prompt_tokens`     | Model input tokens reported for the call.                        |
| `completion_tokens` | Model output tokens reported for the call.                       |
| `audio_seconds`     | Audio duration reported for the call.                            |
| `characters`        | TTS characters reported for the call.                            |
| `tool_calls`        | Number of tool invocations reported for the call.                |

If the response carries no `totals` object, the console substitutes zeros for
all five fields. So **a row of zeros means "nothing has been reported yet",
not "measured as zero"** — an agent that reports no usage and an agent that
has not yet finished a turn look identical in the panel. The distinction is
in whether `rows` is empty, and in the causes listed under
[Why a call shows no metrics](#why-a-call-shows-no-metrics).

Tokens are reported as two numbers, input and output, and the panel keeps them
separate. Do not add them and compare the sum against a provider's single
"total tokens" figure without checking which of the two that figure includes.

## Usage rows vs the live telemetry ring

The Live events pane immediately above the Metrics panel is a *different*
source, and the two fail independently.

| | Metrics panel | Live events pane |
| --- | --- | --- |
| Procedure | `cdr.agentEvents({ orgId, callId })` | `monitoring.agentEvents({ orgId, limit })` |
| Scope | one call sid | the whole org |
| Durability | durable per-call rows | in-memory recent-events ring |
| Shape | `rows` + summed `totals` | `enabled` + `events` |

`monitoring.agentEvents` returns whatever the telemetry consumer currently
holds for the org, newest first, capped by the `limit` you pass. It is a ring:
it holds recent traffic for **every** agent in the org, which is why the screen
filters it down to records that name the current call sid before rendering
anything. Older records fall out of it. Nothing in that pane is a record you
can come back to later.

The ring also reports whether it is running at all. When the telemetry consumer
is not configured, `monitoring.agentEvents` returns `enabled: false` with an
empty `events` array — permanently, not "not yet". The console reads that flag
so a deployment with the observability spine down is distinguishable from a
quiet call. **That flag says nothing about the Metrics panel.** Usage rows do
not travel through the ring, so the panel can be fully populated while the
events pane is switched off, and vice versa.

One overlap worth knowing: the conversation rail merges transcript turns from
the MoQT session with items derived from the ring, and tool calls appear there
because the ring carries them. The `tool_calls` number in the Metrics panel is
the durable count. If the rail shows a tool call the panel has not counted yet,
the turn has not finished; if the panel counts tool calls the rail never showed,
the ring dropped or never received them.

## Reporting usage from an externally hosted agent

An **external agent** is one you run yourself — `agent_config.external_handle`
is non-null on the agent — using LiveKit Agents plus
`livekit-plugins-telequick`. Only its media crosses the
TeleQuick engine. The model calls, the TTS, and the tools all happen inside
your worker process.

That is the whole reason this section exists: the engine cannot meter what it
does not run. For an in-engine agent, the turn machinery that calls the model
is the same machinery that writes the usage row. For an external agent there is
no such point, so **your worker must report its own usage** — `sendUsage` for
token / audio / character accounting and `sendToolCall` for tool invocations.
Whatever your worker does not send does not appear in the panel, and the
corresponding total stays at its zero default.

Two further things follow for external agents on this screen:

- **Text mode is unavailable.** Typed turns run through the in-engine model,
  which an external agent does not have. The console offers the Text toggle
  greyed out with that reason rather than hiding it, and forces the mode back
  to Voice when you switch to an external agent.
- **Presence is a precondition.** See below.

## Worker presence before a test drive

You cannot start a drive against an external agent whose worker is not
connected. The engine dispatches the call to a present worker and refuses when
there is none, so the console checks first:

```ts
const presence = await trpc.mediaApps.presence.query({
  orgId,
  agentConfigId: agentId,
});
// Start is enabled only when presence.connected === true
```

How the screen behaves around it:

- For an in-engine agent the check is skipped entirely and Start is always
  enabled.
- For an external agent, Start is disabled until `connected` is `true`, with
  the hover reason *"Your agent worker is not connected — start it first."*
- The screen shows one of four states: `checking worker…` while the query is in
  flight, `worker status unavailable` if it errors, `worker connected`, or
  `worker not connected` with a hint to start your agent process against the
  connect URL from the External agents screen.
- While no call is running, presence is re-polled, so a worker you start in
  another terminal enables the button without a page reload.
- Polling stops for the duration of a call. The engine holds the worker then,
  and presence going blank mid-call means the worker dropped — which the audio
  watchdog surfaces on its own.

Without this line, a missing worker and a broken media bridge were the same
symptom: a live pill over dead air, and a Metrics panel that never filled in.

## Ending the session so totals settle

Closing the browser tab or navigating away does **not** end the agent session.
The engine is told nothing by a dropped MoQT session: the provider WebSocket
stays open (and billable) on a call nobody is on, the relay uplink stays armed,
and an external worker stays assigned to a job whose caller is gone.

The Stop button, leaving the screen, and switching agents all call the same
release:

```ts
await trpc.telephony.testDriveStop.mutate({ orgId, call_sid });
// → { ok: true, engine } | { ok: false, engine: <error message> }
```

It checks the `voice:owner:<call_sid>` marker against your org — a sid that is
not yours is rejected — then invokes `agentruntime.stop`, which closes the
provider session and tears down the relay uplink, sending an external worker
its shutdown over the media transport. On success the ownership marker is
deleted.

The call is best-effort by contract. The screen has already hung up, so a
failure is logged rather than raised at you. If usage keeps climbing on a drive
you thought you ended, this is the first thing to check.

## Why a call shows no metrics

Work down the list; the causes are ordered by how often they are the answer.

1. **No call sid.** The query is disabled until a drive is running. Idle screen,
   empty panel.
2. **No turn has completed.** Rows land as turns complete. If the session shows
   *"Connected, but the agent's audio track hasn't delivered anything yet"*,
   there is nothing to total yet — your mic is still going through, so watch the
   transcript fill in first.
3. **An external agent that does not report.** If the agent has an
   `external_handle` and your worker never calls `sendUsage` / `sendToolCall`,
   the totals stay at their zero defaults forever. This is the single most
   common cause of a permanently empty panel on a call that otherwise worked.
4. **You are reading the wrong emptiness.** An empty *Live events* pane with the
   feed reported as off is the telemetry consumer being unconfigured, which has
   no bearing on usage rows. Check which pane is empty before you go looking at
   the observability spine.
5. **The panel re-keyed.** Switching agents or stopping the drive clears the call
   sid, and the panel empties by design rather than showing the last call's
   numbers. Restart the drive to get a fresh record.
6. **Audio never reached you, so no turn happened.** If `gatewayConnectInfo`
   returns `enabled: false`, the call still originates and events still show,
   but you will not hear the agent; likewise if the browser is holding playback
   or the subscribe failed. Those are media-path problems, but a conversation
   that never happened produces no usage to total.

## Related

- [Telemetry](/platform/telemetry) — metrics, traces, and the durable CDR stream
- [Telephony Metrics](/glossary/metrics) — the call-quality numbers, as opposed to agent cost
- [Authentication](/concepts/authentication) — API keys for the control plane, relay tokens for the media leg
