Skip to content

Conversation

@longcw
Copy link
Contributor

@longcw longcw commented Jan 19, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced text message handling with improved event-driven processing
  • Bug Fixes

    • Improved resource lifecycle management in console mode
  • Chores

    • Updated livekit-protocol dependency to v1.1.2 or newer

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 19, 2026

📝 Walkthrough

Walkthrough

SMS/text message handling is refactored from a boolean flag (sms_job) to an object-based request (text_request: TextMessageRequest | None). The text flow transitions from handler-driven to server-driven processing. IO lifecycle management is added to AgentsConsole with explicit acquire/release semantics. Core worker and CLI components are updated to support text request queuing and async response collection.

Changes

Cohort / File(s) Summary
Data Structure Updates
livekit-agents/livekit/agents/job.py, livekit-agents/livekit/agents/cli/proto.py, livekit-agents/livekit/agents/ipc/proto.py
Replaced sms_job: bool with text_request: agent.TextMessageRequest | None in RunningJobInfo. Added TextMessageContext.mark_done() method. Updated serialization/deserialization logic across IPC and CLI proto layers to handle text request objects instead of boolean flags.
Worker Core Refactoring
livekit-agents/livekit/agents/worker.py
Introduced _EntrypointWrapper (replaces _WrappedEntrypoint) with text handler routing. Added text request channel (_text_request_ch) and consumer task for queue-based processing. Updated simulate_job signature to accept text_request parameter. Added _handle_text_request handler and message routing in _recv_task.
CLI and Console Lifecycle
livekit-agents/livekit/agents/cli/cli.py
Added explicit IO lifecycle management with acquire_io() and release_io() methods. Refactored _sms_text_mode to accept AgentServer and loop parameters, replacing handler-based flow with server.simulate_job and response queue collection. Updated _ConsoleWorker constructor signature to use simulate_job_on_start: bool = True.
Type Hints
livekit-agents/livekit/agents/voice/agent.py
Added type: ignore comments to four public attribute assignments in configure method (turn_detection, stt, llm, tts) without functional changes.
Example and Configuration
examples/voice_agents/sms_agent.py, livekit-agents/pyproject.toml, tests/test_ipc.py
Renamed sms_handler() to text_handler() in example. Updated test setup to use text_request=None in RunningJobInfo construction. Bumped livekit-protocol dependency from >=1.1,<2 to >=1.1.2,<2.

Sequence Diagram

sequenceDiagram
    participant CLI as CLI/Console
    participant Server as AgentServer
    participant Worker as Agent Worker
    participant Job as Job Context
    participant Handler as Text Handler

    CLI->>Server: simulate_job(text_request)
    Note over Server: Queues job with text_request
    
    Server->>Worker: send StartJobRequest with text_request
    Worker->>Worker: _handle_text_request(text_request)
    Worker->>Worker: enqueue to _text_request_ch
    
    Worker->>Job: create JobContext with text_message_context
    Note over Job: Initializes from text_request
    
    Worker->>Handler: invoke text_handler(job_context)
    Handler->>Job: ctx.text_message_context.mark_done()
    Note over Job: Signals completion via response channel
    
    Job->>CLI: response collected from queue
    CLI->>CLI: serialize session data to file
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 From flags to flows, the text takes a bow,
Server-driven journeys show us how,
IO lifecycles spin their dance divine,
TextRequests flow where booleans once align,
A rabbit's refactor, quite sublime! ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.88% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'hand text message request from lk server' contains a typo ('hand' should be 'handle') and is unclear; it partially relates to the changes but is poorly phrased. Revise the title to 'Handle text message requests from LiveKit server' or similar for clarity and to fix the apparent typo.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@longcw longcw marked this pull request as ready for review January 20, 2026 08:17
@longcw
Copy link
Contributor Author

