Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions livekit-agents/livekit/agents/voice/remote_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,16 +154,15 @@ async def send_message(self, msg: agent_pb.AgentSessionMessage) -> None:

data = msg.SerializeToString()
dest = [self._remote_identity] if self._remote_identity else None
# one stream at a time — concurrent writers interleave on the same topic
# one send at a time, so messages reach the receiver in the order they were sent
async with self._send_lock:
try:
writer = await self._room.local_participant.stream_bytes(
await self._room.local_participant.send_bytes(
data,
name=utils.shortuuid("AS_"),
topic=TOPIC_SESSION_MESSAGES,
destination_identities=dest,
)
await writer.write(data)
await writer.aclose()
except Exception as e:
raise RuntimeError(f"failed to send binary stream message: {e}") from e

Expand Down
2 changes: 1 addition & 1 deletion livekit-agents/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ classifiers = [
]
dependencies = [
"certifi>=2025.6.15",
"livekit==1.1.20",
"livekit==1.1.21",
"livekit-api>=1.2.0,<2",
"livekit-local-inference>=0.2.7",
"livekit-protocol>=1.1.21,<2",
Expand Down
49 changes: 37 additions & 12 deletions tests/test_remote_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from livekit.agents.metrics import AgentSessionUsage
from livekit.agents.voice import remote_session as remote_session_module
from livekit.agents.voice.remote_session import (
TOPIC_SESSION_MESSAGES,
RemoteSession,
RoomSessionTransport,
SessionHost,
Expand Down Expand Up @@ -445,15 +446,12 @@ def _fake_room(*, connected: bool = True, stream_error: Exception | None = None)
room = MagicMock()
room.isconnected = MagicMock(return_value=connected)

async def _stream_bytes(**kwargs):
async def _send_bytes(data, **kwargs):
if stream_error is not None:
raise stream_error
writer = MagicMock()
writer.write = AsyncMock()
writer.aclose = AsyncMock()
return writer
return MagicMock()

room.local_participant.stream_bytes = _stream_bytes
room.local_participant.send_bytes = _send_bytes
return room


Expand Down Expand Up @@ -485,20 +483,17 @@ async def test_room_transport_serializes_concurrent_sends():
overlapping = False
in_flight = 0

async def _stream_bytes(**kwargs):
async def _send_bytes(data, **kwargs):
nonlocal overlapping, in_flight
in_flight += 1
if in_flight > 1:
overlapping = True
await asyncio.sleep(0.01) # a real send yields several times
in_flight -= 1
writer = MagicMock()
writer.write = AsyncMock()
writer.aclose = AsyncMock()
return writer
return MagicMock()

room = _fake_room()
room.local_participant.stream_bytes = _stream_bytes
room.local_participant.send_bytes = _send_bytes
transport = RoomSessionTransport(room)

await asyncio.gather(
Expand All @@ -507,6 +502,36 @@ async def _stream_bytes(**kwargs):
assert not overlapping


@pytest.mark.asyncio
async def test_room_transport_sends_each_message_in_one_call():
"""Each message is a single one-shot send, not an incremental stream.

A one-shot send lets the SDK put a small message in a single packet (and
compress a large one) for clients that support it; an incremental stream
always costs a header, a chunk and a trailer packet per message.
"""
room = _fake_room()
room.local_participant.send_bytes = AsyncMock()
room.local_participant.stream_bytes = AsyncMock()
transport = RoomSessionTransport(room, remote_identity="user-1")

msg = agent_pb.AgentSessionMessage(
event=agent_pb.AgentSessionEvent(
agent_state_changed=agent_pb.AgentSessionEvent.AgentStateChanged(
old_state=agent_pb.AS_LISTENING, new_state=agent_pb.AS_SPEAKING
)
)
)
await transport.send_message(msg)

room.local_participant.stream_bytes.assert_not_called()
room.local_participant.send_bytes.assert_awaited_once()
args, kwargs = room.local_participant.send_bytes.await_args
assert args == (msg.SerializeToString(),)
assert kwargs["topic"] == TOPIC_SESSION_MESSAGES
assert kwargs["destination_identities"] == ["user-1"]


class _FailingTransport(PairedTransport):
async def send_message(self, msg: agent_pb.AgentSessionMessage) -> None:
raise RuntimeError("transport is down")
Expand Down
16 changes: 8 additions & 8 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading