Skip to content

feat(ultravox): implement say() via ForcedAgentMessage - #7001

Open
ssgutierrez42 wants to merge 15 commits into
livekit:mainfrom
11x-ai:deploy/ultravox-forced-agent-message
Open

feat(ultravox): implement say() via ForcedAgentMessage#7001
ssgutierrez42 wants to merge 15 commits into
livekit:mainfrom
11x-ai:deploy/ultravox-forced-agent-message

Conversation

@ssgutierrez42

@ssgutierrez42 ssgutierrez42 commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add say() method to RealtimeSession that speaks verbatim text via Ultravox's ForcedAgentMessage protocol
  • Add ForcedAgentMessageEvent type to the events module and wire it into the UltravoxEventType union
  • Advertise supports_say=True in Ultravox model capabilities so the framework knows say() is available
  • Handle cancellation by sending an immediate barge-in (UserTextMessageEvent with empty text), a pattern seen throughout the plugin.

Why

LiveKit's session.say() lets agents speak exact text without LLM processing. This is useful for scripted content.

The upstream Ultravox plugin didn't implement it because the Ultravox WebSocket API uses its own ForcedAgentMessage data message type rather than other patterns in the project (e.g. response.create).

This PR translates say() calls into Ultravox ForcedAgentMessage events with uninterruptible: true.

References

ssgutierrez42 and others added 11 commits August 13, 2026 12:09
…mption

Deployment-only plumbing (do NOT include in the upstream PR). When the 11x
livekit-agent worker pulls livekit-plugins-ultravox from this fork via a uv git
source, uv discovers this repo's workspace and resolves livekit-agents (plus its
silero/etc. extras) from the git checkout as file URLs, which uv rejects as
transitive URL deps. Excluding the ultravox plugin from the workspace makes it
resolve standalone so its livekit-agents>=1.6.9 requirement comes from PyPI,
while only the forked ultravox plugin is sourced from git.
Add supports_say=True and implement RealtimeSession.say() using
Ultravox's ForcedAgentMessage wire protocol with uninterruptible=true.

This enables session.say() to speak verbatim text server-side without
LLM processing, with server-enforced uninterruptibility — matching the
pattern used by the xAI plugin's force_message primitive.

The existing _handle_state_event("speaking") handler auto-resolves the
pending generation future, so no new lifecycle plumbing is needed.
@ssgutierrez42
ssgutierrez42 requested a review from a team as a code owner August 27, 2026 00:32
devin-ai-integration[bot]

This comment was marked as resolved.

Comment thread pyproject.toml
@@ -70,7 +69,6 @@
livekit-plugins-telnyx = { workspace = true }
livekit-plugins-trugen = { workspace = true }
livekit-plugins-turn-detector = { workspace = true }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is there a reason for this move?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No - fixing this.


return fut

def say(

@ssgutierrez42 ssgutierrez42 Aug 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@ livekit team, let me know what you think of this approach --

It allows the use of Ultravox's ForcedAgentMessage https://docs.ultravox.ai/apps/datamessages, which can force the underlying TTS engine to "say" anything.

Here we expand on the say() LiveKit functionality
https://docs.livekit.io/agents/multimodality/audio/#session-say
However, given existing documentation, I am curious on the best home for this functionality -- one of:

  • say() - current PR
  • generate_reply when allow_interruptions=False
  • A new function altogether.

This update is influenced by the xAI plugin, which also overrides say with a forced_message approach: https://github.com/livekit/agents/blob/main/livekit-plugins/livekit-plugins-xai/livekit/plugins/xai/realtime/realtime_model.py

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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


return fut

def say(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

For a future PR, we may evaluate how-to pass allow_interruptions=False to this function. It maps to ForcedAgentMessageEvent.uninterruptible

devin-ai-integration[bot]

This comment was marked as resolved.

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.

2 participants