Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4259538
Updated to use Voice SDK.
sam-s10s Dec 22, 2025
d4d1662
Include missing updates
sam-s10s Dec 22, 2025
7655115
format update
sam-s10s Dec 22, 2025
c43b6d9
remove messages not needed
sam-s10s Dec 22, 2025
7681cd2
mypy fixes
sam-s10s Dec 22, 2025
096fc27
Updated for using `FIXED` as an end of turn mode.
sam-s10s Dec 22, 2025
fd59362
Ann START_ and END_OF_TURN messages by default
sam-s10s Dec 22, 2025
d047bad
remove extra debugging
sam-s10s Dec 22, 2025
c9dd7cf
Resolved comments in PR.
sam-s10s Jan 20, 2026
9959505
support for external VAD (e.g. Silero)
sam-s10s Feb 2, 2026
5bd61c5
remove debug code
sam-s10s Feb 2, 2026
8c96976
doc update
sam-s10s Feb 2, 2026
3bf80a8
updated docs
sam-s10s Feb 2, 2026
d2e7dbc
merged with PR
sam-s10s Feb 2, 2026
8576e2f
fixes from Devin AI
sam-s10s Feb 2, 2026
8d221b1
Merge branch 'main' into smx/stt-voice-sdk
sam-s10s Feb 2, 2026
ee807ac
linting updates from CI
sam-s10s Feb 2, 2026
1569d05
doc update
sam-s10s Feb 2, 2026
e306249
fix from Devin AI
sam-s10s Feb 2, 2026
f1932e5
Updated Devin AI errors
sam-s10s Feb 2, 2026
6ed69eb
Added support for audio metrics
sam-s10s Feb 2, 2026
a3d36dc
remove redundant disconnect() on Speechmatics client
sam-s10s Feb 2, 2026
24d4a6b
restore disconnect()
sam-s10s Feb 2, 2026
a8afc2d
attribute error
sam-s10s Feb 2, 2026
628091c
Show deprecation warnings for migrated parameters.
sam-s10s Feb 3, 2026
f939ccc
type checking
sam-s10s Feb 3, 2026
ba68543
fix to AgentServerMessageType
sam-s10s Feb 3, 2026
38ed1b9
fix missing dependency
sam-s10s Feb 3, 2026
8d1c488
Squashed commit of the following:
sam-s10s Feb 3, 2026
434500d
remove debug messages
sam-s10s Feb 3, 2026
3b01689
ignore `on` message
sam-s10s Feb 3, 2026
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
2 changes: 1 addition & 1 deletion examples/voice_agents/email_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async def register_for_event(self, context: RunContext):
async def entrypoint(ctx: JobContext):
session = AgentSession(
vad=silero.VAD.load(),
llm=inference.LLM("google/gemini-2.5-flash"),
llm=inference.LLM("openai/gpt-4.1-mini"),
stt=inference.STT("deepgram/nova-3"),
tts=inference.TTS("cartesia/sonic-3"),
)
Expand Down
12 changes: 12 additions & 0 deletions livekit-agents/livekit/agents/llm/chat_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ def copy(
exclude_instructions: bool = False,
exclude_empty_message: bool = False,
exclude_handoff: bool = False,
exclude_config_update: bool = False,
tools: NotGivenOr[Sequence[Tool | Toolset | str]] = NOT_GIVEN,
) -> ChatContext:
items = []
Expand Down Expand Up @@ -343,6 +344,9 @@ def get_tool_names(
if exclude_handoff and item.type == "agent_handoff":
continue

if exclude_config_update and item.type == "agent_config_update":
continue

if (
is_given(tools)
and (item.type == "function_call" or item.type == "function_call_output")
Expand Down Expand Up @@ -389,6 +393,7 @@ def merge(
*,
exclude_function_call: bool = False,
exclude_instructions: bool = False,
exclude_config_update: bool = False,
) -> ChatContext:
"""Add messages from `other_chat_ctx` into this one, avoiding duplicates, and keep items sorted by created_at."""
existing_ids = {item.id for item in self._items}
Expand All @@ -407,6 +412,9 @@ def merge(
):
continue

if exclude_config_update and item.type == "agent_config_update":
continue

if item.id not in existing_ids:
idx = self.find_insertion_index(created_at=item.created_at)
self._items.insert(idx, item)
Expand All @@ -422,6 +430,7 @@ def to_dict(
exclude_timestamp: bool = True,
exclude_function_call: bool = False,
exclude_metrics: bool = False,
exclude_config_update: bool = False,
) -> dict[str, Any]:
items: list[ChatItem] = []
for item in self.items:
Expand All @@ -431,6 +440,9 @@ def to_dict(
]:
continue

if exclude_config_update and item.type == "agent_config_update":
continue

if item.type == "message":
item = item.model_copy()
if exclude_image:
Expand Down
4 changes: 0 additions & 4 deletions livekit-agents/livekit/agents/llm/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,6 @@ def generate_reply(
@abstractmethod
def commit_audio(self) -> None: ...

# commit the user turn to the server
@abstractmethod
def commit_user_turn(self) -> None: ...

# clear the input audio buffer to the server
@abstractmethod
def clear_audio(self) -> None: ...
Expand Down
1 change: 1 addition & 0 deletions livekit-agents/livekit/agents/stt/stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class SpeechData:
@dataclass
class RecognitionUsage:
audio_duration: float
"""Incremental audio duration/usage in seconds"""


@dataclass
Expand Down
15 changes: 15 additions & 0 deletions livekit-agents/livekit/agents/voice/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,18 @@ def _handle_task_done(_: asyncio.Task[Any]) -> None:
old_agent = old_activity.agent
session = old_activity.session

old_allow_interruptions = True
if speech_handle:
if speech_handle.interrupted:
raise RuntimeError(
f"{self.__class__.__name__} cannot be awaited inside a function tool that is already interrupted"
)

# lock the speech handle to prevent interruptions until the task is complete
# there should be no await before this line to avoid race conditions
old_allow_interruptions = speech_handle.allow_interruptions
speech_handle.allow_interruptions = False

blocked_tasks = [current_task]
if (
old_activity._on_enter_task
Comment on lines +768 to 773

Choose a reason for hiding this comment

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

🔴 SpeechHandle.allow_interruptions not restored if session._update_activity raises

In the InlineTask.__await_impl__ method, speech_handle.allow_interruptions is set to False before entering the try block, but restoration only occurs in the finally block. If session._update_activity() raises an exception (line 790), the finally block never executes and allow_interruptions remains permanently disabled.

Click to expand

Code flow showing the issue:

# Line 768-769: allow_interruptions is set to False
old_allow_interruptions = speech_handle.allow_interruptions
speech_handle.allow_interruptions = False

# Lines 770-790: Various setup code runs OUTSIDE try block
blocked_tasks = [current_task]
# ...
await session._update_activity(...)  # If this raises, finally won't run!

# Line 801-806: try/finally starts here
try:
    return await asyncio.shield(self.__fut)
finally:
    if speech_handle:
        speech_handle.allow_interruptions = old_allow_interruptions  # Never reached if above throws

Impact:

If _update_activity fails for any reason (network issues, invalid state, etc.), the speech handle will be stuck with allow_interruptions=False, preventing all future interruptions for that speech. This could cause the agent to become uninterruptible, leading to a poor user experience where users cannot stop the agent from speaking.

(Refers to lines 768-790)

Recommendation: Move the speech_handle.allow_interruptions = False assignment inside the try block, or wrap the entire section from line 768 to the try block in a try/finally to ensure restoration happens regardless of where exceptions occur.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Expand Down Expand Up @@ -790,6 +802,9 @@ def _handle_task_done(_: asyncio.Task[Any]) -> None:
return await asyncio.shield(self.__fut)

finally:
if speech_handle:
speech_handle.allow_interruptions = old_allow_interruptions

# run_state could have changed after self.__fut
run_state = session._global_run_state

Expand Down
Loading
Loading