Skip to content

Your first brain

A brain is a class. You subclass Brain, implement on_user_message, and yield speech. That is the whole required surface — everything else is a callback you add when you need it, and a method on session when it is not speech.

class Concierge(Brain):
async def on_user_message(self, session, msg):
yield SpeechStart()
yield Chunk(await self.answer(msg.text))
yield SpeechEnd()

That is a brain that runs. The Quickstart has the same class with its imports, its greeting and the route it mounts on; this page is about everything you can add to it.

on_user_message is an async generator and the only one. Speech is the only thing it yields, because speech is the only thing with a position on the audio timeline. An action, a language switch, hanging up — each is a method on session, callable from anywhere, including from a callback that is not a generator at all.

greet returns a string or None. It is async so you can look something up — session.init carries whatever your page passed at connect — not so you can generate the sentence. The caller is connected and hearing nothing while it runs, so a model call here is dead air before the first word.

Eight, and on_user_message is the only one you must implement. Two are generators — they are the two moments the floor is yours — and the other six return None, which is what stops a click or an error from talking over the caller.

CallbackFiresYields
on_session_start(session)Once, before greet. Load history, configure the voice.
greet(session)Once, after it. Return a string or None.
on_user_message(session, msg)The caller finished a turn. msg.text is the finalized transcript.speech
on_user_idle(session, idle)The caller went quiet. idle.level counts escalations, so nudge at 1 and wrap up at 3.speech
on_rtvi(session, msg)The app said something — a tap, a keystroke, a state sync.
on_finalize(session, fin)One speech unit finished playing. fin.heard is what the caller actually got.
on_error(session, error)Voqalize signalled something. The session is never killed by it.
on_session_end(session)Once, for any reason. Best-effort; it never blocks the close.

Everything that is not speech is a method on session and callable from any of them: session.dispatch(action), await session.configure(config), session.send_rtvi(...), session.end(reason).

on_finalize is the one people skip and then debug for a week. Write fin.heard into your history rather than what you generated — a barged-in reply that produced three sentences and delivered one must be remembered as one, or the model will reference things it never finished saying. Nothing in a metric shows this.

Signatures, types and the exact shape of every argument are in the Brain API.

SpeakingThe three frames, why a turn is many units, what streaming buys.
ActionsThe second channel — typed, rendered, never spoken.
ToolsLocal function calls, and what the clock costs you.
Context and historyWhat the caller does in the app, flowing back.
TranscriptsWhat was heard, which is not what you sent.