livekit-agents 1.6.0#5688
Conversation
- Add RemoteSession client class for controlling agents via the lk.agent.session byte stream protocol (run, wait_for_ready, get_chat_history, get_agent_info, get_session_state) - Remove text_input_cb from SessionHost RunInput handler β always use session.run() to properly collect response items - Simulator mode: detect lk.simulator attribute, disable audio I/O - Propagate errors to RunResult
- TcpSessionTransport for local development via TCP socket - SessionHost integration for console mode with TCP audio I/O - Fix IPC: use AgentDevMessage, serialize job as proto bytes - Fix TCP event loop mismatch: defer connection to start()
- Replace rich CLI with minimal argparse-based wrapper - Add --dev flag for development mode - Add --log-format flag (json/colored) - Read LIVEKIT_AGENT_NAME from environment
- Drop tracebacks from expected warnings - Log room name instead of room SID in job metadata - Decouple log format from dev mode flag
There was a problem hiding this comment.
π‘ participant_attributes_changed event handler never deregistered in RoomIO.aclose()
In room_io.py:181, the participant_attributes_changed event handler is registered on the room, but aclose() (lines 208-214) never calls self._room.off("participant_attributes_changed", self._on_participant_attributes_changed). This means the callback persists after the RoomIO is closed, and if the room fires the event later, it will invoke the handler on a stale/closed RoomIO instance, potentially causing errors or unexpected behavior.
(Refers to line 211)
Was this helpful? React with π or π to provide feedback.
| try: | ||
| await server.run(devmode=args.devmode, unregistered=jupyter) | ||
|
|
||
| await server.run(devmode=devmode, unregistered=False) |
There was a problem hiding this comment.
π΄ Jupyter integration broken: worker always registers with LiveKit server
The removal of the jupyter parameter from _run_worker means jupyter now always runs with unregistered=False (hardcoded at cli.py:319). Previously, jupyter.py called cli._run_worker(server, args, jupyter=True) which passed unregistered=jupyter (True) to server.run(), keeping the worker in standalone simulation mode. Now jupyter.py:133 calls cli._run_worker(server, args) which forces unregistered=False, causing the worker to register with the LiveKit agent dispatch system over WebSocket. This means the jupyter worker will accept real dispatched jobs from the server β unintended for notebook experimentation. Additionally, the old code guarded signal handler setup with if not jupyter: to avoid interfering with the Jupyter kernel's own signal handling; the new code always installs signal handlers that raise _ExitCli, which can be problematic in notebook environments.
Prompt for agents
The _run_worker function at cli.py:281 lost its `jupyter` parameter. It now always passes `unregistered=False` to server.run(), but jupyter.py:133 calls _run_worker expecting the old unregistered=True behavior. The fix should either:
1. Add an `unregistered` parameter to _run_worker (or restore the `jupyter` flag) so jupyter.py can request unregistered mode, OR
2. Add an `unregistered` field to proto.CliArgs so it can be passed through, OR
3. Have jupyter.py call server.run() directly with the correct parameters instead of going through _run_worker.
Also, the signal handler setup (cli.py:307-308) should be skipped for jupyter contexts to avoid interfering with the Jupyter kernel's signal handling.
Was this helpful? React with π or π to provide feedback.
| self._text_input_cb: TextInputCallback | None = None | ||
| self._text_stream_handler_registered = False | ||
|
|
||
| self._text_input_cb: TextInputCallback | None = None | ||
| self._chat_handler_registered = False |
There was a problem hiding this comment.
π‘ Duplicate register_text_input method definition causes dead code and duplicate handler registration
The RoomIO class defines register_text_input twice: at line 96 and again at line 349. Python uses the second definition, making the first one dead code. The first definition (line 96) registers _on_chat_text_stream with _chat_handler_registered flag, while the second (line 349) registers _on_user_text_input with _text_stream_handler_registered flag. Similarly, _text_input_cb is declared twice (lines 90 and 93). The stale first method body, _chat_handler_registered flag, and its cleanup in aclose() (lines 216-221) are all dead code. This also means _on_chat_text_stream (line 534) is never registered β if it was intended to serve as the handler, it silently goes unused.
(Refers to lines 90-106)
Prompt for agents
In room_io.py, the class RoomIO has two definitions of `register_text_input` (at line 96 and line 349) and two declarations of `_text_input_cb` (lines 90 and 93). The first `register_text_input` (line 96) is dead code because Python's second definition overrides it. Remove the first definition (lines 96-106), the duplicate `_text_input_cb` at line 90, the `_text_stream_handler_registered` at line 91, and the `_chat_handler_registered` flag plus its cleanup code in aclose() (lines 216-221). Also remove the dead `_on_chat_text_stream` method at line 534 if it's fully superseded by `_on_user_text_input` at line 362.
Was this helpful? React with π or π to provide feedback.
| client = RemoteSession(client_transport) | ||
| await client.start() | ||
|
|
||
| resp = await client.run_input("order a big mac", timeout=5.0) |
There was a problem hiding this comment.
π΄ Tests call non-existent method run_input() on RemoteSession β method is named run()
The RemoteSession class defines a method run() at livekit-agents/livekit/agents/voice/remote_session.py:951, but the tests at tests/test_remote_session.py:194 and tests/test_run_input_errors.py:120 call client.run_input(...) which does not exist on the class. This will cause an AttributeError at runtime, making both test files fail.
Method definition vs. test usage
The implementation:
# remote_session.py:951
async def run(
self, text: str, timeout: float = 60.0
) -> agent_pb.SessionResponse.RunInputResponse:The tests:
# test_remote_session.py:194
resp = await client.run_input("order a big mac", timeout=5.0)
# test_run_input_errors.py:120
await client.run_input("order a big mac", timeout=10.0)| resp = await client.run_input("order a big mac", timeout=5.0) | |
| resp = await client.run("order a big mac", timeout=5.0) | |
Was this helpful? React with π or π to provide feedback.
| await client.start() | ||
|
|
||
| with pytest.raises(RuntimeError, match="failed"): | ||
| await client.run_input("order a big mac", timeout=10.0) |
There was a problem hiding this comment.
π΄ Test calls non-existent run_input() on RemoteSession in error propagation test
Same issue as in test_remote_session.py: the test calls client.run_input(...) but the RemoteSession class only exposes run() (livekit-agents/livekit/agents/voice/remote_session.py:951).
| await client.run_input("order a big mac", timeout=10.0) | |
| await client.run("order a big mac", timeout=10.0) | |
Was this helpful? React with π or π to provide feedback.
β¦t dev log format - Log user input and agent responses at DEBUG level in agent_session - Add ChatContext.to_proto() for serializing to proto ChatContext - Dev mode logs: time-only (no date), no dash separators
| if tts_task and tts_task.done() and not tts_task.cancelled() and (exc := tts_task.exception()): | ||
| speech_handle._mark_done(error=exc) | ||
| return |
There was a problem hiding this comment.
π΄ Unreachable _mark_done(error=exc) for tts_task due to earlier raise exc in for loop
In _do_play_speech, the for loop at lines 2293-2297 iterates over (tts_task, forward_audio_task, forward_text_task) and raises the first exception it finds (raise exc). The tts_task-specific check at lines 2299-2301 that calls speech_handle._mark_done(error=exc) is therefore unreachable when tts_task has an exception β the for loop already raised it. The intent of the new code was to gracefully propagate TTS errors through the speech handle (via _mark_done) instead of raising, but the pre-existing for loop short-circuits that path. The same pattern works correctly in _generate_llm_reply_and_play_speech (lines 2641-2647) because there is no preceding for-loop raising the exception.
Prompt for agents
In _do_play_speech, lines 2293-2297 contain a for loop that raises exceptions from tts_task/forward_audio_task/forward_text_task. This pre-empts the new graceful error propagation via _mark_done at lines 2299-2301. The fix should either: (a) remove or refactor the for loop at 2293-2297 to use _mark_done instead of raise, similar to how _generate_llm_reply_and_play_speech handles it at lines 2641-2647; or (b) remove the dead code at 2299-2301. The intent appears to be (a), since the parallel method _generate_llm_reply_and_play_speech uses the _mark_done pattern without a preceding for-loop raise.
Was this helpful? React with π or π to provide feedback.
| if c._tcp_transport is not None: | ||
| self._session_host = SessionHost( | ||
| c._tcp_transport, | ||
| audio_input=c._tcp_audio_input, | ||
| audio_output=c._tcp_audio_output, | ||
| ) | ||
| self._session_host.register_session(self) |
There was a problem hiding this comment.
π΄ TCP console SessionHost never started β transport connection never established
In the TCP console code path, a TcpSessionTransport is created in _run_tcp_console (cli/cli.py:215) but start() is never called on it. When AgentSession.start() creates a SessionHost using this transport (agent_session.py:692-698), neither SessionHost.start() nor transport.start() is invoked. Because TcpSessionTransport.start() is what opens the TCP connection (setting _reader and _writer), both remain None. This means send_message silently no-ops (remote_session.py:177: if self._closed or self._writer is None: return), and the _recv_loop never runs. As a result, the SessionHost cannot receive any messages β including audio input frames dispatched via _dispatch_transport_message to TcpAudioInput.push_frame() β making the entire TCP console mode non-functional.
Prompt for agents
In agent_session.py around line 698 (after self._session_host.register_session(self)), add `await self._session_host.start()` to establish the TCP connection and start the recv loop. SessionHost.start() internally calls self._transport.start() (which opens the TCP connection via asyncio.open_connection) and creates the _recv_task. Without this call, _writer and _reader on TcpSessionTransport remain None, causing all sends to silently no-op and the receive loop to never start. The same pattern should be checked for the RoomSessionTransport path at line 731-735, though that is a pre-existing issue.
Was this helpful? React with π or π to provide feedback.
No description provided.