Skip to content

feat(node-voice-agent): use @deepgram/sdk for the Deepgram Agent connection - #22

Draft
GregHolmes wants to merge 2 commits into
mainfrom
feat/use-deepgram-sdk
Draft

feat(node-voice-agent): use @deepgram/sdk for the Deepgram Agent connection#22
GregHolmes wants to merge 2 commits into
mainfrom
feat/use-deepgram-sdk

Conversation

@GregHolmes

Copy link
Copy Markdown

What

Migrates the backend from a raw WebSocket proxy to Deepgram's Voice Agent API over to the official @deepgram/sdk (^5.7.0) using client.agent.v1 streaming support. Part of the SDK-migration rollout across the Node starters.

How

  • Replaced the hand-rolled new WebSocket('wss://agent.deepgram.com/...') connection with deepgram.agent.v1.createConnection(), following the open sequence createConnection → attach open/message/error/close handlers → connect()await waitForOpen().
  • The SDK now owns the Deepgram-side socket: auth (server-side API key only), reconnection, and binary-audio framing.
  • Browser protocol is unchanged (frontend needs no edits):
    • browser → Deepgram: each control JSON message is routed to its matching typed SDK method (sendSettings, sendUpdateListen/Think/Speak, sendUpdatePrompt, sendInjectUserMessage, sendInjectAgentMessage, sendFunctionCallResponse, sendKeepAlive); binary mic audio → sendMedia.
    • Deepgram → browser: binary agent audio forwarded as binary, JSON events forwarded as JSON (Blob/ArrayBuffer/Buffer/string/object handling).
    • Messages that arrive before the Deepgram socket is open are buffered and flushed on open.
  • DEEPGRAM_BASE_URL is honored via the SDK environment (base/production/agent).
  • Left the ws browser server, JWT/session auth, /api/session, /api/metadata, and graceful shutdown unchanged.
  • Added @deepgram/sdk@^5.7.0 to package.json (+ lockfile), set sdk = "@deepgram/sdk" in deepgram.toml.

Verified

  • node --check server.js — passes.
  • Boot test: DEEPGRAM_API_KEY=dummy PORT=<free> node server.js starts cleanly, banner prints; GET /api/metadata returns the expected JSON (sdk: "@deepgram/sdk"), GET /api/session issues a JWT; graceful shutdown works.
  • corepack pnpm install updates the lockfile with @deepgram/sdk 5.7.0.

Manual verification needed before merge

  • Live run with a real DEEPGRAM_API_KEY: full-duplex voice conversation via WS /api/voice-agent (Settings applied, agent audio plays back, ConversationText events received).
  • Exercise live updates (UpdateSpeak / UpdatePrompt / InjectUserMessage) mid-conversation.
  • Confirm the paired voice-agent-html frontend works without changes.

…ection

Replace the raw WebSocket proxy to the Voice Agent API with the official
@deepgram/sdk client.agent.v1 streaming support. The SDK now manages the
Deepgram-side WebSocket, auth, reconnection, and binary-audio framing.

The browser-facing protocol is unchanged: the frontend still sends a Settings
message plus live-update / inject control JSON and binary mic audio, and
receives Deepgram's binary agent audio + JSON events exactly as before. Each
browser control message is routed to the matching typed SDK send method, and
mic audio is forwarded via sendMedia; early messages are buffered until the
Deepgram socket is open. DEEPGRAM_BASE_URL is supported via the SDK
environment (base/production/agent).
@GregHolmes GregHolmes self-assigned this Jul 30, 2026
… clipping

Binary agent audio arrives from the Deepgram SDK socket as a Blob and
must be async-converted before forwarding, while JSON events
(AgentAudioDone) forward synchronously. Firing each forward independently
let the AgentAudioDone event overtake the still-converting final audio
chunk, clipping the agent-audio tail. Serialize all forwards through a
per-connection promise chain so frames reach the browser in the order
Deepgram sent them.
@GregHolmes

Copy link
Copy Markdown
Author

Corey feedback addressed — agent audio tail clipping fixed

Root cause: binary agent-audio frames arrive from the @deepgram/sdk socket as a Blob, which requires an async data.arrayBuffer() conversion before forwarding, while JSON events (AgentAudioDone) forward synchronously. Firing each forward independently let the synchronous AgentAudioDone overtake the still-converting final audio chunk, clipping the tail.

Fix (server.js, dgConn.on('message', ...)): serialize every forward through a per-connection promise chain so frames reach the browser in the exact order Deepgram sent them.

let sendChain = Promise.resolve();
dgConn.on('message', (data) => {
  sendChain = sendChain
    .then(() => forwardToBrowser(clientWs, data))
    .catch((err) => console.error('Failed to forward Deepgram message:', err));
});

Ordering evidence — the real forwardToBrowser was driven with the exact SDK emission that triggers the bug (six audio Blobs followed synchronously by an AgentAudioDone event), comparing the old independent-forward wiring against the new chained wiring:

PRE-FIX  (independent forwards): CONTROL AgentAudioDone, AUDIO, AUDIO, AUDIO, AUDIO, AUDIO, AUDIO
   -> AgentAudioDone at index 0, last audio at index 6  => FAIL (tail clipped)
POST-FIX (promise-chained):      AUDIO x6, then CONTROL AgentAudioDone
   -> last audio at index 5, AgentAudioDone at index 6  => PASS (no tail clip)

The last binary frame is now received before the terminal AgentAudioDone. node --check server.js passes; backend boots on 8431 and serves /api/session, /api/metadata, and the authenticated agent WS.

⚠️ Live re-verify BLOCKED on a valid key: the shared DEEPGRAM_API_KEY is revoked org-wide (confirmed 401 INVALID_AUTH on REST /v1/projects, REST /v1/speak, and the SDK WS upstream), so a full live turn that returns real agent audio could not be run end-to-end. The serialized promise chain guarantees forward order regardless of Blob async-convert timing, and the evidence above exercises that path with the repo's real forwardToBrowser + real Node Blob. Recommend a live long-turn re-verify once a valid key is available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant