Overview

A bot with its own phone number answers calls through the call worker, a mode of the Voice Service binary: allternit-voice-service worker. A call reaches the bot’s number, the carrier sends it to self-hosted LiveKit SIP, and LiveKit dispatches it to the worker. The worker speaks first, holds the conversation, and writes the call into the bot’s thread as call.* events.
Status: the worker is built and unit tested, but it is not live yet. Two pieces have to land before a test call can go through: the Voice Session core (the streaming STT and TTS engine the worker drives), and the cloud-api routes for call start, events and turns. Spec: services/voice/spec/CALL_WORKER.md.

What the caller hears

Every call opens with a fixed line that bot settings can’t change. The line says the caller is talking to an AI assistant and whether the call is recorded, and then the bot’s greeting plays:
Hi, you’ve reached Acme Plumbing. I’m an AI assistant, and this call isn’t recorded. How can I help?
Recording is off unless the bot owner has configured recording consent. If recording is configured but isn’t available at that moment, the line says the call isn’t recorded, and nothing is recorded. The opening is spoken from cloud-api’s cached copy of the bot’s settings, so it starts within about a second even if the bot’s cloud computer is asleep. The disclosure and greeting are pre-rendered into the voice phrase cache when the call starts, so the caller hears them with little delay. If the bot takes more than about a second to start a reply, it says a short filler such as “One moment.”, and another every 6 seconds (up to 3) while a slow reply is still coming; they stop as soon as the reply starts or the caller speaks. A reply can take up to 60 seconds before the bot gives up and says it can’t reach its tools. The caller can interrupt the bot at any time; the bot stops talking immediately. Each turn goes to the bot’s own runtime, so the bot uses its normal tools, memory and thread on a call. If the runtime can’t be reached, the bot says “I can’t reach my tools right now; I’ll have someone follow up.” It never improvises an answer.

How a call flows

  1. Dispatch. The worker registers with LiveKit as agent allternit-voice. The phone number’s SIP dispatch rule sends each call to a call-… room and to this agent.
  2. Join. The worker joins the room, publishes the bot’s voice track, and reads the caller and number details from the SIP participant.
  3. Call start. The worker calls POST /api/v1/voice/calls on cloud-api. The response gives the call id and the bot’s name, voice, greeting and recording setting.
  4. Conversation. Caller audio goes to the Voice Session core. Each finished turn goes to cloud-api, which relays it to the bot’s runtime, and the reply is spoken as it streams back.
  5. Events. Transcript, keypad presses, state changes, transfers, takeovers and the call’s end are queued per call. They are delivered to the thread in order, with retries, so none are lost while the runtime wakes.

Controls from the phone screen

The thread’s phone screen sends hang up, mute (bot or caller), hold/resume, keypad (DTMF), transfer (cold, or warm: the bot briefs the person you’re sending the call to and connects them only if they accept), take over, hand back, and listen. cloud-api forwards each one to the worker over the room’s data channel, and the worker confirms it with an event so the screen updates. Hold plays soft music to the caller. During a takeover the bot goes quiet and the thread transcribes both the caller and you. After hand back, the bot picks up with the same context.

Events written to the thread

Running it

Builds without the call-worker feature, such as the Desktop sidecar, don’t include LiveKit or libwebrtc. In those builds, worker prints how to rebuild. The full list of environment variables is in the spec.