Voice
Voice calls run on a LiveKit project: a LiveKit server, on LiveKit Cloud or your own, that carries the audio between a caller and your agent. Your box (the container running your workspace, on your laptop under
stackbone devor deployed in your cloud) needs that project's URL, API key and API secret to open a room. You save the three once per box, in Settings › Voice settings in Studio. The key and the secret go in and never come back out.
Save the LiveKit credentials
Open Settings › Voice settings in Studio. A box with nothing saved says Not configured.
Paste the URL, the API key and the API secret from your LiveKit project's settings. The URL starts with
wss://, or withhttps://, which is the same server under the name the LiveKit server SDKs use. Studio refusesws://andhttp://before it sends anything: the room tokens your box signs travel to that address.Click Save. The status turns to Configured and shows the URL. The API key and API secret fields empty themselves.
The box keeps the three values in its own secret store, encrypted at rest. The change applies to the next call, with no restart.
What Studio shows afterwards
| Value | What the screen shows |
|---|---|
| URL | The address, in clear. It is an address, not a credential. |
| API key | Saved. Never the value, a mask of it or its length. |
| API secret | Saved. Never the value, a mask of it or its length. |
No screen, no route of the box's HTTP API and no CLI command returns the key or the secret. If you need them again, copy them from LiveKit.
LiveKit status
Once the credentials are saved, Studio shows whether your box can reach LiveKit with them. You see the status on the Voice settings screen and next to Call on an agent's Voice tab.
| Status | What it means |
|---|---|
| LiveKit reachable | The box asked LiveKit for its rooms with the saved credentials, and LiveKit answered. |
| LiveKit unreachable | The box could not reach LiveKit, or LiveKit refused the key or the secret. Check the URL, key and secret. |
| Not configured | The box has no credentials it can read. The Voice tab links to Voice settings, where you save them (again). |
The status only checks the credentials. It does not tell you whether a voice
worker (the process that joins the room and speaks for your agent) runs on that
LiveKit project: LiveKit has no API that lists them. Call stays available in
every status. Under stackbone dev, the terminal tells you when the worker is
ready: see Calls under stackbone dev.
The box keeps each answer for a few seconds, so Check again right after a
fix can show the old result for a moment. The check needs the runs:invoke
permission, the same one that places a call. Without it, Studio shows no status.
Replace or remove them
To replace them, type all three again and click Save. The box takes the URL, the key and the secret as one write, so a new URL never runs with a key that belongs to another project.
To remove them, click Remove and confirm. The three values are deleted together. Voice agents on this box cannot open a call until you save new ones.
Who can change them
Reading the status, saving and removing all need the secrets:rotate
permission, which the owner and admin roles hold. Any other role sees Your
role cannot open this on the screen.
Studio's Activity screen records each save with the URL and the person who saved, and each removal with the person who removed. Neither entry holds the key or the secret. Governance covers that trail.
They are not ordinary secrets
The box stores the three values under the names LIVEKIT_URL,
LIVEKIT_API_KEY and LIVEKIT_API_SECRET, and it reserves those names. They do
not appear on the Secrets screen, and you cannot create, replace or delete a
secret with one of those names there. A secret of your own with a similar name
(MY_LIVEKIT_API_KEY, LIVEKIT_URL_BACKUP) is an ordinary secret and stays
yours.
The screen calls GET, PUT and DELETE /api/voice-provider on your box. The
API reference your box serves documents
the three.
Calls under stackbone dev
A call needs a voice worker: the process that joins the LiveKit room, hears
the caller and speaks your agent's replies. Under stackbone dev you do not
start one. The CLI starts it on your machine when both of these are true:
- At least one deep agent in the workspace declares
voice: true. - The box has LiveKit credentials saved in Settings › Voice settings.
When LiveKit accepts the worker, the terminal prints:
◆ Voice worker ready (stackbone-voice-dev-1a2b3c4d)From then on, Call on the agent's Voice tab reaches that worker. While
the session runs, the CLI keeps the worker in step with your workspace and your
box. You do not restart stackbone dev for any of these changes:
| You | The CLI |
|---|---|
Add the first voice: true agent, or save credentials |
Starts the worker. |
| Remove the last voice agent, or remove the credentials | Stops the worker. |
| Save different credentials | Restarts the worker with the new ones. |
The first run installs the worker
The worker ships inside the CLI, at the same version as the box it talks to.
Its dependencies do not. The first time a session needs the worker, the CLI
installs them with npm into ~/.cache/stackbone/voice-worker/<cli version>/,
and the terminal shows Installing the voice worker while it runs. The
download is about 270 MB, most of it the ONNX runtime. On a slow network it can
take a few minutes.
Later sessions on the same CLI version reuse that folder. A new CLI version installs into a folder of its own. The folders of older versions stay until you delete them.
Each session has its own worker name
Each stackbone dev process picks a random worker name, stackbone-voice-dev-
followed by 8 hex characters, and its box sends every call to that name. No
other worker takes those calls, even on the same LiveKit project:
- a worker you deployed to LiveKit Cloud, which registers as
stackbone-voice; - the
stackbone devof another developer; - a second
stackbone devon your own machine.
So a worker on LiveKit Cloud is of no use under stackbone dev, and you need
none to test calls on your machine. Your dev worker does not take the calls of a
deployed box either.
The worker reaches the box at its local address, http://127.0.0.1:4242 by
default, also when the tunnel is open. Its requests to the box stay on your
machine.
When the worker fails
If the worker cannot install, cannot start or exits, the CLI starts it once
more. After a second failure, the CLI prints one warning with the cause and
stackbone dev keeps running with no worker. Calls stay unanswered until you
change the voice agents or the LiveKit credentials: either change starts a new
worker. If you have nothing to change, restart stackbone dev.
In Studio, a call that no worker joins within 15 seconds shows No voice worker joined the call.
A call under stackbone dev records nothing, even with recording on: LiveKit
cannot reach the bucket on your machine. See
When the box records nothing.
Remember callers
An agent can opt into reading and saving customer memory independently. Telephone
calls supply the original customer's number under phoneNumber; the agent must
have that fixed identity key to select it. Browser calls without an explicit test
identity continue without memory. Settings stay fixed for the call.
Reading recalls scoped memory before the first output. Saving runs once after closure and uses only the committed conversation the caller heard. Uncertain interrupted output is omitted. See Memory during voice calls for the four modes and identity contract.
Recording calls
Recording is off by default: the box keeps no audio of a call unless the agent turns recording on, since a caller may need to consent first. With it on, your box records each call of that agent to its own bucket as an audio-only OGG file, and Studio plays it back from the call's session. A recording that fails never ends the call.
Turn recording on for one agent
Set recording: true in the agent's voiceSettings:
import { defineDeepAgent } from '@stackbone/sdk/deep';
export default defineDeepAgent({
name: 'support-line',
model: 'openai/gpt-5-mini',
voice: true,
voiceSettings: { recording: true },
});The agent's Voice tab in Studio has the same switch, Record the call. A value saved in Studio wins over the one in code, and Reset puts the code value back. The change applies to the next call.
Give LiveKit a key for the recordings only
LiveKit uploads the recording to your bucket itself, so the box hands LiveKit the S3 key it uploads with. By default that is the box's own key, which opens the whole bucket. To hand LiveKit a narrower one, set these two variables on the box:
| Variable | What it is |
|---|---|
STACKBONE_VOICE_RECORDING_S3_ACCESS_KEY |
Access key that can write under <agent-id>/voice/recordings/. |
STACKBONE_VOICE_RECORDING_S3_SECRET_KEY |
Its secret key. |
<agent-id> is your box's STACKBONE_AGENT_ID, the prefix of every key the box
writes to the bucket. It is not the name of the voice agent. The recordings still
go to the same bucket and endpoint. Set both or neither: with only one, the box
refuses to start and names the two variables. The box never logs either value.
Play a recording
Open the call's session in Studio. The player reads a signed link that stays
valid for one hour, so you can seek through a long call. Playing it needs the
runs:invoke permission, the same one that places a call.
When the box records nothing
LiveKit cannot reach a bucket on your own machine or private network. When the
bucket address is localhost, a loopback or private IP, a name with no dot
(such as minio in a Compose network), or a name that only a private resolver
answers (.local, *.svc and *.svc.cluster.local in Kubernetes, .internal, .home.arpa),
the box opens the call as usual, records
nothing, and logs one line that names the address. This is the case under
stackbone dev.