Skip to content

feat(telemetry): trace the end-of-turn wait and the turn handoff - #2496

Open
davidzhao wants to merge 1 commit into
mainfrom
dz/telemetry-eot-wait
Open

davidzhao wants to merge 1 commit into
mainfrom
dz/telemetry-eot-wait

Conversation

@davidzhao

@davidzhao davidzhao commented Sep 15, 2026

Copy link
Copy Markdown
Member

Port of livekit/agents#7130.

Description

eou_detection only covered the turn-detector inference, so the endpointing delay itself (often seconds) showed up as an empty gap between user_turn and agent_turn. This makes the wait, the user hook, and the speech queue visible.

eou_wait span, one per user turn, child of user_turn:

  • back-dated to the last speaking time, so the bar starts where the user stopped talking;
  • ends on the turn decision with lk.eou.outcome (committed / user_resumed / dropped), lk.eou.wait_duration, lk.eou.rearm_count, the final lk.eou.endpointing_delay, and the last lk.eou.source;
  • a later trigger for the same turn (late STT final, another VAD end of speech) re-arms the wait with a rearmed event instead of starting a new span;
  • a rejected commit counts in lk.eou.not_committed_count and keeps waiting; resumed speech ends the span at the speech start (user_resumed event) and lk.eou.resume_count on user_turn counts those; teardown drops it. A running eou_detection is closed with a superseded event so it stays inside the wait. A turn-detection mode change to or from manual drops the pending wait.

eou_detection keeps its name and now nests under eou_wait.

on_user_turn_completed span around the user hook that gates the reply, under the user_turn it completes: recognition hands the open user_turn span to the activity, which ends it after the hook. Exceptions are recorded redaction-aware (session and job flags); StopResponse is an event.

Per-turn breakdown on the reply's agent_turn: lk.speech.queue_wait (scheduling to first generation authorization), plus lk.end_of_turn_delay, lk.transcription_delay, lk.on_user_turn_completed_delay next to lk.e2e_latency.

user_turn
├─ user_speaking
└─ eou_wait                 ← new, rearmed ×1
   └─ eou_detection         ← now nested under the wait
on_user_turn_completed      ← new
agent_turn  (lk.speech.queue_wait=0.01)

Changes Made

  • voice/audio_recognition.ts: eou_wait state machine (ensureEouWaitSpan / endEouWaitSpan / releaseEouDetectionSpan / stampUserTurnResumes), EndOfTurnInfo.userTurnSpan / userTurnSpanAdopted, lazily-created user_turn starts at the earliest known anchor, updateOptions now cancels the pending decision on a manual mode switch.
  • voice/agent_activity.ts: on_user_turn_completed span, adopted user_turn ended after the hook, recordQueueWait, recordUserTurnStages.
  • voice/speech_handle.ts: scheduled / authorized timestamps and the queue-wait accessor.
  • telemetry/trace_types.ts: seven new attributes (none PII); unit docs on the existing delay attributes.
  • voice/amd.ts: AMD duration attributes converted to seconds.

Adaptations from the Python source

  • lk.agent_label uses agent.id (JS agents have no label), like every other JS site.
  • A non-StopResponse hook error keeps the existing JS behavior (log, record on the span, continue to the reply); Python drops the turn. Left unchanged to keep this PR to telemetry.
  • Every duration attribute on a span is now in seconds, as in Python. lk.eou.endpointing_delay (eou_detection, eou_wait), lk.eou.detection_delay, lk.transcription_delay / lk.end_of_turn_delay (user_turn), and lk.amd.speech_duration / lk.amd.delay were stamped in milliseconds before, which put them at odds with the new attributes and with the Python SDK on shared dashboards. Called out in the changeset.

Testing

  • New voice/eou_wait_span.test.ts (10 tests): commit, re-arm, resumed speech, teardown drop, not-committed, nested and superseded eou_detection, manual mode switch, and a full fake session asserting the wait, the hook span, the queue wait and session-only redaction of a hook exception.
  • audio_recognition_span.test.ts parenting assertions updated (eou_detectioneou_waituser_turn).
  • Full agents suite green; build, typecheck, lint, API report updated.

🤖 Generated with Claude Code

@changeset-bot

changeset-bot Bot commented Sep 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a89d4fa

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

