Skip to content

feat(telemetry): interruption detail, handoff span, fallback events, text input - #2499

Draft
davidzhao wants to merge 1 commit into
dz/telemetry-rpcfrom
dz/telemetry-coverage
Draft

davidzhao wants to merge 1 commit into
dz/telemetry-rpcfrom
dz/telemetry-coverage

Conversation

@davidzhao

@davidzhao davidzhao commented Sep 15, 2026

Copy link
Copy Markdown
Member

Port of livekit/agents#7137. Stacked on #2498.

Description

Smaller coverage gaps on existing spans, each of which came up when reading a trace and not being able to answer a question from it.

Interruptions. agent_turn carried a single lk.interrupted boolean. It now also carries lk.interruption.source, set by the caller that knows the cause: audio_activity (barge-in from VAD / STT activity), user_turn (a committed user turn preempting the reply), programmatic (session.interrupt(), a tool, teardown). The first cause wins. The pipeline and say paths also stamp lk.playout.position, how many seconds had actually played when the user cut in.

A cancelled preemptive attempt names why it was dropped too: SpeechHandle._cancel takes the cause like interrupt. An attempt superseded by more of the user's turn (a later preemptive trigger, or the transcript changing at commit) is user_turn, one dropped by a barge-in is audio_activity; a cancel with no cause (teardown, a pause) still reads as programmatic. A cloud export showed such attempts, cancelled while the user kept talking, labelled programmatic.

Agent handoff. updateAgent() spans an update_agent bar (parent agent_session) with lk.previous_agent_label and lk.agent_label. The old agent's drain_agent_activity (with on_exit inside) and the new agent's start_agent_activity nest under it. The initial start stays under session_start.

Fallback adapters (LLM, TTS, STT). FallbackAdapter.model / .provider follow the instance that serves next, so llm_node, tts_node and start_agent_activity name a real model instead of the adapter. The attempt span carries lk.fallback.label / lk.fallback.index, and a failover mid-request is recorded on the response side: gen_ai.response.model and gen_ai.provider.name of the serving instance on the adapter's request span and on llm_node / tts_node. Usage metrics needed no change.

Text input. A keyterm_detection span around the keyterm-detection LLM pass, nested under the agent_turn that answers the user message (falling back to agent_session), so its llm_request no longer looks like a second inference step. Attributes are counts only (lk.keyterms.count/added/removed) plus model and provider; the terms stay in the session report as PII.

Changes Made

  • voice/speech_handle.ts: InterruptionSource, interrupt(force, source); voice/agent_activity.ts: recordInterruption, source threaded through every interrupt path, lk.playout.position; voice/agent_session.ts: update_agent span.
  • llm|tts|stt/fallback_adapter.ts: nextInstance(), model / provider overrides, served-instance attribution; llm/llm.ts, tts/tts.ts: protected request-span getters.
  • voice/keyterm_detection.ts: keyterm_detection span.
  • telemetry/trace_types.ts: eight new attributes (none PII).

Adaptations from the Python source

  • lk.agent_label / lk.previous_agent_label use agent.id.
  • recordInterruption also stamps lk.interrupted=true: the JS say and realtime paths never set it before (only the pipeline reply did).
  • STT: the now-unused _activeStt last-served tracking is removed with the getters that read it.
  • Python's llm_fallback_adapter span name has no JS counterpart; the adapter's request span keeps the fixed llm_request name and is found through its llm_request_run child.

Testing

  • New voice/coverage_spans.test.ts (barge-in source and playout position through a fake session, update_agent nesting, LLM fallback serving provider, first-cause-wins) and voice/keyterm_detection_span.test.ts; fallback adapter tests updated for next-instance identity; agent_session_handoff.test.ts accepts the threaded trace context.
  • 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: cd08174

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-coverage branch from b21aa74 to f3e8092 Compare September 15, 2026 06:46
@davidzhao
davidzhao force-pushed the dz/telemetry-coverage branch 2 times, most recently from 8b62e64 to a6e6bd6 Compare September 16, 2026 04:34
@davidzhao
davidzhao force-pushed the dz/telemetry-coverage branch from a6e6bd6 to fc1883c Compare September 20, 2026 05:02
@davidzhao
davidzhao force-pushed the dz/telemetry-coverage branch 2 times, most recently from b9fc426 to b4ecdb0 Compare September 20, 2026 06:11
@davidzhao
davidzhao force-pushed the dz/telemetry-coverage branch 2 times, most recently from a25a390 to a7c5d40 Compare September 20, 2026 07:15
…text input

Port of livekit/agents#7137.

Interruptions: `agent_turn` carries `lk.interruption.source`, set by the
caller that knows the cause (`audio_activity` for a VAD/STT barge-in and the
realtime server's own speech detection, `user_turn` for a committed turn or a
final transcript ending a pause, `programmatic` for session.interrupt(),
tools and teardown); the first interruption's cause stands. The pipeline and
`say` paths stamp `lk.playout.position`, the seconds actually played when the
user cut in. The say and realtime paths now also stamp `lk.interrupted`,
which only the pipeline reply did before.

Agent handoff: `updateAgent()` opens an `update_agent` span under
`agent_session` with `lk.previous_agent_label` / `lk.agent_label`; the old
agent's `drain_agent_activity` (with `on_exit`) and the new agent's
`start_agent_activity` / `resume_agent_activity` nest under it. The initial
start stays under `session_start`.

Fallback adapters: LLM, TTS and STT `model` / `provider` follow the instance
that serves next (first available, else the primary), so `llm_node`,
`tts_node` and `start_agent_activity` name a real model. The LLM and TTS
attempt span carries `lk.fallback.label` / `lk.fallback.index` and the
serving instance's request model and provider; the adapter's request span
and the caller's node span get `gen_ai.response.model` / provider of the
instance that answered, per request. The STT adapter's last-served tracking
is replaced by the same next-instance rule.

Text input: the keyterm-detection LLM pass runs in its own
`keyterm_detection` span under the `agent_turn` whose reply added the user
message, else under `agent_session`, with counts only
(`lk.keyterms.count/added/removed`) plus model and provider.

Adaptations: agents carry `id` where Python has `label`; JS `SpeechHandle`
and `AgentActivity.interrupt` take the source as a second positional
parameter / option instead of a keyword. The LLM and TTS stream base classes
expose their request span to subclasses (`llmRequestSpan`,
`ttsRequestSpan`), the JS counterpart of Python's `_llm_request_span`.

A cancelled preemptive attempt names why it was dropped. SpeechHandle._cancel
takes the cause like interrupt does: an attempt superseded by more of the
user's turn (a later preemptive trigger, or the transcript changing at
commit) is user_turn, one dropped by a barge-in audio_activity through
interrupt(); a cancel with no cause (teardown, a pause) still reads as
programmatic. A cloud export showed such attempts, cancelled when the user
kept talking, labelled programmatic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@davidzhao
davidzhao force-pushed the dz/telemetry-coverage branch from a7c5d40 to cd08174 Compare September 20, 2026 07:21
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