Back to blog
Gophone: A Go-Native Programmable Voice AI Platform

Gophone: A Go-Native Programmable Voice AI Platform

Building a production voice AI system means solving a stack of hard problems: provisioning SIP trunks reliably, managing WebRTC sessions, bridging telephony signaling with AI inference, and deploying it all without an army of infrastructure engineers. Most solutions reach for a cloud API or a heavy PBX — either locking you into per-minute pricing or forcing you to manage a complex telephony stack alongside your AI logic. Gophone takes a different approach: a single Go binary that handles SIP trunking, WebRTC media, webhook-driven call flows, and a native Voice AI agent — all CGO-free, all locally deployable with systemd and Litestream.

Construir un sistema de voz IA de producción significa resolver una pila de problemas difíciles: aprovisionar troncales SIP de forma confiable, gestionar sesiones WebRTC, puentear la señalización telefónica con inferencia de IA y desplegarlo todo sin un ejército de ingenieros de infraestructura. La mayoría de las soluciones recurren a una API en la nube o a una PBX pesada — ya sea bloqueándote en precios por minuto o forzándote a gestionar una pila de telefonía compleja junto a tu lógica de IA. Gophone toma un enfoque diferente: un único binario Go que maneja troncales SIP, medios WebRTC, flujos de llamada basados en webhooks y un agente de voz IA nativo — todo CGO-free, todo desplegable localmente con systemd y Litestream.

Architecture: The Transactional Outbox Pattern

Arquitectura: El Patrón de Outbox Transaccional

Gophone’s control plane is built around a transactional outbox pattern. When you provision a SIP trunk via gofone trunk create, the system atomically writes the trunk record and enqueues a sync job in the same SQLite transaction. An event-driven reconciler — woken instantly by a DB signal channel (with a 30-second fallback ticker) — picks up the job and provisions the trunk on the LiveKit SIP Gateway via gRPC. On failure, the reconciler retries with exponential backoff. The trunk’s SyncStatus (pending, synced, failed) is the single source of truth shared across Go and SQL, with typed constants in the domain layer. This means no polling loop, no missed jobs, and a clear audit trail for every trunk operation.

El plano de control de Gophone está construido alrededor de un patrón de outbox transaccional. Cuando provisionas una troncal SIP vía gofone trunk create, el sistema escribe atómicamente el registro de la troncal y encola un trabajo de sincronización en la misma transacción SQLite. Un reconciler guiado por eventos — despertado instantáneamente por un canal de señal DB (con un ticker de respaldo de 30 segundos) — recoge el trabajo y provisiona la troncal en la puerta de enlace SIP de LiveKit vía gRPC. En caso de fallo, el reconciler reintenta con retroceso exponencial. El SyncStatus de la troncal (pending, synced, failed) es la única fuente de verdad compartida entre Go y SQL, con constantes tipadas en la capa de dominio. Esto significa sin bucle de polling, sin trabajos perdidos y una pista de auditoría clara para cada operación de troncal.

The Voice AI Agent: STT → LLM → TTS Loop

El Agente de Voz IA: Bucle STT → LLM → TTS

When a call arrives, the engine joins the LiveKit room as an AI participant. The agent subscribes to the caller’s Opus audio and streams it to Deepgram for live STT at sample_rate=48000 with encoding=opus. Transcribed text goes to an OpenAI-compatible LLM, and the LLM response is synthesized via Cartesia raw Opus or an OpenAI-compatible TTS endpoint, written directly to track.WriteSample. The entire loop is CGO-free — pure Go Opus streaming from WebRTC through to the speech provider. The agent maintains multi-turn conversation history across the call (capped at 40 messages), giving the LLM contextual awareness of everything said so far.

Cuando llega una llamada, el motor se une a la sala de LiveKit como un participante IA. El agente se suscribe al audio Opus del llamante y lo transmite a Deepgram para STT en vivo a sample_rate=48000 con encoding=opus. El texto transcrito va a un LLM compatible con OpenAI, y la respuesta del LLM se sintetiza vía Cartesia raw Opus o un endpoint TTS compatible con OpenAI, escrita directamente en track.WriteSample. El bucle completo es CGO-free — streaming Opus puro en Go desde WebRTC hasta el proveedor de voz. El agente mantiene historial de conversación multi-turno a través de la llamada (limitado a 40 mensajes), dando al LLM conciencia contextual de todo lo dicho hasta el momento.

During tool execution — when the LLM decides to look something up — the agent plays async filler audio from a rotating set of prompts in a background goroutine (“Let me check that for you…”, “One moment please…”). The filler stops automatically when the tool response arrives. This means callers hear natural speech during processing gaps rather than dead silence. The barge-in system uses acoustic noise-gate logic: only the first non-empty interim transcript triggers an interrupt. SpeechStarted events (triggered by coughing, door slams, or any VAD energy) are explicitly ignored — they never cause false interrupts. Call transfer is available as an opt-in LLM function tool (transfer_call) via local_tools.enable_call_transfer.

Durante la ejecución de herramientas — cuando el LLM decide buscar algo — el agente reproduce audio filler asíncrono de un conjunto rotativo de prompts en una goroutine de fondo (“Déjame verificar eso…”, “Un momento por favor…”). El filler se detiene automáticamente cuando llega la respuesta de la herramienta. Esto significa que los llamantes escuchan habla natural durante los intervalos de procesamiento en lugar de silencio muerto. El sistema de barge-in usa lógica de puerta de ruido acústica: solo el primer transcript provisional no vacío dispara una interrupción. Los eventos SpeechStarted (disparados por tos, portazos o cualquier energía VAD) se ignoran explícitamente — nunca causan falsas interrupciones. La transferencia de llamada está disponible como una función herramienta LLM opt-in (transfer_call) vía local_tools.enable_call_transfer.

Dynamic MCP Tool Discovery

Descubrimiento Dinámico de Herramientas MCP

Gophone’s agent auto-discovers every tool from every connected MCP server at startup via session.ListTools. Declare Postgres databases, CRM APIs, or internal services as MCP servers in gofone.yaml, and the agent registers their tools with the LLM — no Go code to write, no new deployments. An operator connecting a business database has exactly one step: write the MCP server URL in the YAML config. Tool visibility can be scoped per agent in multi-agent deployments via mcp_tool_servers, so a sales agent sees CRM tools while a support agent sees ticket tools — all from the same engine binary.

El agente de Gophone auto-descubre cada herramienta de cada servidor MCP conectado al inicio vía session.ListTools. Declara bases de datos Postgres, APIs de CRM o servicios internos como servidores MCP en gofone.yaml, y el agente registra sus herramientas con el LLM — sin escribir código Go, sin nuevos despliegues. Un operador que conecta una base de datos de negocio tiene exactamente un paso: escribir la URL del servidor MCP en el archivo YAML. La visibilidad de las herramientas puede limitarse por agente en despliegues multi-agente vía mcp_tool_servers, así que un agente de ventas ve herramientas CRM mientras que un agente de soporte ve herramientas de tickets — todo desde el mismo binario del motor.

Multi-Agent Handoff

Transferencia Multi-Agente

Gophone supports declarative multi-agent fleets defined in YAML — each agent with its own system prompt, TTS voice, greeting, and tool set. A single handoff LLM function tool swaps the active state sub-millisecond, keeping the WebRTC session alive. The initial greeting and system prompt for the new agent take effect on the next turn. This means a single phone number can route to different specialized AIs: a receptionist triages the call, hands off to a sales agent, who then transfers to a support agent — all seamless, all configurable in YAML, no code changes.

Gophone soporta flotas de agentes múltiples declarativas definidas en YAML — cada agente con su propio prompt del sistema, voz TTS, saludo y conjunto de herramientas. Una sola función herramienta LLM de handoff intercambia el estado activo en submilisegundos, manteniendo la sesión WebRTC viva. El saludo inicial y el prompt del sistema para el nuevo agente toman efecto en el siguiente turno. Esto significa que un solo número de teléfono puede enrutar a diferentes IAs especializadas: una recepcionista tria la llamada, transfiere a un agente de ventas, que luego transfiere a un agente de soporte — todo sin interrupciones, todo configurable en YAML, sin cambios de código.

The Call Flow Engine

El Motor de Flujo de Llamada