longcw commented Jan 20, 2026

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@livekit-agents/livekit/agents/voice/agent.py`:
- Around line 134-138: The llm parameter may be an LLMModels literal but only
str values are converted to an LLM instance; update the logic around the llm
parameter handling in agent.py (the block that checks is_given(llm) and assigns
self._llm) so that LLMModels values are passed through
inference.LLM.from_model_string(...) just like str values before setting
self._llm, and remove the use of the type: ignore assignment; use
inference.LLM.from_model_string(llm) for any string-like model identifier (str
or LLMModels) to ensure self._llm always holds an inference.LLM instance.
- Around line 139-142: The tts handling is inconsistent between __init__ and
configure: either mirror the # type: ignore usage or improve type narrowing so
mypy accepts both; preferred fix—remove the # type: ignore in configure and
explicitly narrow/cast the type in both places: in __init__ and configure check
isinstance(tts, str) and convert via inference.TTS.from_model_string(tts), then
assign self._tts = typing.cast(inference.TTS, converted_or_original_tts) (or use
typing.cast on the final value) so mypy knows self._tts is inference.TTS; update
both __init__ and configure where the tts parameter is processed (variable name
tts and attribute self._tts) to use the same explicit cast/narrowing approach.
- Around line 120-123: The type: ignore on the configure() assignment indicates
_turn_detection has no explicit class annotation; add an explicit type
annotation for the attribute (e.g., declare _turn_detection:
NotGivenOr[TurnDetectionMode | None] or _turn_detection:
Optional[TurnDetectionMode] depending on your sentinel type) on the class so
mypy can see assignments from both the constructor and the
is_given(turn_detection) branch; alternatively, if you prefer not to change the
attribute, replace the suppressed assignment in configure() with an explicit
cast to the annotated type (using typing.cast) so mypy no longer requires #
type: ignore.

In `@livekit-agents/livekit/agents/worker.py`:
- Around line 1376-1385: _handle_text_request currently calls
self._text_request_ch.send_nowait(request) which can raise ChanClosed if
aclose() ran while _recv_task is processing; wrap the send in a try/except
catching the ChanClosed (or the specific channel-close exception type used) and
either drop/log the message (use logger.debug/info with agent_name, message_id,
session_id) or check a worker-closing flag before sending to avoid raising;
ensure symbols referenced: _handle_text_request,
self._text_request_ch.send_nowait, aclose(), and _recv_task.

In `@livekit-agents/pyproject.toml`:
- Line 34: Update the broken dependency spec "livekit-protocol>=1.1.2,<2" to a
published release: change the requirement to "livekit-protocol>=1.1.1,<2" (or
another valid published version) so installation succeeds; locate the string
"livekit-protocol>=1.1.2,<2" in the pyproject dependency list and replace it
with the corrected version specifier.
🧹 Nitpick comments (1)
livekit-agents/livekit/agents/worker.py (1)

623-623: Consider adding a buffer size to prevent unbounded queue growth.

The _msg_chan on line 622 specifies a buffer size of 128, but _text_request_ch has no buffer limit. If text requests arrive faster than they can be processed (each spawns a simulated job), the channel could grow unbounded.

Suggested fix
-            self._text_request_ch = utils.aio.Chan["agent.TextMessageRequest"](loop=self._loop)
+            self._text_request_ch = utils.aio.Chan["agent.TextMessageRequest"](128, loop=self._loop)
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7bdae50 and b503126.

📒 Files selected for processing (9)
  • examples/voice_agents/sms_agent.py
  • livekit-agents/livekit/agents/cli/cli.py
  • livekit-agents/livekit/agents/cli/proto.py
  • livekit-agents/livekit/agents/ipc/proto.py
  • livekit-agents/livekit/agents/job.py
  • livekit-agents/livekit/agents/voice/agent.py
  • livekit-agents/livekit/agents/worker.py
  • livekit-agents/pyproject.toml
  • tests/test_ipc.py
🧰 Additional context used
📓 Path-based instructions (1)
**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

**/*.py: Format code with ruff
Run ruff linter and auto-fix issues
Run mypy type checker in strict mode
Maintain line length of 100 characters maximum
Ensure Python 3.9+ compatibility
Use Google-style docstrings

Files:

  • livekit-agents/livekit/agents/job.py
  • livekit-agents/livekit/agents/voice/agent.py
  • tests/test_ipc.py
  • livekit-agents/livekit/agents/ipc/proto.py
  • examples/voice_agents/sms_agent.py
  • livekit-agents/livekit/agents/worker.py
  • livekit-agents/livekit/agents/cli/cli.py
  • livekit-agents/livekit/agents/cli/proto.py
🧬 Code graph analysis (5)
livekit-agents/livekit/agents/voice/agent.py (2)
livekit-agents/livekit/agents/voice/agent_session.py (4)
  • turn_detection (389-390)
  • stt (1347-1348)
  • llm (1351-1352)
  • tts (1355-1356)
livekit-agents/livekit/agents/utils/misc.py (1)
  • is_given (25-26)
livekit-agents/livekit/agents/ipc/proto.py (1)
livekit-agents/livekit/agents/ipc/channel.py (4)
  • write_bool (91-92)
  • write_bytes (62-64)
  • read_bool (95-96)
  • read_bytes (67-69)
examples/voice_agents/sms_agent.py (3)
livekit-agents/livekit/agents/worker.py (3)
  • text_handler (522-525)
  • text_handler (528-533)
  • text_handler (535-562)
livekit-agents/livekit/agents/job.py (2)
  • TextMessageContext (134-161)
  • text (156-157)
livekit-agents/livekit/agents/llm/tool_context.py (1)
  • info (142-143)
livekit-agents/livekit/agents/worker.py (1)
livekit-agents/livekit/agents/job.py (4)
  • TextMessageContext (134-161)
  • text_message_context (374-375)
  • mark_done (148-149)
  • agent (363-364)
livekit-agents/livekit/agents/cli/proto.py (3)
livekit-agents/livekit/agents/ipc/channel.py (5)
  • write_bool (91-92)
  • write_bytes (62-64)
  • read_bytes (67-69)
  • read_string (78-80)
  • read_bool (95-96)
livekit-agents/livekit/agents/ipc/proto.py (10)
  • read (33-38)
  • read (51-52)
  • read (65-66)
  • read (81-83)
  • read (108-128)
  • read (142-143)
  • read (156-157)
  • read (174-177)
  • read (196-201)
  • read (213-214)
livekit-agents/livekit/agents/job.py (6)
  • job (344-346)
  • job (717-718)
  • agent (363-364)
  • RunningJobInfo (92-99)
  • JobAcceptArguments (84-88)
  • worker_id (349-351)
🔇 Additional comments (24)
livekit-agents/livekit/agents/voice/agent.py (1)

124-129: No conversion issue exists. STTModels values are string literals that are caught and converted via the isinstance(stt, str) check at line 125. The from_model_string() method converts them to STT instances (line 126), just as intended. The # type: ignore on line 127 suppresses a mypy type-narrowing limitation with union types, not a missing code path. This pattern is identical to the constructor (line 81), which lacks the suppression comment only because of different type context.

Likely an incorrect or invalid review comment.

examples/voice_agents/sms_agent.py (1)

101-103: Rename aligns with the server-driven text flow.
Log wording update is consistent with the new handler name.

livekit-agents/livekit/agents/cli/cli.py (7)

22-43: Imports updated for text-request handling.
No issues with the new imports.


294-369: IO lifecycle tracking + release hook look solid.
Centralized state and cleanup should prevent stale console IO.


404-450: Guarded IO access is solid.
The stricter checks/asserts make IO usage safer.


611-688: Explicit IO asserts keep callbacks safe.
Nice to see clearer assumptions in the audio callbacks.


1402-1444: simulate_job_on_start flag cleanly separates console vs SMS.
The toggle keeps behavior explicit and easy to reason about.


1507-1507: Console/SMS worker wiring looks consistent.
Defaults for console and the explicit disable for SMS are clear.

Also applies to: 1569-1575


1164-1256: No action needed — response channel closure is guaranteed.
The text handler invocation in worker.py is wrapped in a try/finally block that automatically calls ctx.text_message_context.mark_done() (which closes response_ch) regardless of handler success or failure. Channel closure is managed by the framework, not dependent on handler behavior.

Likely an incorrect or invalid review comment.

tests/test_ipc.py (1)

110-119: Fixture aligned with text_request.
Matches the updated RunningJobInfo schema.

livekit-agents/livekit/agents/job.py (3)

91-100: RunningJobInfo now carries text_request.
Looks correct and keeps the payload optional.


134-150: mark_done() provides a clean completion signal.
Good to have an explicit close for the response channel.


199-205: TextMessageContext wiring into JobContext looks correct.
The new property keeps the text flow discoverable.

Also applies to: 373-375

livekit-agents/livekit/agents/ipc/proto.py (1)

94-128: IPC StartJobRequest serialization stays symmetric.
Optional text_request gating is handled consistently.

livekit-agents/livekit/agents/cli/proto.py (1)

48-87: ActiveJobsResponse now carries optional text_request cleanly.
Serialization/deserialization aligns with the new schema.

livekit-agents/livekit/agents/worker.py (9)

74-121: LGTM! Well-structured entrypoint wrapper with proper cleanup.

The refactored _EntrypointWrapper correctly routes between RTC sessions and text message handling. The finally block ensures mark_done() and shutdown() are called even if the text handler raises an exception, which is appropriate for resource cleanup.


645-645: LGTM!

Correctly passes both the RTC entrypoint and text handler functions to the wrapper.


695-695: LGTM!

Consistent initialization pattern with other optional tasks like _conn_task and _load_task.


1019-1021: LGTM! Correct shutdown sequence.

Closing the channel first allows the async for loop to exit gracefully, then cancel_and_wait ensures complete cleanup.


1195-1196: LGTM!

The handler correctly dispatches to _handle_text_request. Since that method uses send_nowait, it won't block the receive loop.


1238-1238: LGTM!

Correctly propagates text_request when reloading jobs.


1312-1312: LGTM!

Correctly sets text_request=None for RTC jobs accepted through the standard availability flow.


372-372: LGTM!

The _text_handler_fnc field is properly initialized and follows the same pattern as other handler functions.


943-996: Backward-compatible parameter enhancement - no breaking changes.

The change from sms_job: bool to text_request: agent.TextMessageRequest | None = None is backward compatible. The text_request parameter is optional and defaults to None, allowing all existing call sites to work without modification. All callers have been properly updated where applicable.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 120 to +123
if is_given(turn_detection):
self._turn_detection = turn_detection
self._turn_detection = turn_detection # type: ignore
restart_required = True

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, find the agent.py file and check its location
find . -name "agent.py" -path "*/voice/*" | head -20

Repository: livekit/agents

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Check the _turn_detection attribute type annotation in the file
rg -n "_turn_detection" livekit-agents/livekit/agents/voice/agent.py -A 2 -B 2

Repository: livekit/agents

Length of output: 897


🏁 Script executed:

#!/bin/bash
# Find the is_given function definition and check if it's a TypeGuard
rg -n "def is_given" --type=py | head -20

Repository: livekit/agents

Length of output: 156


🏁 Script executed:

#!/bin/bash
# Check the type hints and imports in the agent.py file
head -100 livekit-agents/livekit/agents/voice/agent.py

Repository: livekit/agents

Length of output: 3862


🏁 Script executed:

#!/bin/bash
# Search for the _turn_detection attribute declaration in the Agent class
rg -n "class Agent" livekit-agents/livekit/agents/voice/agent.py -A 200 | grep -E "_turn_detection|^[0-9]+-\s*(def|@|class)" | head -30

Repository: livekit/agents

Length of output: 936


🏁 Script executed:

#!/bin/bash
# Look for all occurrences of _turn_detection to understand its type
rg -n "_turn_detection" livekit-agents/livekit/agents/voice/agent.py

Repository: livekit/agents

Length of output: 257


🏁 Script executed:

#!/bin/bash
# Check the imports and type annotations more carefully
sed -n '1,120p' livekit-agents/livekit/agents/voice/agent.py

Repository: livekit/agents

Length of output: 4813


🏁 Script executed:

#!/bin/bash
# Look at the configure method signature and context around line 120-142
sed -n '95,160p' livekit-agents/livekit/agents/voice/agent.py

Repository: livekit/agents

Length of output: 2564


🏁 Script executed:

#!/bin/bash
# Check if there's an explicit type annotation for _turn_detection as a class variable
rg -n "^\s*_turn_detection\s*:" livekit-agents/livekit/agents/voice/agent.py

Repository: livekit/agents

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check the is_given implementation to understand the TypeGuard
sed -n '20,40p' livekit-agents/livekit/agents/utils/misc.py

Repository: livekit/agents

Length of output: 530


🏁 Script executed:

#!/bin/bash
# Look for any mypy config or similar patterns in the codebase
find . -name "pyproject.toml" -o -name "mypy.ini" -o -name ".mypy.ini" | head -5

Repository: livekit/agents

Length of output: 302


🏁 Script executed:

#!/bin/bash
# Check mypy configuration in pyproject.toml
cat livekit-agents/pyproject.toml | grep -A 20 "\[tool.mypy\]"

Repository: livekit/agents

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Run mypy on the specific file to see what errors occur
cd livekit-agents && python -m pip install -q mypy typing-extensions 2>/dev/null; python -m mypy livekit/agents/voice/agent.py --strict 2>&1 | grep -A 3 "_turn_detection\|line 121\|line 127\|line 137\|line 142" | head -30

Repository: livekit/agents

Length of output: 144


🏁 Script executed:

#!/bin/bash
# Check if NotGiven and NotGivenOr are properly defined
rg -n "class NotGiven|type NotGivenOr" livekit-agents/livekit/agents/types.py -A 3

Repository: livekit/agents

Length of output: 149


🏁 Script executed:

#!/bin/bash
# Check the NotGivenOr type definition
rg -n "NotGivenOr\s*=" livekit-agents/livekit/agents/types.py -A 2

Repository: livekit/agents

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Look at the full types.py to understand the type system
head -80 livekit-agents/livekit/agents/types.py

Repository: livekit/agents

Length of output: 2030


🏁 Script executed:

#!/bin/bash
# Check what TurnDetectionMode is
rg -n "class TurnDetectionMode|TurnDetectionMode\s*=" livekit-agents/livekit/agents/voice/audio_recognition.py | head -10

Repository: livekit/agents

Length of output: 150


Address the type: ignore suppression for turn_detection assignment in the configure method.

The is_given() function is a proper TypeGuard[_T] that should narrow NotGivenOr[TurnDetectionMode | None] to TurnDetectionMode | None. However, the # type: ignore comment on line 121 suggests mypy is not fully trusting this narrowing.

The discrepancy is notable: the constructor (line 70) assigns the same unguarded NotGivenOr parameter type to _turn_detection without requiring suppression, yet the configure method requires it despite the TypeGuard. Since _turn_detection lacks an explicit type annotation, its type is inferred from assignments, which may be creating ambiguity.

Given the coding guideline to "Run mypy type checker in strict mode," type errors should be resolved properly rather than suppressed. Either:

  1. Add an explicit type annotation to _turn_detection in the class to clarify its intended type
  2. Use explicit type assertions instead of relying on the TypeGuard if mypy has limitations
  3. Verify that the TypeGuard is actually working as expected with mypy
🤖 Prompt for AI Agents
In `@livekit-agents/livekit/agents/voice/agent.py` around lines 120 - 123, The
type: ignore on the configure() assignment indicates _turn_detection has no
explicit class annotation; add an explicit type annotation for the attribute
(e.g., declare _turn_detection: NotGivenOr[TurnDetectionMode | None] or
_turn_detection: Optional[TurnDetectionMode] depending on your sentinel type) on
the class so mypy can see assignments from both the constructor and the
is_given(turn_detection) branch; alternatively, if you prefer not to change the
attribute, replace the suppressed assignment in configure() with an explicit
cast to the annotated type (using typing.cast) so mypy no longer requires #
type: ignore.

Comment on lines 134 to +138
if is_given(llm):
if isinstance(llm, str):
llm = inference.LLM.from_model_string(llm)
self._llm = llm
self._llm = llm # type: ignore

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for LLMModels definition
rg -n 'LLMModels\s*=' livekit-agents/livekit/agents/ --type py -B2 -A2

# Also search for TypeAlias definitions with LLMModels
rg -n 'LLMModels' livekit-agents/livekit/agents/ --type py | head -20

Repository: livekit/agents

Length of output: 1641


🏁 Script executed:

#!/bin/bash
# Check the from_model_string method to understand what it does
rg -n 'from_model_string' livekit-agents/livekit/agents/inference/llm.py -B2 -A10

# Also check what the type signature says about accepting LLMModels
rg -n 'def from_model_string' livekit-agents/livekit/agents/inference/llm.py -A15

Repository: livekit/agents

Length of output: 876


🏁 Script executed:

#!/bin/bash
# Check the LLM class constructor to see what types it accepts
rg -n 'class LLM' livekit-agents/livekit/agents/inference/llm.py -A30 | head -50

# Check the types of OpenAIModels, GoogleModels, etc. to confirm they're Literal
rg -n 'OpenAIModels|GoogleModels|QwenModels|KimiModels|DeepSeekModels' livekit-agents/livekit/agents/inference/llm.py | head -10

Repository: livekit/agents

Length of output: 2203


🏁 Script executed:

#!/bin/bash
# Check what the _llm attribute is typed as in the Agent class
rg -n '_llm' livekit-agents/livekit/agents/voice/agent.py -B2 -A2 | head -30

# Also check the full context around line 134-138
sed -n '130,145p' livekit-agents/livekit/agents/voice/agent.py

Repository: livekit/agents

Length of output: 1710


🏁 Script executed:

#!/bin/bash
# Check the stt parameter handling for comparison
sed -n '125,133p' livekit-agents/livekit/agents/voice/agent.py

# Check the type annotation for _llm attribute
rg -n '_llm.*:' livekit-agents/livekit/agents/voice/agent.py

Repository: livekit/agents

Length of output: 527


Handle LLMModels type conversion consistently with str.

The parameter accepts llm: NotGivenOr[llm.LLM | llm.RealtimeModel | LLMModels | str | None], where LLMModels is a type alias for a union of specific model literal strings (OpenAI, Google, Qwen, Kimi, DeepSeek models). However, only str values are converted to llm.LLM instances (line 136); LLMModels values bypass this conversion and are assigned directly with # type: ignore (line 137).

Since LLMModels represents valid model identifiers and LLM.__init__ accepts both LLMModels | str, the LLMModels case should be converted to llm.LLM using from_model_string() for consistency—just like the str case. The current pattern allows a literal model string to be stored directly in self._llm rather than wrapped as an LLM instance, which is inconsistent with the intent of converting string-based models to proper LLM objects.

🤖 Prompt for AI Agents
In `@livekit-agents/livekit/agents/voice/agent.py` around lines 134 - 138, The llm
parameter may be an LLMModels literal but only str values are converted to an
LLM instance; update the logic around the llm parameter handling in agent.py
(the block that checks is_given(llm) and assigns self._llm) so that LLMModels
values are passed through inference.LLM.from_model_string(...) just like str
values before setting self._llm, and remove the use of the type: ignore
assignment; use inference.LLM.from_model_string(llm) for any string-like model
identifier (str or LLMModels) to ensure self._llm always holds an inference.LLM
instance.

Comment on lines 139 to +142
if is_given(tts):
if isinstance(tts, str):
tts = inference.TTS.from_model_string(tts)
self._tts = tts
self._tts = tts # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify what TTSModels is and check overall type pattern

# Check the definition of TTSModels
rg -nP --type=py -C5 'TTSModels\s*=' livekit-agents/livekit/agents/

# Check all three Models types together
rg -nP --type=py -C3 'class (STT|LLM|TTS)Models|(STT|LLM|TTS)Models.*(TypeAlias|Protocol)' livekit-agents/livekit/agents/inference/

Repository: livekit/agents

Length of output: 884


🏁 Script executed:

#!/bin/bash
# Check STTModels and LLMModels definitions
rg -nP --type=py 'STTModels\s*=' livekit-agents/livekit/agents/
rg -nP --type=py 'LLMModels\s*=' livekit-agents/livekit/agents/

# Also check the agent.py constructor to see the full pattern
cat -n livekit-agents/livekit/agents/voice/agent.py | head -100

Repository: livekit/agents

Length of output: 4768


🏁 Script executed:

#!/bin/bash
# Check the actual code at lines 139-142
sed -n '130,160p' livekit-agents/livekit/agents/voice/agent.py

Repository: livekit/agents

Length of output: 1107


🏁 Script executed:

#!/bin/bash
# Find the method containing this code block
sed -n '100,145p' livekit-agents/livekit/agents/voice/agent.py | head -50

Repository: livekit/agents

Length of output: 1893


🏁 Script executed:

#!/bin/bash
# Verify the field type for self._tts to understand full assignment context
rg -nP --type=py 'self\._tts\s*[:=]' livekit-agents/livekit/agents/voice/agent.py | head -5

Repository: livekit/agents

Length of output: 132


🏁 Script executed:

#!/bin/bash
# Check mypy configuration to understand strict mode requirement
fd -name 'pyproject.toml|mypy.ini|setup.cfg' livekit-agents/ | head -5
rg -l '\[tool\.mypy\]|^\[mypy\]' livekit-agents/ | head -3

Repository: livekit/agents

Length of output: 288


🏁 Script executed:

#!/bin/bash
# Check mypy configuration to understand strict mode requirement
fd 'pyproject.toml|mypy.ini|setup.cfg' livekit-agents/ | head -5
cat livekit-agents/pyproject.toml | grep -A 20 '\[tool\.mypy\]'

Repository: livekit/agents

Length of output: 87


🏁 Script executed:

#!/bin/bash
# Read pyproject.toml to check mypy settings
cat livekit-agents/pyproject.toml | grep -A 15 'tool.mypy'

Repository: livekit/agents

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Check if mypy strict mode is enforced elsewhere or in the file directly
cat livekit-agents/pyproject.toml | head -100

Repository: livekit/agents

Length of output: 3657


🏁 Script executed:

#!/bin/bash
# Continue reading pyproject.toml to find mypy settings
tail -100 livekit-agents/pyproject.toml

Repository: livekit/agents

Length of output: 3470


🏁 Script executed:

#!/bin/bash
# Search for mypy configuration files
find livekit-agents -name 'mypy.ini' -o -name '.mypy.ini' 2>/dev/null

# Check if there's mypy config in setup.cfg
grep -l 'mypy' livekit-agents/setup.cfg 2>/dev/null || echo "No setup.cfg mypy config"

# Check ruff config since the guidelines mention ruff
grep -A 10 'tool.ruff' livekit-agents/pyproject.toml | head -20

Repository: livekit/agents

Length of output: 82


🏁 Script executed:

#!/bin/bash
# Check what from_model_string expects in the inference.TTS class
rg -nP --type=py 'def from_model_string' livekit-agents/livekit/agents/inference/ -A 5

# Also check the TTS class definition to see what it accepts
rg -nP --type=py 'class TTS' livekit-agents/livekit/agents/inference/tts.py -A 10 | head -30

Repository: livekit/agents

Length of output: 1954


🏁 Script executed:

#!/bin/bash
# Check what self._tts is typed as to understand the full picture
rg -nP --type=py 'self\._tts\s*:' livekit-agents/livekit/agents/voice/agent.py

# Also check if there are any type annotations on _tts in the class
rg -nP --type=py '_tts' livekit-agents/livekit/agents/voice/agent.py | grep -E ':\s*(tts\.|TTS|NotGivenOr)' | head -5

Repository: livekit/agents

Length of output: 126


🏁 Script executed:

#!/bin/bash
# Get the full class definition to find type annotations
sed -n '41,300p' livekit-agents/livekit/agents/voice/agent.py | grep -E '(class Agent|self\._tts|self\._stt|self\._llm)' | head -20

Repository: livekit/agents

Length of output: 274


🏁 Script executed:

#!/bin/bash
# Let's verify if there are type stubs or if the fields have type annotations elsewhere
sed -n '1,100p' livekit-agents/livekit/agents/voice/agent.py | grep -A 50 'class Agent'

Repository: livekit/agents

Length of output: 2250


Inconsistency in type narrowing and # type: ignore usage across similar code blocks.

In the configure() method, the tts parameter handling (lines 140-142) adds # type: ignore to suppress a mypy strict mode type narrowing issue. However, the identical logic in __init__ (lines 78-79) does not include # type: ignore.

The issue: TTSModels is Union[CartesiaModels, ElevenlabsModels, RimeModels, InworldModels], where each is a Literal of strings. The code only explicitly checks isinstance(tts, str) and converts it. While TTSModels values are strings at runtime and will be caught by this check, mypy in strict mode cannot narrow Union[TTSModels, str] properly because TTSModels is itself a string union. This leaves remaining union members unresolved after the isinstance check, which is why # type: ignore is needed.

Either:

  1. Remove the # type: ignore from line 142 and add it to line 83 in __init__ for consistency, or
  2. Improve type narrowing by explicitly checking for and handling TTSModels separately, eliminating the need for # type: ignore
🤖 Prompt for AI Agents
In `@livekit-agents/livekit/agents/voice/agent.py` around lines 139 - 142, The tts
handling is inconsistent between __init__ and configure: either mirror the #
type: ignore usage or improve type narrowing so mypy accepts both; preferred
fix—remove the # type: ignore in configure and explicitly narrow/cast the type
in both places: in __init__ and configure check isinstance(tts, str) and convert
via inference.TTS.from_model_string(tts), then assign self._tts =
typing.cast(inference.TTS, converted_or_original_tts) (or use typing.cast on the
final value) so mypy knows self._tts is inference.TTS; update both __init__ and
configure where the tts parameter is processed (variable name tts and attribute
self._tts) to use the same explicit cast/narrowing approach.

Comment on lines +1376 to +1385
def _handle_text_request(self, request: agent.TextMessageRequest) -> None:
logger.debug(
"received text request",
extra={
"agent_name": self._agent_name,
"message_id": request.message_id,
"session_id": request.session_id,
},
)
self._text_request_ch.send_nowait(request)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle potential ChanClosed exception from send_nowait.

If aclose() is called while _recv_task is still processing messages, send_nowait will raise a ChanClosed exception when the channel is closed. Consider wrapping in a try/except or checking if the worker is closing.

Suggested fix
     def _handle_text_request(self, request: agent.TextMessageRequest) -> None:
         logger.debug(
             "received text request",
             extra={
                 "agent_name": self._agent_name,
                 "message_id": request.message_id,
                 "session_id": request.session_id,
             },
         )
-        self._text_request_ch.send_nowait(request)
+        with contextlib.suppress(utils.aio.ChanClosed):
+            self._text_request_ch.send_nowait(request)
🤖 Prompt for AI Agents
In `@livekit-agents/livekit/agents/worker.py` around lines 1376 - 1385,
_handle_text_request currently calls self._text_request_ch.send_nowait(request)
which can raise ChanClosed if aclose() ran while _recv_task is processing; wrap
the send in a try/except catching the ChanClosed (or the specific channel-close
exception type used) and either drop/log the message (use logger.debug/info with
agent_name, message_id, session_id) or check a worker-closing flag before
sending to avoid raising; ensure symbols referenced: _handle_text_request,
self._text_request_ch.send_nowait, aclose(), and _recv_task.

"livekit==1.0.23",
"livekit-api>=1.0.7,<2",
"livekit-protocol>=1.1,<2",
"livekit-protocol>=1.1.2,<2",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

livekit-protocol 1.1.2 PyPI release

💡 Result:

There is no livekit-protocol 1.1.2 on PyPI — the latest release listed is 1.1.1 (uploaded Dec 2, 2025). [1]

[1] livekit-protocol · PyPI.


🏁 Script executed:

#!/bin/bash
# Search for TextMessageRequest usage in the codebase
rg "TextMessageRequest" --type py -A 2 -B 2

Repository: livekit/agents

Length of output: 3303


Version 1.1.2 does not exist on PyPI — update the requirement to an available release.

The livekit-protocol version 1.1.2 specified on line 34 is not available on PyPI. The latest release is version 1.1.1 (released Dec 2, 2025). Installing this dependency as-is will fail. Update the requirement to livekit-protocol>=1.1.1,<2 unless version 1.1.2 is planned for imminent release and should be held until it's published.

🤖 Prompt for AI Agents
In `@livekit-agents/pyproject.toml` at line 34, Update the broken dependency spec
"livekit-protocol>=1.1.2,<2" to a published release: change the requirement to
"livekit-protocol>=1.1.1,<2" (or another valid published version) so
installation succeeds; locate the string "livekit-protocol>=1.1.2,<2" in the
pyproject dependency list and replace it with the corrected version specifier.

@longcw longcw merged commit 1eff439 into longc/text-mode Jan 20, 2026
5 checks passed
@longcw longcw deleted the longc/text-mode-job-dispatch branch January 20, 2026 08:40
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