Skip to content

feat(voice): port AvatarSession base class and transcript sync asymmetric detach warning#1280

Open
toubatbrian wants to merge 1 commit intomainfrom
claude/jolly-lovelace-wm4Lr
Open

feat(voice): port AvatarSession base class and transcript sync asymmetric detach warning#1280
toubatbrian wants to merge 1 commit intomainfrom
claude/jolly-lovelace-wm4Lr

Conversation

@toubatbrian
Copy link
Copy Markdown
Contributor

Summary

Automated port of livekit/agents#5499 (feat(avatar): add AvatarSession base class, warn on sync mis-wire) into agents-js.

cc @toubatbrian @livekit/agent-devs — please review.

This PR was created by an automated Claude Code Routine (currently in experimentation stage). It mirrors portable core + plugin changes from the Python repo; Python-only plugin diffs (e.g. avatario, avatartalk, bithuman, did, keyframe, liveavatar, simli, tavus) that have no JS counterpart were skipped.

Ported Features

1. voice.AvatarSession base class (new)

New file: agents/src/voice/avatar/avatar_session.ts — re-exported from voice/avatar/index.ts so it lands on voice.AvatarSession.

Behavior mirrors the Python base at livekit-agents/livekit/agents/voice/avatar/_types.py:

  • start(agentSession, room):
    • Calls getJobContext(false) and, if present, registers () => this.aclose() via jobCtx.addShutdownCallback(...). If no job context is active, logs a debug message telling the caller to aclose() manually — same message as Python.
    • Reads agentSession.output.audio and, when the session has already been started, logs a warning that the existing audio output will be replaced. In JS there is no .label on AudioOutput, so the warning attaches audioOutput.constructor.name instead of label.
  • aclose(): default no-op, overridable by subclasses.

2. AgentSession._started internal getter

AgentSession exposes an internal _started getter that reads the private started field. This is the JS analog of Python's agent_session._started attribute access inside the base AvatarSession.start. Marked @internal with a // Ref: comment pointing at the Python source.

3. TranscriptionSynchronizer asymmetric-detach warning

In agents/src/voice/transcription/synchronizer.ts:

  • Added _audioAttached, _textAttached, and _warnedAsymmetricDetach internal flags to TranscriptionSynchronizer (defaults true, true, false — matches Python).
  • Added _onAttachmentChanged({ audioAttached?, textAttached? }) which updates the attached flags and calls this.enabled = this._audioAttached && this._textAttached.
  • set enabled() now resets _warnedAsymmetricDetach = false on the disabled → enabled transition (matches Python set_enabled).
  • SyncedAudioOutput and SyncedTextOutput now override onAttached() / onDetached() to forward the event into _onAttachmentChanged. This is the JS equivalent of Python's on_attached / on_detached overrides in _SyncedAudioOutput / _SyncedTextOutput.
  • In SyncedAudioOutput.captureFrame (when the synchronizer is disabled) and SyncedTextOutput.captureText (same), we now emit a one-shot warning when the synchronizer finds itself with an asymmetric (audio attached / text detached, or vice versa) state. Warning strings match Python verbatim.

4. Avatar plugin updates

Updated plugins to extend voice.AvatarSession and call super.start(agentSession, room) at the top of start():

  • plugins/anam/src/avatar.ts
  • plugins/bey/src/avatar.ts
  • plugins/lemonslice/src/avatar.ts
  • plugins/trugen/src/avatar.ts

Each call site carries a // Ref: python ... comment pointing at the line in the corresponding Python plugin.

plugins/hedra is already deprecated (the ctor throws), so it was intentionally left unchanged. plugins/runway is TTS-only in agents-js and has no avatar, so it was skipped.

Implementation Notes (language-level differences)

  • Return type of AvatarSession.start: Python uses None, and some plugins (e.g. lemonslice) return a session id. Python's type system lets them override with # type: ignore[override]. TypeScript rejects widening Promise<void> to Promise<string> in an override, so the base class's return type is Promise<unknown> instead of Promise<void>. Subclasses are free to return void or any other value; the base implementation explicitly return undefined.
  • No label on AudioOutput: The Python AudioOutput base has a label property that's surfaced in the "audio output will be replaced" warning. AudioOutput in agents-js does not expose label, so the JS warning uses audioOutput.constructor.name instead. Same signal, slightly different shape.
  • Attachment wiring: Python wires on_attached / on_detached on _SyncedAudioOutput / _SyncedTextOutput via the base io.AudioOutput / io.TextOutput hooks, which are already called on session.output.audio = ... / session.output.transcription = ... assignment in AgentOutput. The JS AudioOutput / TextOutput base classes already have onAttached() / onDetached() with the same call-site wiring in AgentOutput (see agents/src/voice/io.ts), so the JS port simply overrides these methods in the synced subclasses — no new plumbing was required.
  • Bithuman local-mode aclose: The Python PR also migrates bithuman's local-mode runtime cleanup from an inline job_ctx.add_shutdown_callback into AvatarSession.aclose(). agents-js has no bithuman plugin, so this piece is not ported.

Tests

  • pnpm build passes (all packages).
  • pnpm test agents/src/voice/transcription passes (19/19 existing synchronizer tests unaffected).
  • pnpm test plugins/lemonslice passes (2/2).
  • pnpm lint passes (only pre-existing warnings in unrelated files).

Test plan

  • Verify AvatarSession.start() registers aclose on the job shutdown callback (anam, bey, lemonslice, trugen).
  • Verify the "started after AgentSession.start()" warning fires when a developer wires the avatar after session.start({ agent, room }).
  • Verify the asymmetric-detach warning fires exactly once per enabled cycle when session.output.audio = null or session.output.transcription = null while the other side is still attached.
  • Smoke test at least one avatar plugin end-to-end against LiveKit.

https://claude.ai/code/session_01HwutyuE78oUGoYwUV69pF8

…tric detach warning

Ports livekit/agents#5499 from the Python agents repo.

- Add `voice.AvatarSession` base class that registers `aclose` as a job
  shutdown callback and warns when started after `AgentSession.start()`
  has already wired an audio output.
- Port the `TranscriptSynchronizer` one-shot warning for asymmetric
  audio/text detach: track `_audioAttached` / `_textAttached` via
  `onAttached` / `onDetached`, reset the warn flag on re-enable, and
  log when only one of audio/text is detached.
- Wire avatar plugins (anam, bey, lemonslice, trugen) to inherit from
  the new base class and call `super.start(agentSession, room)` first.

https://claude.ai/code/session_01HwutyuE78oUGoYwUV69pF8
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 20, 2026

🦋 Changeset detected

Latest commit: cc45c7c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 26 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-trugen Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hedra Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-resemble Patch
@livekit/agents-plugin-rime Patch
@livekit/agents-plugin-runway Patch
@livekit/agents-plugin-sarvam Patch
@livekit/agents-plugin-silero Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-xai Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

const _AVATAR_NAME = 'anam-avatar-agent';

export class AvatarSession {
export class AvatarSession extends voice.AvatarSession {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing // Ref comment on extends voice.AvatarSession class declaration (anam)

Per the CLAUDE.md porting rule, every JS change that corresponds to a Python change must carry an inline // Ref: python <path> - <line-range> lines comment directly above the relevant line(s). The class declaration change to extends voice.AvatarSession is a direct port from the Python plugin (where the class similarly extends the base AvatarSession), but it lacks the required reference comment. The super() call at plugins/anam/src/avatar.ts:53 is also a ported change without a // Ref.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread plugins/bey/src/avatar.ts
* ```
*/
export class AvatarSession {
export class AvatarSession extends voice.AvatarSession {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing // Ref comment on extends voice.AvatarSession class declaration (bey)

Same issue as in the anam plugin. The class declaration change to extends voice.AvatarSession at plugins/bey/src/avatar.ts:99 is a direct port from the Python plugin but lacks the required // Ref comment per CLAUDE.md. The super() call at plugins/bey/src/avatar.ts:116 also lacks a // Ref.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

* ```
*/
export class AvatarSession {
export class AvatarSession extends voice.AvatarSession {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing // Ref comment on extends voice.AvatarSession class declaration (lemonslice)

Same issue as in the anam plugin. The class declaration change to extends voice.AvatarSession at plugins/lemonslice/src/avatar.ts:125 is a direct port from the Python plugin but lacks the required // Ref comment per CLAUDE.md. The super() call at plugins/lemonslice/src/avatar.ts:146 also lacks a // Ref.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

* @public
*/
export class AvatarSession {
export class AvatarSession extends voice.AvatarSession {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Missing // Ref comment on extends voice.AvatarSession class declaration (trugen)

Same issue as in the anam plugin. The class declaration change to extends voice.AvatarSession at plugins/trugen/src/avatar.ts:67 is a direct port from the Python plugin but lacks the required // Ref comment per CLAUDE.md. The super() call at plugins/trugen/src/avatar.ts:84 also lacks a // Ref.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

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.

3 participants