Skip to main content
A call does not pass through admit. The voice() mixin from @telnyx/edge-runtime/voice runs STT and TTS, takes each end of turn from STT, and asks the class for one thing per utterance, a turn engine. By default that is the direct engine in managed/direct-turn.ts. The turn goes straight to the model with the conversation held in memory, and the confirmed exchange is written behind the call. With voice.harness true the harness itself is the engine. Each turn then runs the harness’s model and tools as a direct turn, not as a persistent run, at the cost of a second or more before the first word. defineManagedVoiceAgent in managed/voice.ts wraps the managed class in the mixin:
The mixin answers the call, runs barge-in and the keypad, and ends the call. The class overrides its hooks, and adds warmCall of its own.
  • warmCall runs before the call is answered, as The Call Webhook shows.
  • greeting is spoken when the call is answered. The mixin’s own says nothing.
  • turnEngine returns the engine above. The mixin’s own returns none.
  • speechProviders builds the STT and TTS providers from spec.voice.
  • reply wraps the mixin’s own reply in withFiller from managed/filler.ts. When the caller is confirmed done and voice.filler.afterMs pass without a word from the model, one short phrase is spoken as its own sentence, and the reply follows. The phrase is said, never written, so it reaches neither the message log nor the caller’s memory.
  • turnEnded logs the turn’s clock and writes the exchange to the message log after the reply has finished playing.
  • callEnded writes the last reply to state with setState() and, with memory on, pushes the call to Agent Memory under its id.
Each turn reaches the engine through reply:
super.reply(transcript, call) is the mixin’s own reply, which hands the transcript to the engine that turnEngine() returns.
  • transcript is STT’s first pass at the caller’s words, and call.confirmed resolves to the confirmed words.
  • While the turn is speculative, the mixin calls the engine’s begin() with the first pass, then commits the confirmed words or discards the turn.
  • Otherwise it calls the engine’s turn() with the confirmed words, and the result’s text is spoken.
  • A reply the caller cuts short is still a turn. The direct engine remembers the caller’s words and what was said of the reply before the cut, with the note [cut off by the caller] after it, in its history and in the journal; once playback reports what the caller heard, the entry is corrected to that. The next turn’s context holds both, so an interrupted “Sure, what is the new…” and the caller’s “12 Main St” line up.
With voice.harness true the engine is ready({ calls: true }), the recovered harness for calls, and both calls are its own. That harness leaves out every tool marked ask, since a call cannot wait for an approval; without any ask it is the same instance texts use.
  • begin() streams provisional text, with no writes or tools until the turn is committed.
  • turn() runs the input to completion and records the turn in the message log.
By default the direct engine answers both calls itself, with the AI SDK’s streamText and no harness. Over the mount, the voice class is the managed class unchanged except for webSocket(), with the same six RPC methods. Extending the Managed Agent has the hooks’ signatures.