Skip to content

livekit-agents 1.6.0#5688

Open
theomonnom wants to merge 8 commits into
mainfrom
theo/1.6.0-v2
Open

livekit-agents 1.6.0#5688
theomonnom wants to merge 8 commits into
mainfrom
theo/1.6.0-v2

Conversation

@theomonnom
Copy link
Copy Markdown
Member

No description provided.

theomonnom added 5 commits May 8, 2026 09:31
- 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
@chenghao-mou chenghao-mou requested a review from a team May 8, 2026 16:35
@theomonnom theomonnom changed the title Agent service: RemoteSession client, TCP transport, CLI, logging livekit-agents 1.6.0 May 8, 2026
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 3 potential issues.

View 7 additional findings in Devin Review.

Open in Devin Review

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 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)

Open in Devin Review

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”΄ 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.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Comment on lines +90 to 94
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟑 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.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 2 new potential issues.

View 11 additional findings in Devin Review.

Open in Devin Review

client = RemoteSession(client_transport)
await client.start()

resp = await client.run_input("order a big mac", timeout=5.0)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”΄ 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)
Suggested change
resp = await client.run_input("order a big mac", timeout=5.0)
resp = await client.run("order a big mac", timeout=5.0)
Open in Devin Review

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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”΄ 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).

Suggested change
await client.run_input("order a big mac", timeout=10.0)
await client.run("order a big mac", timeout=10.0)
Open in Devin Review

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
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 13 additional findings in Devin Review.

Open in Devin Review

Comment on lines +2299 to +2301
if tts_task and tts_task.done() and not tts_task.cancelled() and (exc := tts_task.exception()):
speech_handle._mark_done(error=exc)
return
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”΄ 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.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 new potential issue.

View 14 additional findings in Devin Review.

Open in Devin Review

Comment on lines +692 to +698
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

πŸ”΄ 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.
Open in Devin Review

Was this helpful? React with πŸ‘ or πŸ‘Ž to provide feedback.

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