fix(voice): report e2e_latency for the first reply after a handoff - #2456
fix(voice): report e2e_latency for the first reply after a handoff#2456rosetta-livekit-bot[bot] wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: 58a3ccb The changes in this PR will be included in the next version bump. This PR includes changesets to release 38 packages
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 |
There was a problem hiding this comment.
Devin Review found 3 potential issues.
2 flags not posted on this PR by your GitHub settings — view them in Devin Review. (Configure)
| speechHandle: handle, | ||
| }), | ||
| ); | ||
| const previousUserMetrics = this.takeOnEnterUserMetrics(); |
There was a problem hiding this comment.
🟡 Realtime handoff latency is lost
With a realtime model, takeOnEnterUserMetrics consumes the pending turn before selecting the reply path. The realtime path never receives it, so the first handoff reply omits end-to-end latency.
Prompt for agents
Preserve the metrics returned by takeOnEnterUserMetrics for RealtimeModel replies. Pass them through realtimeReplyTask and realtimeGenerationTask, then attach e2eLatency and ATTR_E2E_LATENCY when the first realtime output starts. Ensure a handoff onEnter reply consumes the pending turn only once, matching the pipeline and say paths.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (!chainContinues) { | ||
| this.agentSession._unansweredUserMetrics = undefined; |
There was a problem hiding this comment.
🟡 Older tools erase newer latency
When a newer turn starts while an older tool runs, the older chain can later clear _unansweredUserMetrics unconditionally. The newer reply then loses its end-to-end latency.
Prompt for agents
Make chain termination clear only the metrics owned by that reply chain. Retain the original userMetrics identity throughout _pipelineReplyTaskImpl and compare the session field before clearing it, including the toolOutput.output.length === 0 branch. Add an overlap test where a second user turn commits while an older tool is still running.
Was this helpful? React with 👍 or 👎 to provide feedback.
| const metrics = this.agentSession._unansweredUserMetrics; | ||
| this.agentSession._unansweredUserMetrics = undefined; |
There was a problem hiding this comment.
🟡 Failed greetings consume pending latency
takeOnEnterUserMetrics removes the pending turn before the selected speech produces output. If that speech fails or gets interrupted, a later greeting cannot report latency for the unanswered turn.
Prompt for agents
Model pending metrics as claimed rather than permanently removed at speech creation. Restore the claim when the owning say or generateReply task ends without starting output, while avoiding restoration if a newer user turn has superseded it. Add tests for interrupted and failed first onEnter speeches followed by a successful greeting.
Was this helpful? React with 👍 or 👎 to provide feedback.
Ports livekit/agents#7167.
Keeps the latest unanswered user-turn metrics on the session so the first speech created by a handoff or inline
AgentTaskcan reporte2eLatency. The turn is settled when a speech reports it or when its reply chain ends without an answer.Source diff coverage
livekit-agents/livekit/agents/voice/agent_activity.py: adapted toagents/src/voice/agent_activity.ts. Preserves pending-turn ownership,onEnterclaim/decline behavior,sayandgenerateReplyhandling, tool-reply propagation, and reply-chain termination using TypeScript naming, milliseconds-to-seconds conversion, andAsyncLocalStorage.livekit-agents/livekit/agents/voice/agent_session.py: adapted toagents/src/voice/agent_session.tsas a session-scoped internalMetricsReportfor the latest unanswered user turn.tests/fake_vad.py: not applicable. agents-js has no shared fake-VAD counterpart; its voice tests inject timestamped user turns directly rather than reopening scripted VAD streams, so the Python cross-stream fake-clock fix has no target infrastructure to modify.tests/test_e2e_latency_handoff.py: adapted in full toagents/src/voice/agent_activity_e2e_latency_handoff.test.ts, preserving all 15 source scenarios with Vitest, the targetFakeLLM, and timestampedChatMessageturns.Infrastructure gap
The source framework exports
e2e_latencythrough an OpenTelemetry metrics SDK andlk.agents.turn.e2e_latencyhistogram. agents-js currently has no meter provider or metrics exporter, so this PR cannot export that histogram without introducing the target's missing metrics subsystem. Messagee2eLatencyand the existinglk.e2e_latencyspan attribute are ported.Testing
pnpm exec vitest run agents(152 files, 2,505 passed, 5 skipped)pnpm --filter @livekit/agents buildpnpm buildpnpm --filter @livekit/agents lint(passes with existing warnings)pnpm exec prettier --check "agents/src/**/*.{ts,tsx,md,json}"pnpm --filter @livekit/agents typecheckcue-cliruntime handoff drive attempted, but the configured LiveKit endpoint rejected the supplied API key with401 Unauthorized - invalid API key, so runtime framework-event validation could not complete.Ported from livekit/agents#7167
Original PR description
Problem:
e2e_latencyis computed from the user turn's metrics held in a local of the reply task, so the first reply after an agent handoff never gets it, and a tool reply after an inlineAgentTaskreports the whole sub-conversation as one latency. The slowest turns of a call are the ones missing from the message, thelk.e2e_latencyspan attribute, and the OTel histogram.Fix: The session keeps the latest user turn no speech has answered, set where the reply task commits the user message. The tool reply reads it, a
say()orgenerate_reply()created insideon_enterclaims it, and it ends when a speech reports it, when the reply chain ends without a tool reply or a handoff, or when anon_enterreturns without speaking.Closes #7157. Supersedes #7161.
Context for reviewing and coding agents