This PR includes changesets to release 39 packages
Name Type
@livekit/agents Patch
@livekit/agents-plugin-anam Patch
@livekit/agents-plugin-anthropic Patch
@livekit/agents-plugin-assemblyai Patch
@livekit/agents-plugin-azure Patch
@livekit/agents-plugin-baseten Patch
@livekit/agents-plugin-bey Patch
@livekit/agents-plugin-cartesia Patch
@livekit/agents-plugin-cerebras Patch
@livekit/agents-plugin-deepgram Patch
@livekit/agents-plugin-did Patch
@livekit/agents-plugin-elevenlabs Patch
@livekit/agents-plugin-fishaudio Patch
@livekit/agents-plugin-google Patch
@livekit/agents-plugin-hume Patch
@livekit/agents-plugin-inworld Patch
@livekit/agents-plugin-krisp Patch
@livekit/agents-plugin-lemonslice Patch
@livekit/agents-plugin-liveavatar Patch
@livekit/agents-plugin-livekit Patch
@livekit/agents-plugin-meta Patch
@livekit/agents-plugin-minimax Patch
@livekit/agents-plugin-mistral Patch
@livekit/agents-plugin-mistralai Patch
@livekit/agents-plugin-neuphonic Patch
@livekit/agents-plugin-openai Patch
@livekit/agents-plugin-perplexity Patch
@livekit/agents-plugin-phonic Patch
@livekit/agents-plugin-protoface 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-plugin-soniox Patch
@livekit/agents-plugin-tavus Patch
@livekit/agents-plugins-test Patch
@livekit/agents-plugin-trugen 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

@davidzhao
davidzhao added this pull request to stack #2502 September 15, 2026 06:32
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch 4 times, most recently from e1a2bf3 to 0a818b6 Compare September 20, 2026 06:11
@davidzhao
davidzhao marked this pull request as ready for review September 20, 2026 07:17
@davidzhao
davidzhao requested a review from a team as a code owner September 20, 2026 07:17
devin-ai-integration[bot]

This comment was marked as resolved.

@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch from 0a818b6 to a89d4fa Compare September 20, 2026 17:17
@davidzhao davidzhao added effort:high review_effort:medium Focused changes that need domain context or careful validation and removed effort:high labels Sep 21, 2026
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch from a89d4fa to c3ef86e Compare September 21, 2026 15:35
Port of livekit/agents#7130.

- `eou_wait` span, one per user turn under `user_turn`, back-dated to the last
  speaking time and ended on the turn decision with lk.eou.outcome
  (committed / user_resumed / dropped), lk.eou.wait_duration, lk.eou.rearm_count,
  lk.eou.not_committed_count and the final lk.eou.endpointing_delay / source.
  Later triggers for the same turn re-arm the wait (a `rearmed` event) instead of
  opening a new span; resumed speech ends it at the VAD/STT speech start; a mode
  change to or from manual and teardown drop it. `eou_detection` now nests under
  the wait, which owns its early end (`superseded` event) so the child always
  stays inside the parent. user_turn carries lk.eou.resume_count.
- `on_user_turn_completed` span around the user hook, nested under the user_turn
  it completes: recognition hands the open span to the activity (EndOfTurnInfo
  userTurnSpan / userTurnSpanAdopted) which ends it after the hook, so the turn
  covers the wait for the hook. StopResponse is an event; exceptions are
  recorded redaction-aware, honouring the session's redaction as well as the
  job's flag.
- lk.speech.queue_wait on agent_turn (pipeline, say and realtime generation);
  lk.end_of_turn_delay, lk.transcription_delay and
  lk.on_user_turn_completed_delay next to lk.e2e_latency on the reply's turn.

Units: every duration attribute on a span is in seconds, as in Python.
lk.eou.endpointing_delay (eou_detection, eou_wait), lk.eou.detection_delay,
lk.transcription_delay and lk.end_of_turn_delay (user_turn), and
lk.amd.speech_duration / lk.amd.delay were stamped in milliseconds before,
which put them at odds with the new attributes and with the Python SDK on
shared dashboards.

Adaptations: Agent.id stands in for Python's Agent.label on the hook span;
the user hook's non-StopResponse errors keep the existing JS behavior of
logging and continuing with the reply (Python drops the turn); the realtime
reply task records no queue wait, like Python.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@davidzhao
davidzhao force-pushed the dz/telemetry-eot-wait branch from c3ef86e to a65416a Compare September 21, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review_effort:medium Focused changes that need domain context or careful validation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant