Skip to content

voice: TypeScript/Python parity matrix and gaps (adapters, telemetry, testing surface) #986

Description

@drewdrewthis

Why

The LangWatch platform consumes only the TypeScript package, but the SDK is a product commitment in both languages, and the voice surface has drifted in a few places. This issue records the voice parity matrix as of main @ edf70006 (2026-09-07) plus #982, so gaps are tracked explicitly instead of rediscovered per PR.

Origin: langwatch/langwatch#7978 (voice V2), owner's track 7: "check to see if it's covering both TS and python (the scenario sdk)".

Versions

TypeScript Python
main manifest javascript/package.json 1.6.0 python/pyproject.toml 1.5.0
Latest release tag javascript/v1.6.0 (2026-09-06) python/v1.5.0
Vendored by the LangWatch platform 1.3.0 (platform/app/vendor/langwatch-scenario-1.3.0.tgz) not consumed

Adapter matrix

Adapter TypeScript Python Parity
Composable (STT→LLM→TTS) javascript/src/voice/adapters/composable.ts python/scenario/voice/adapters/composable.py same
ElevenLabs Convai adapters/elevenlabs.ts adapters/elevenlabs.py same
Gemini Live adapters/gemini-live.ts adapters/gemini_live.py differs: TS declares interruption: true, Python matrix lists it deferred (docs/docs/pages/voice/capability-matrix.mdx:52-55)
OpenAI Realtime adapters/openai-realtime.ts adapters/openai_realtime.py same
Pipecat adapters/pipecat.ts adapters/pipecat.py same (TS webrtc transport pending)
Twilio b-leg twilio.ts, twilio-server.ts, twilio-shared.ts, twilio-tunnel.ts twilio.py, _twilio_server.py, _twilio_shared.py same
Twilio a-leg external dialing (#982, unmerged) same files same files same, built in lockstep from one spec (specs/voice-twilio-a-leg-external.feature)
LiveKit absent adapters/livekit.py (stub, raises PendingTransportError) Python only, #563
Vapi absent adapters/vapi.py (stub) Python only, #563
WebRTC (generic) absent adapters/webrtc.py (stub) Python only, #563
WebSocket (generic, BYO protocol) absent adapters/websocket.py (real I/O) Python only, functional gap
Twilio testing harness test-only __tests__/a-leg-harness.ts public voice/testing/{twilio_harness,tunnel,wrapper_harness}.py Python only as public surface

Telemetry

javascript/src/voice/telemetry.ts declares itself a mirror of python/scenario/voice/_telemetry.py. Span names voice.adapter.connect|disconnect|dial, voice.turn, voice.audio.send, voice.audio.receive and every attribute (voice.adapter.class, voice.turn.index, voice.turn.latency_ms, voice.turn.user_audio_bytes, voice.turn.agent_audio_bytes, voice.audio.bytes, voice.audio.first_chunk_latency_ms, voice.audio.terminated_reason, voice.audio.chunk_count, vendor-specific voice.elevenlabs.*, voice.gemini.*, voice.realtime.*, voice.pipecat.*, voice.twilio.*) match one to one.

Documented, intentional differences:

  • voice.stt.transcribe: Python per turn under voice.turn (adapter.py:409); TS once per run under a voice.stt.backfill batch span (execution/scenario-execution.ts:852,882). Disambiguated by voice.stt.scope = turn | run.
  • voice.stt.backfill exists only in TS.

Needs a single-file check, not confirmed as bugs:

  • ElevenLabs voice.elevenlabs.brokered / voice.elevenlabs.session_id are set in Python (elevenlabs.py:446-448); the TS connect span only showed voice.elevenlabs.agent_id (elevenlabs.ts:517).

Recording, judge STT, effects, limits

Same on both sides: PCM16/24 kHz internal format, mulaw/8000 on the Twilio wire, save formats wav/mp3/ogg/flac via ffmpeg, tail-silence 0.6 s with the shared tail_silence reason, judge/backfill STT default gpt-4o-transcribe, effects namespace and assets, Twilio a-leg maxCallDurationSeconds / allowedCallees deny-by-default with matching error text.

Python only: modality negotiation (ModalityTier, resolve_modality, ModalityNegotiationError), process-global set_stt_provider / get_stt_provider (TS removed the global on purpose, ADR-001/002 in config.ts:1-18; Python is moving the same way in #973).

TS only: lowercase factory functions (elevenLabsAgent, twilioAgent, ...), additive sugar.

Test files per adapter

Adapter TS Python
ElevenLabs 5 10
Gemini Live 2 3
OpenAI Realtime 7 3
Pipecat 2 5
Twilio incl. a-leg 13 16
LiveKit / Vapi / WebRTC / WebSocket n/a generic stub tests only

Gaps to close (checklist)

  • G1 TS port of the generic WebSocketAgentAdapter (the only shipping capability Python has that TS lacks). Today the TS workaround is subclassing VoiceAgentAdapter.
  • G2 Decide the fate of the Python LiveKit, Vapi, WebRTC stubs (feat(typescript-sdk/voice): remaining TS platform adapter transports — LiveKit, Vapi, generic WebRTC #563): implement in both languages, or remove from the Python public barrel so the matrix is honest.
  • G3 Reconcile the Gemini Live interruption capability flag between the TS adapter and the Python capability matrix doc.
  • G4 Confirm on a live TS trace whether voice.elevenlabs.brokered / session_id are emitted; add them if the TS adapter brokers a session.
  • G5 Publish the Twilio testing harness in TS (or document it as Python-only).
  • G6 Keep the parity matrix doc (docs/docs/pages/voice/capability-matrix.mdx) as the single source and add a CI check that both barrels export the same adapter set, allowing an explicit per-language exemption list.

Acceptance criteria

  • AC1 Given the two voice barrels (javascript/src/voice/index.ts, python/scenario/voice/__init__.py), when the parity check runs in CI, then every adapter class exported by one language is exported by the other or listed in an exemption file with an issue link. Evidence: CI job green on main, red on a branch that adds a one-sided export.
  • AC2 Given a TS scenario using WebSocketAgentAdapter against the same echo server the Python tests use, when the run finishes, then the transcript and voice.turn spans match the Python run's shape. Evidence: mirrored feature scenario bound in both languages.
  • AC3 Given the capability matrix doc, when G2 and G3 land, then no row says "deferred" for a capability that ships, and no stub is exported without a "pending" marker. Evidence: doc diff plus stub tests.

Not in scope

The LangWatch platform's own version drift (vendored 1.3.0 vs 1.6.0) is tracked in langwatch/langwatch#7978 and closed by a platform PR, not here.

🤖 Generated with Claude Code
https://claude.ai/code/session_01KVwRyDjKCmNUesqaKAc4At

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestjavascriptPull requests that update javascript code

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions