Skip to content

Voice gateway: full-duplex voice for any registered agent#90

Open
lezama wants to merge 2 commits into
mainfrom
feat/voice-gateway
Open

Voice gateway: full-duplex voice for any registered agent#90
lezama wants to merge 2 commits into
mainfrom
feat/voice-gateway

Conversation

@lezama

@lezama lezama commented Jun 12, 2026

Copy link
Copy Markdown
Owner

What

A sidecar daemon (voice-gateway/, Python — same ship-a-daemon-in-the-plugin-repo pattern as wp-carpeta's bridge/) that gives full-duplex voice to any agent registered with the Agents API on a site running openclaWP.

browser mic (PCM 16 kHz over WSS)  ⇄  gateway.py  ⇄  Gemini Live API
                                         │ ask_agent(consulta)
                                         ▼
        POST {site}/wp-json/openclawp/v1/agenttic/{agent}
        (JSON-RPC message/send, app-password auth, sticky sessionId)

Design: one brain

Gemini Live is only the voice shell — native STT/TTS, turn-taking, barge-in. It gets exactly one tool, ask_agent, which forwards every domain request to the agenttic chat endpoint. The registered agent stays the single brain: conversational memory (sticky sessionId), destructive-tool gating (/decisions) and transcripts all stay in WordPress. The voice model is instructed to never answer domain questions from its own knowledge and to verbalize the wait while the agent thinks (tool runs async over the open audio channel).

This deliberately avoids the duplicated-brain architecture (declaring the product's full tool surface to the voice model), which would drift from the agent's prompt and bypass site-side gating.

Pieces

  • gateway.py — FastAPI WS bridge browser⇄Gemini Live (BidiGenerateContent), async tool execution, per-session audio-seconds cost log, /healthz.
  • static/index.html — dependency-free page: mic capture (PCM s16 16 kHz), scheduled 24 kHz playback with barge-in flush, live transcripts, text-input fallback. WS URL is computed relative to the page so a reverse-proxy path prefix works.
  • README.md — setup, Caddy snippet, launchd unit, wire protocol.

Tested

  • Server boots, serves the page, /healthz reports missing config, WS politely rejects unconfigured sessions.
  • Live end-to-end (Gemini key + real agent) pending on the first deployment (wp-carpeta's Menta on menta).

Not in this PR

  • Mic in ChatSurface (this ships a standalone page first).
  • Agent-token auth (uses app passwords like the WhatsApp bridge; agents-api tokens are the natural upgrade).
  • Mapping a spoken "sí" onto pending tool decisions — v1 reads the pending confirmation out loud and relays the user's reply as a normal turn.

🤖 Generated with Claude Code

lezama and others added 2 commits June 12, 2026 18:09
Sidecar daemon (Python, same pattern as wp-carpeta's bridge/) that puts
Gemini Live in front of openclaWP's agenttic chat endpoint as a pure
voice shell: native STT/TTS, turn-taking and barge-in stay in Gemini,
while every domain request is forwarded through a single ask_agent tool
to the registered agent — so memory, destructive-tool gating and
transcripts stay in WordPress.

- gateway.py: WS bridge browser⇄Gemini Live (BidiGenerateContent),
  async tool execution, sticky openclawp sessionId per voice session,
  per-session audio cost log, /healthz.
- static/index.html: mic capture (PCM s16 16kHz), scheduled 24kHz
  playback with barge-in flush, live transcripts, text fallback.
- README: setup, Caddy path-prefix proxy, launchd unit, wire protocol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /openclawp/v1/voice/session exchanges the Google key already stored
in the AI Client credential store (read via AiClient::defaultRegistry()
->getProviderRequestAuthentication) for a single-use Gemini Live
ephemeral token (v1alpha auth_tokens, model-constrained, 30 min expiry).
The long-lived key never leaves WordPress; returning it raw sits behind
an off-by-default filter.

The gateway now asks WP for its credential per voice session
(?access_token= on the v1alpha Bidi WS) and keeps a local
GEMINI_API_KEY only as fallback for older sites.

Shaped deliberately as a core proposal proof of concept: php-ai-client
providers could expose ephemeral client credentials for realtime
sessions natively — see docs/voice-gateway-core-proposal.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@lezama

lezama commented Jun 12, 2026

Copy link
Copy Markdown
Owner Author

Update: credentials now come from WordPress, shaped as a core proposal.

New POST /openclawp/v1/voice/session (includes/class-openclawp-voice-session.php):

  1. Reads the Google key the canonical way — AiClient::defaultRegistry()->getProviderRequestAuthentication('google') (wp-ai-client credential store; the key never enters a REST response).
  2. Mints a single-use ephemeral Live token (v1alpha/auth_tokens, model-constrained, 30 min expiry) and returns {credential, ws_url, model}.
  3. Raw-key fallback exists but is behind an off-by-default filter (openclawp_voice_session_allow_api_key) — the core-shaped contract is that the stored key never leaves the server.

The gateway asks WP for its credential per session (?access_token= on the v1alpha Bidi WS, per the ephemeral-tokens docs) and keeps local GEMINI_API_KEY only as a fallback.

docs/voice-gateway-core-proposal.md is the pitch to take upstream: php-ai-client provider capability for ephemeral client credentials for realtime sessions (createEphemeralCredential('google', $constraints)), with this endpoint as the working proof of concept. Same shape covers OpenAI Realtime's client_secret flow.

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