> ## Documentation Index
> Fetch the complete documentation index at: https://developers.telnyx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The Voice Loop

```text {16,17,18} theme={null}
support-agent/
├── agent.json
├── instructions.md
└── .telnyx/                        written by deploy
    ├── index.ts
    ├── telnyx.toml
    ├── telnyx-env.d.ts
    └── package.json

@telnyx/acp/                      the package
├── managed/
│   ├── index.ts
│   ├── agent.ts
│   ├── model.ts
│   ├── memory.ts
│   ├── tools.ts
│   ├── mcp.ts
│   ├── voice.ts                                       ◀
│   ├── direct-turn.ts                                 ◀
│   └── filler.ts                                      ◀
├── channels/
│   ├── index.ts
│   ├── phone.ts
│   ├── email.ts
│   ├── web.ts
│   └── wire.ts
├── synth/
│   ├── index.ts
│   ├── code-entry.ts
│   └── vendor.ts
└── spec/
    ├── schema.ts
    ├── model.ts
    ├── phone.ts
    ├── project.ts
    └── io.ts
```

A call does not pass through `admit`. The `voice()` mixin from [`@telnyx/edge-runtime/voice`](/docs/agent-sdk/runtime-sdk) 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:

```ts theme={null}
// managed/voice.ts
export function defineManagedVoiceAgent(spec, host = {}) {
  const Base = defineManagedAgent(spec, host);
  class ManagedVoiceAgent extends voice(Base) {
    greeting(call) {
      this.nameCaller(call.from);   // the caller's number names their memory profile; the summary read starts here
      return defaultGreeting(spec);
    }
    // turnEngine, speechProviders, reply, turnEnded, callEnded, warmCall …
  }
  return ManagedVoiceAgent;
}
```

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](/docs/agent-sdk/inside/request-handling/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](/docs/agent-sdk/interface/messages) after the reply has finished playing.
* `callEnded` writes the last reply to state with [`setState()`](/docs/agent-sdk/interface/setState) and, with memory on, pushes the call to Agent Memory under its id.

Each turn reaches the engine through `reply`:

```ts theme={null}
// managed/voice.ts
reply(transcript, call) {
  const base = super.reply(transcript, call);
  if (!this.filler) return base;
  // …the same reply, wrapped in withFiller
}
// …
turnEngine() {
  const self = this;
  if (harnessOnCalls(spec)) return self.ready({ calls: true });
  return (this.direct ??= createDirectTurnEngine({
    model: voiceModel(spec, host, self.env),
    // …instructions, history and record
  }));
}
```

`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()`](/docs/agent-sdk/harness-sdk/harness/begin) streams provisional text, with no writes or tools until the turn is committed.
* [`turn()`](/docs/agent-sdk/harness-sdk/harness/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()`](/docs/agent-sdk/interface/webSocket), with the same six RPC methods. [Extending the Managed Agent](/docs/agent-sdk/extending#the-voice-hooks) has the hooks' signatures.