When no Voice AI agent is configured, Gophone falls back to a webhook-driven call flow engine. Your application server receives a POST on each call cycle with state (caller, status, digits) and responds with verbs: play (stream an Ogg/Opus file), gather (collect DTMF digits), or hangup. The Ogg/Opus playback is demuxed via the official LiveKit oggreader at a fixed 20ms cadence with real RFC 6716 frame durations via ParsePacketDuration. DTMF gathering uses a push-model channel with configurable timeout and max digits. A billing Call Detail Record is written automatically on flow exit — duration × $0.0015 per second — ready for billing systems.

Cuando no hay un agente de voz IA configurado, Gophone cae a un motor de flujo de llamada basado en webhooks. Tu servidor de aplicación recibe un POST en cada ciclo de llamada con el estado (caller, status, digits) y responde con verbos: play (transmite un archivo Ogg/Opus), gather (recoge dígitos DTMF), o hangup. La reproducción Ogg/Opus se demuxea vía el oggreader oficial de LiveKit a una cadencia fija de 20ms con duraciones reales de trama RFC 6716 vía ParsePacketDuration. La recolección DTMF usa un canal push-model con timeout configurable y dígitos máximos. Un Call Detail Record de facturación se escribe automáticamente al salir del flujo — duración × $0.0015 por segundo — listo para sistemas de facturación.

Appliance Mode: Zero-IT Deployment

Modo Appliance: Despliegue Zero-IT

For single-tenant deployments — a restaurant receptionist, a doctor’s office, a hotel front desk — Gophone offers appliance mode. Set appliance.enabled: true in gofone.yaml with carrier credentials, and the engine provisions the SIP trunk and dispatch rule directly on the LiveKit SIP Gateway at boot. No CLI commands, no database outbox, no manual trunk creation. Idempotent and safe to restart. The deployment stack is systemd target — build the binary, copy it to /usr/local/bin/gofone, enable gofone.target, and the system manages both the engine and Litestream replication as native services. No Docker, no Kubernetes, no container registry. Litestream monitors the SQLite WAL and replicates incrementally to R2/S3 with sub-second latency for disaster recovery.

Para despliegues mono-inquilino — la recepcionista de un restaurante, el consultorio de un médico, la recepción de un hotel — Gophone ofrece modo appliance. Activa appliance.enabled: true en gofone.yaml con credenciales del operador, y el motor provisiona la troncal SIP y la regla de despacho directamente en la puerta de enlace SIP de LiveKit al arrancar. Sin comandos CLI, sin outbox de base de datos, sin creación manual de troncales. Idempotente y seguro de reiniciar. La pila de despliegue es systemd target — compila el binario, cópialo a /usr/local/bin/gofone, activa gofone.target, y el sistema gestiona tanto el motor como la replicación de Litestream como servicios nativos. Sin Docker, sin Kubernetes, sin registro de contenedores. Litestream monitorea el WAL de SQLite y replica incrementalmente a R2/S3 con latencia de sub-segundos para recuperación ante desastres.

Observability and Recording

Observabilidad y Grabación

Gophone ships with OpenTelemetry tracing via OTLP/gRPC — configurable sample ratio (10% by default), SIP errors carry structured diagnostics (twirp codes, SIP response codes) as span attributes. The telemetry init is non-fatal: the engine runs fine without an OTLP collector. Call recording is opt-in via agent.recording.enabled — LiveKit Room Composite Egress captures MP3/OGG/MP4 files to local disk or S3/MinIO, started when the AI session joins and stopped on hangup. The recording lifecycle is fully managed through LiveKit’s Egress API.

Gophone incluye OpenTelemetry tracing vía OTLP/gRPC — ratio de muestreo configurable (10% por defecto), errores SIP llevan diagnósticos estructurados (códigos twirp, códigos de respuesta SIP) como atributos de span. La inicialización de telemetría no es fatal: el motor funciona sin un colector OTLP. La grabación de llamadas es opt-in vía agent.recording.enabled — LiveKit Room Composite Egress captura archivos MP3/OGG/MP4 a disco local o S3/MinIO, iniciada cuando la sesión IA se une y detenida al colgar. El ciclo de vida de la grabación se gestiona completamente a través de la API Egress de LiveKit.

Architecture and Hexagonal Layers

Arquitectura y Capas Hexagonales

