--- name: edge-voice-agent-holds-call title: "Edge Voice Agent That Holds a Call" description: "Voice agent on Telnyx Edge Compute + Agent SDK — answers an inbound call and runs an STT → LLM → TTS conversation loop, all in the same PoP. Zero-credential inference via the [telnyx] binding." language: nodejs framework: telnyx-edge (Agent SDK) telnyx_products: [Edge Compute, Voice, AI Inference] --- # Edge Voice Agent That Holds a Call Voice agent on Telnyx Edge Compute + Agent SDK — answers an inbound phone call and holds a conversation loop with streaming speech-to-text, LLM inference, and text-to-speech, all in the same PoP. Uses the `[telnyx]` binding for zero-credential inference — no API key in code for the LLM. ## Why Telnyx Telnyx is an **AI Communications Infrastructure** platform — voice, messaging, SIP, AI, and IoT on one private, global network. This example composes Call Control, streaming transcription, text-to-speech, AI inference, and stateful actors on Edge Compute in a single deployable function — the flagship "voice agent that holds a call" that only Telnyx can ship because we own the telephony network. ## Telnyx API Endpoints Used - **Call Control**: `POST /v2/calls/{call_control_id}/actions/answer` — answer the inbound call - **Call Control TTS**: `POST /v2/calls/{call_control_id}/actions/speak` — text-to-speech for greeting and replies - **Call Control Transcription**: `POST /v2/calls/{call_control_id}/actions/transcription_start` — streaming STT (Google engine, inbound track) - **Call Control Transcription**: `POST /v2/calls/{call_control_id}/actions/transcription_stop` — stop STT before LLM turn - **Call Control Hangup**: `POST /v2/calls/{call_control_id}/actions/hangup` — end the call - **AI Inference**: `POST /v2/ai/openai/chat/completions` — via `this.env.TELNYX.ai.openai.chat.createCompletion()` (pre-authenticated binding, zero-credential) ## Architecture ``` Inbound call → webhook → VoiceAgent actor (one per call) │ ▼ ┌────────────────────────────────────────────────────┐ │ call.initiated → answer() │ │ call.answered → speak(greeting) │ │ speak.ended → transcription_start() │ │ call.transcription (final) │ │ → appendUser(transcript) │ │ → stopTranscription() │ │ → respond() ────────────────────────────┐ │ │ │ │ │ │ ▼ Agent SDK (Stateful Actor) │ │ │ ┌────────────────────────────────┐ │ │ │ │ this.messages.toOpenAI() │ │ │ │ │ env.TELNYX.ai.openai.chat │ │ │ │ │ .createCompletion() │ │ │ │ │ this.messages.add("assistant") │ │ │ │ └────────────────────────────────┘ │ │ │ → speak(reply) ◄────────────────────────┘ │ │ speak.ended → transcription_start() (loop) │ │ call.hangup → finishCall() (state persisted) │ └────────────────────────────────────────────────────┘ ``` ## Environment Variables | Variable | Type | Required | Description | |----------|------|----------|-------------| | `TELNYX_API_KEY` | secret | **yes** | Telnyx API key for Call Control REST (answer, speak, transcription, hangup) | | `[telnyx]` binding | toml | **yes** | Pre-authenticated Telnyx client for zero-credential AI inference | | `AI_MODEL` | env_var | no | Inference model name (default: `zai-org/GLM-5.2`) | > **Agent / CLI access** > > ```bash > # Buy a phone number for the voice agent > telnyx number-orders create --phone-number "+16282564655" > > # List your numbers > telnyx numbers list > ``` ## Setup ### Prerequisites - [Telnyx Edge CLI](https://github.com/team-telnyx/edge-compute/releases) v0.2.2+ - Node.js 18+ - A Telnyx phone number with a Call Control application ### 1. Clone and install ```bash git clone https://github.com/team-telnyx/telnyx-code-examples.git cd telnyx-code-examples/edge-voice-agent-holds-call npm install ``` ### 2. Configure secrets ```bash # Set your Telnyx API key as a secret on the Edge Compute function telnyx-edge secret set TELNYX_API_KEY your_telnyx_api_key ```
Programmatic / CLI setup ```bash # Buy a number (if you don't have one) telnyx number-orders create --phone-number "+16282564655" # Create a Call Control application telnyx call-control-applications create \ --application-name "voice-agent-holds-call" \ --webhook-url "https://edge-voice-agent-holds-call-.telnyxcompute.com/webhooks/voice" # Assign the number to the application telnyx numbers update +16244444444 --connection-id ```
### 3. Deploy ```bash telnyx-edge ship ``` `ship` prints a URL like `edge-voice-agent-holds-call-.telnyxcompute.com`. ### 4. Point your Call Control webhook In the [Telnyx Portal](https://portal.telnyx.com): 1. Create or edit a Call Control application assigned to your Telnyx number 2. Set the **Webhook URL** → `https://edge-voice-agent-holds-call-.telnyxcompute.com/webhooks/voice` ### 5. Test ```bash # Health check curl https://edge-voice-agent-holds-call-.telnyxcompute.com/health/liveness # Call your Telnyx number from your phone — the agent answers, greets, and converses ``` ## API Reference See [API.md](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/edge-voice-agent-holds-call/API.md) for the full typed endpoint reference. ### `POST /webhooks/voice` Receives Telnyx Call Control webhooks and drives the conversation loop. Events handled: | Event | Action | |-------|--------| | `call.initiated` | Record start, answer the call | | `call.answered` | Speak greeting | | `call.speak.ended` (greeting/reply) | Start streaming transcription | | `call.transcription` (final) | Stop transcription, run LLM turn, speak reply | | `call.hangup` | Finalize call state | ### `GET /debug/call?call_control_id=...` Inspect actor state for a call (phase, turn count, conversation history count, last message). ```bash curl "https://edge-voice-agent-holds-call-.telnyxcompute.com/debug/call?call_control_id=v3:abc123" ``` ### `POST /debug/respond` Run an LLM turn without a live call (for testing the inference binding). ```bash curl -X POST https://edge-voice-agent-holds-call-.telnyxcompute.com/debug/respond \ -H "Content-Type: application/json" \ -d '{"call_control_id":"v3:abc123"}' ``` ### `GET /health/{liveness,readiness}` Health checks. ## How It Works 1. **Inbound call** → Telnyx sends `call.initiated` webhook → the webhook handler creates a `VoiceAgent` actor keyed by `call_control_id` and calls `answer()` 2. **Greeting** → on `call.answered`, the handler speaks a greeting via Call Control TTS (`speak`) 3. **Listening** → when the greeting finishes (`call.speak.ended`), the handler starts streaming transcription (`transcription_start`, Google engine, inbound track only) 4. **Thinking** → when a final transcript arrives (`call.transcription` with `is_final=true`), the handler stops transcription, adds the user's speech to the actor's durable message history, and calls `respond()` on the actor 5. **Responding** → `respond()` reads conversation history via `this.messages.toOpenAI()`, calls `this.env.TELNYX.ai.openai.chat.createCompletion()` (zero-credential binding), adds the reply to history, and returns it for TTS 6. **Looping** → the reply is spoken via `speak()`; when it finishes, transcription starts again — the loop continues until the caller hangs up 7. **Persistence** — conversation history and call state (phase, turn count, timestamps) survive restarts in the actor's durable storage ## Agent SDK Primitives Used | Primitive | API | What it does | |-----------|-----|--------------| | Message History | `this.messages.add()` / `this.messages.toOpenAI()` / `this.messages.last()` | Durable conversation log per call | | Durable State | `this.setState()` / `this.getState()` | Per-call state (callId, from, to, phase, turnCount) | | Telnyx Binding | `this.env.TELNYX.ai.openai.chat.createCompletion()` | Zero-credential AI inference | ## Troubleshooting | Issue | Cause | Fix | |-------|-------|-----| | Call answers but no audio | TTS voice unavailable | Check `TTS_VOICE` in `src/index.ts` — try `female` as fallback | | No transcription events | Wrong transcription track | Ensure `transcription_tracks: "inbound"` — caller audio only | | LLM returns empty | Model unavailable | Check `AI_MODEL` env var — try `zai-org/GLM-5.2` | | `404 page not found` | Function still deploying | Wait ~30s, then retry | | Actor not processing | `[telnyx]` binding missing | Ensure `telnyx.toml` has `[telnyx] binding = "TELNYX"` | | `TELNYX_API_KEY not configured` | Secret not set | Run `telnyx-edge secret set TELNYX_API_KEY ` | ## Related Examples - [SMS Support Agent with Follow-Up (TypeScript, Agent SDK)](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/sms-support-agent-with-followup/README.md) - [Edge URL Summarizer (TypeScript)](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/edge-url-summarizer/README.md) - [Edge Prompt A/B Tester (TypeScript)](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/edge-prompt-ab-tester/README.md) - [Edge Agri Crop Advisory (TypeScript)](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/edge-agri-crop-advisory/README.md) ## Agent Discovery This example is part of the [Telnyx Code Examples](https://github.com/team-telnyx/telnyx-code-examples) catalog. - **Agent signup**: [telnyx.com/agent-signup.md](https://telnyx.com/agent-signup.md) — automated account provisioning via agent mail; get an API key with no human intervention - **Agent CLI**: [github.com/team-telnyx/ai/tree/main/cli](https://github.com/team-telnyx/ai/tree/main/cli) — composite commands for agents ([commands reference](https://github.com/team-telnyx/ai/tree/main/cli/src/commands)) - **Agent skills**: [github.com/team-telnyx/ai/tree/main/skills](https://github.com/team-telnyx/ai/tree/main/skills) - **Telnyx AI repo**: [github.com/team-telnyx/ai](https://github.com/team-telnyx/ai) - **LLM-optimized docs**: [`llms-full.txt`](https://developers.telnyx.com/llms-full.txt) - **Example index**: [`llms.txt`](https://raw.githubusercontent.com/team-telnyx/telnyx-code-examples/main/llms.txt) - **Telnyx CLI (human)**: [developers.telnyx.com/development/cli](https://developers.telnyx.com/development/cli) — `go install github.com/team-telnyx/telnyx-cli/cmd/telnyx@latest` ## Resources - [Agent SDK Overview](https://developers.telnyx.com/docs/agent-sdk) - [Agent SDK Quickstart](https://developers.telnyx.com/docs/agent-sdk/quickstart) - [Roll Your Own Agent](https://developers.telnyx.com/docs/agent-sdk/examples/roll-your-own) - [Stateful Actors Quick Start](https://developers.telnyx.com/docs/edge-compute/stateful-actors/quick-start) - [Call Control API Reference](https://developers.telnyx.com/api-reference/call-control) - [Streaming Transcription Guide](https://developers.telnyx.com/docs/voice/programmable-voice/transcription) - [AI Inference Guide](https://developers.telnyx.com/docs/inference) - [Edge Compute CLI](https://github.com/team-telnyx/edge-compute/releases) - [Telnyx Developer Docs](https://developers.telnyx.com) - [Telnyx Portal](https://portal.telnyx.com)