Internally, Gophone follows a strict hexagonal architecture. The domain package defines entities (SIPTrunk, OutboxJob, CDR, VoiceVerb) and ports (DBRepository interface) — it imports nothing external. The infra package implements those ports (SQLite with WAL, OTLP telemetry). The services package orchestrates the reconciler, call manager, agent state, and voice agent loop. The ports package handles inbound webhooks with cryptographic signature verification via github.com/livekit/protocol/webhook. The worker package handles media playback and DTMF. Dependencies point one way inward: domain → services → infra/ports/worker. The layer boundaries are enforced by compile-time checks and documented in a DOX hierarchy of AGENTS.md files throughout the project tree, each specifying preconditions, open questions, and verification commands for its subtree.

Internamente, Gophone sigue una arquitectura hexagonal estricta. El paquete domain define entidades (SIPTrunk, OutboxJob, CDR, VoiceVerb) y puertos (interfaz DBRepository) — no importa nada externo. El paquete infra implementa esos puertos (SQLite con WAL, telemetría OTLP). El paquete services orquesta el reconciler, el gestor de llamadas, el estado del agente y el bucle del agente de voz. El paquete ports maneja webhooks entrantes con verificación de firma criptográfica vía github.com/livekit/protocol/webhook. El paquete worker maneja reproducción de medios y DTMF. Las dependencias apuntan unidireccionalmente hacia adentro: domain → services → infra/ports/worker. Los límites de las capas se aplican mediante verificaciones en tiempo de compilación y se documentan en una jerarquía DOX de archivos AGENTS.md a través del árbol del proyecto, cada uno especificando precondiciones, preguntas abiertas y comandos de verificación para su subárbol.

For developers, the build system is intentionally simple: go build -o gofone ./cmd/engine produces a single self-contained binary. All automated checks pass clean: go vet ./..., go test -race ./..., golangci-lint run ./.... In-process SQLite (WAL mode, write-serialized) means zero external dependencies to run locally — no Postgres, no Redis, no container runtime. The same binary that runs on a developer laptop deploys to production with the systemd + Litestream stack.

Para los desarrolladores, el sistema de compilación es intencionalmente simple: go build -o gofone ./cmd/engine produce un único binario auto-contenido. Todas las verificaciones automatizadas pasan limpiamente: go vet ./..., go test -race ./..., golangci-lint run ./.... SQLite en proceso (modo WAL, serializado en escritura) significa cero dependencias externas para ejecutar localmente — sin Postgres, sin Redis, sin runtime de contenedores. El mismo binario que se ejecuta en un laptop de desarrollo se despliega a producción con la pila systemd + Litestream.


References

Referencias

  • octagono. Gophone — Go-native programmable-voice platform. GitHub
  • LiveKit. Open-source WebRTC platform for real-time audio and video. livekit.io
  • LiveKit. server-sdk-go — Go server SDK. GitHub
  • pion. webrtc — Pure Go WebRTC implementation. GitHub
  • Deepgram. Speech-to-text API with real-time streaming. deepgram.com
  • Cartesia. Real-time voice AI and TTS. cartesia.ai
  • OpenAI. Audio API for speech-to-text and text-to-speech. openai.com
  • Model Context Protocol (MCP). Protocol specification for AI agent tool integration. modelcontextprotocol.io
  • Litestream. SQLite replication to S3-compatible storage. litestream.io
  • OpenTelemetry. Observability framework for cloud-native software. opentelemetry.io
  • Spf13/Cobra. CLI framework for Go. GitHub
  • Spf13/Viper. Go configuration management. GitHub
  • octagono. Gophone — Plataforma de voz programable nativa de Go. GitHub
  • LiveKit. Plataforma WebRTC de código abierto para audio y video en tiempo real. livekit.io
  • LiveKit. server-sdk-go — SDK de servidor Go. GitHub
  • pion. webrtc — Implementación WebRTC en Go puro. GitHub
  • Deepgram. API de voz a texto con streaming en tiempo real. deepgram.com
  • Cartesia. Voz IA y TTS en tiempo real. cartesia.ai
  • OpenAI. API de Audio para voz a texto y texto a voz. openai.com
  • Model Context Protocol (MCP). Especificación del protocolo para integración de herramientas de IA. modelcontextprotocol.io
  • Litestream. Replicación de SQLite a almacenamiento compatible con S3. litestream.io
  • OpenTelemetry. Framework de observabilidad para software cloud-native. opentelemetry.io
  • Spf13/Cobra. Framework CLI para Go. GitHub
  • Spf13/Viper. Gestión de configuración en Go. GitHub
Share