Skip to content

Conversation

@longcw
Copy link
Contributor

@longcw longcw commented Jan 20, 2026

  1. close the job process and delete room after AgentSession closed
  2. add extra_description and on_tool_called, on_tool_completed

Summary by CodeRabbit

  • New Features

    • Enhanced End Call tool: configurable end instructions, lifecycle callbacks, and optional room deletion.
    • Agents now support an on-enter greeting and richer tool-completion hooks for controlled goodbye/greeting flows.
    • Toolset events added to surface tool-called and tool-completed lifecycle events.
  • Updates

    • Session start simplified to a leaner API.
    • Default TTS provider switched to cartesia/sonic-3.

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

@chenghao-mou chenghao-mou requested a review from a team January 20, 2026 02:44
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Introduces a full-featured EndCallTool with lifecycle hooks, delayed shutdown and optional room deletion; adds Toolset event dataclasses for tool-called/completed; and updates the voice-agent example to use an on_enter-driven RealtimeAgent, EndCallTool integration, and simplified session.start with new TTS.

Changes

Cohort / File(s) Summary
EndCallTool implementation
livekit-agents/livekit/agents/beta/tools/end_call.py
Adds EndCallTool class, constructor params (extra_description, delete_room, end_instructions, on_tool_called, on_tool_completed), async _end_call tool wrapper, delayed shutdown logic awaiting SpeechCreatedEvent, session-close handler _on_session_close, emits ToolCalledEvent/ToolCompletedEvent, and exposes tool via tools property. Review for shutdown sequencing, event emission, and job scheduling.
Toolset event types
livekit-agents/livekit/agents/llm/tool_context.py
Adds nested dataclasses ToolCalledEvent and ToolCompletedEvent to Toolset; introduces TYPE_CHECKING guard and a forward RunContext import for typing. Check public API surface and imports.
Voice agent example
examples/voice_agents/session_close_callback.py
Reworks example: uses RealtimeAgent/MyAgent with stt, llm, tts, integrates EndCallTool(end_instructions=..., delete_room=...), adds async def on_enter(self), removes on_exit and room_options flow, switches TTS to cartesia/sonic-3, and simplifies await session.start(agent=MyAgent(), room=ctx.room). Verify example correctness and updated APIs.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Agent as Agent
  participant EndCall as EndCallTool
  participant Session as AgentSession
  participant JobCtx as JobContext
  participant RoomMgr as RoomManager

  Client->>Agent: invoke end-call tool
  Agent->>EndCall: _end_call(ctx)
  EndCall->>Agent: emit ToolCalledEvent(ctx, args)
  EndCall->>Agent: perform end_instructions (TTS/LLM)
  EndCall->>Session: request session shutdown
  Session->>JobCtx: emit CloseEvent(reason)
  JobCtx->>EndCall: call _on_session_close(ev)
  EndCall->>RoomMgr: schedule room deletion if delete_room=True
  EndCall->>Agent: emit ToolCompletedEvent(ctx, output)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through code with nimble paws,
EndCallTool whispers polite goodnights and bows.
Agents enter, speak, then softly part,
Rooms cleared gently — a tidy art.
Carrots celebrate the fresh new start.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.77% 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 'improve EndCallTool' is vague and generic, using a non-descriptive term 'improve' that doesn't convey the specific changes made. Consider a more specific title like 'Enhance EndCallTool with lifecycle hooks and room cleanup' or 'Add job process closure and room deletion to EndCallTool' to better reflect the actual changes.
✅ 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

📜 Recent review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c7f7a32 and 23b5e7b.

📒 Files selected for processing (2)
  • examples/voice_agents/session_close_callback.py
  • livekit-agents/livekit/agents/beta/tools/end_call.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/voice_agents/session_close_callback.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/beta/tools/end_call.py
🧠 Learnings (1)
📚 Learning: 2026-01-22T03:28:16.289Z
Learnt from: longcw
Repo: livekit/agents PR: 4563
File: livekit-agents/livekit/agents/beta/tools/end_call.py:65-65
Timestamp: 2026-01-22T03:28:16.289Z
Learning: In code paths that check capabilities or behavior of the LLM processing the current interaction, prefer using the activity's LLM obtained via ctx.session.current_agent._get_activity_or_raise().llm instead of ctx.session.llm. The session-level LLM may be a fallback and not reflect the actual agent handling the interaction. Use the activity LLM to determine capabilities and to make capability checks or feature toggles relevant to the current processing agent.

Applied to files:

  • livekit-agents/livekit/agents/beta/tools/end_call.py
🧬 Code graph analysis (1)
livekit-agents/livekit/agents/beta/tools/end_call.py (4)
livekit-agents/livekit/agents/job.py (1)
  • get_job_context (56-63)
livekit-agents/livekit/agents/llm/tool_context.py (10)
  • Tool (34-35)
  • Toolset (44-59)
  • function_tool (189-194)
  • function_tool (198-203)
  • function_tool (207-213)
  • function_tool (217-223)
  • function_tool (226-268)
  • ToolCalledEvent (46-48)
  • ToolCompletedEvent (51-53)
  • tools (57-59)
livekit-agents/livekit/agents/voice/events.py (4)
  • CloseEvent (221-225)
  • RunContext (32-81)
  • SpeechCreatedEvent (191-201)
  • speech_handle (52-53)
livekit-agents/livekit/agents/voice/speech_handle.py (2)
  • SpeechHandle (16-251)
  • add_done_callback (163-164)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: livekit-plugins-deepgram
  • GitHub Check: unit-tests
  • GitHub Check: type-check (3.9)
  • GitHub Check: type-check (3.13)
🔇 Additional comments (5)
livekit-agents/livekit/agents/beta/tools/end_call.py (5)

1-9: LGTM!

Imports are well-organized with standard library imports first, followed by internal package imports.


11-25: LGTM!

The description constant provides clear, actionable guidance for the LLM on when to use the end call tool, with appropriate do/don't call scenarios.


29-61: LGTM!

Constructor properly initializes the toolset with configurable options. The function_tool wrapper correctly composes the description from the base constant and extra_description.


110-123: LGTM!

The session close handler correctly manages room deletion and job shutdown. Using get_job_context() is appropriate since this tool is designed to run within a job entrypoint.


125-127: LGTM!

The tools property correctly exposes the end call tool as required by the Toolset interface.

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


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

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: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
examples/voice_agents/session_close_callback.py (1)

19-19: Duplicate AgentServer instantiation.

server = AgentServer() appears twice (lines 19 and 39). The second instantiation on line 39 overwrites the first, making line 19 dead code.

🐛 Proposed fix

Remove one of the duplicate declarations:

 server = AgentServer()
 
 
 class MyAgent(Agent):
     ...
 
-
-server = AgentServer()
-
 
 `@server.rtc_session`()
 async def entrypoint(ctx: JobContext):

Also applies to: 39-39

🧹 Nitpick comments (1)
examples/voice_agents/session_close_callback.py (1)

35-36: Missing await for generate_reply.

The on_enter method calls self.session.generate_reply() without await. Looking at the generate_reply signature in agent_session.py, it returns a SpeechHandle synchronously, so this is technically valid. However, for consistency and to potentially handle any returned handle, consider being explicit about the intent.

If the intent is fire-and-forget, the current code works. If you want to ensure the greeting completes before proceeding, consider:

     async def on_enter(self) -> None:
-        self.session.generate_reply(instructions="say hello to the user")
+        handle = self.session.generate_reply(instructions="say hello to the user")
+        await handle.wait_for_playout()
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0722371 and 38a19e4.

📒 Files selected for processing (2)
  • examples/voice_agents/session_close_callback.py
  • livekit-agents/livekit/agents/beta/tools/end_call.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/beta/tools/end_call.py
  • examples/voice_agents/session_close_callback.py
🧬 Code graph analysis (2)
livekit-agents/livekit/agents/beta/tools/end_call.py (3)
livekit-agents/livekit/agents/job.py (3)
  • get_job_context (56-63)
  • delete_room (423-447)
  • _delete_room (431-442)
livekit-agents/livekit/agents/voice/events.py (2)
  • CloseEvent (221-225)
  • RunContext (32-81)
examples/voice_agents/session_close_callback.py (1)
  • on_end_call (24-28)
examples/voice_agents/session_close_callback.py (3)
livekit-agents/livekit/agents/voice/agent_session.py (3)
  • AgentSession (135-1324)
  • generate_reply (904-957)
  • tools (429-430)
livekit-agents/livekit/agents/voice/events.py (2)
  • RunContext (32-81)
  • session (48-49)
livekit-agents/livekit/agents/beta/tools/end_call.py (2)
  • tools (82-83)
  • EndCallTool (30-83)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: livekit-plugins-cartesia
  • GitHub Check: livekit-plugins-inworld
  • GitHub Check: type-check (3.13)
  • GitHub Check: livekit-plugins-deepgram
  • GitHub Check: livekit-plugins-groq
  • GitHub Check: livekit-plugins-openai
  • GitHub Check: type-check (3.9)
  • GitHub Check: unit-tests
🔇 Additional comments (9)
livekit-agents/livekit/agents/beta/tools/end_call.py (7)

1-7: LGTM!

The imports are well-organized and include all necessary types for the implementation: Awaitable and Callable for type hints, CloseEvent and RunContext from the voice events module.


9-23: LGTM!

The END_CALL_INSTRUCTIONS constant provides clear, well-structured guidance for the LLM on when to invoke the end call tool, with appropriate do/don't scenarios.


26-27: LGTM!

The default callback provides sensible behavior by generating a goodbye message with tool_choice="none" to prevent recursive tool calls.


31-55: LGTM!

The constructor is well-designed with sensible defaults. The function_tool wrapper correctly combines the base instructions with any extra instructions provided.


67-79: LGTM!

The _on_session_close handler correctly:

  1. Retrieves the job context
  2. Conditionally registers a shutdown callback for room deletion
  3. Shuts down the job context with the close reason

The separation of concerns between session close and job shutdown is well-designed.


81-83: LGTM!

The tools property correctly returns the configured end call tool.


57-65: Use the correct API: ctx.wait_for_playout() instead of ctx.speech_handle.wait_for_playout().

The proposed fix has a critical API error. Calling ctx.speech_handle.wait_for_playout() from within a function tool raises RuntimeError with the message: "cannot call SpeechHandle.wait_for_playout() from inside the function tool ... that owns this SpeechHandle." The SpeechHandle class explicitly directs to use RunContext.wait_for_playout() instead for this use case.

Additionally, ctx.session.shutdown() with its default drain=True parameter already awaits the completion of the current speech before closing the session, so the goodbye message should not be interrupted. If you still want to add an explicit wait for clarity, use await ctx.wait_for_playout().

Likely an incorrect or invalid review comment.

examples/voice_agents/session_close_callback.py (2)

1-10: LGTM!

Imports are well-organized and include the new RunContext type needed for the on_end_call callback.


42-80: LGTM!

The session setup and on_close handler are well-implemented. The handler correctly iterates through all chat history item types and provides informative output.

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

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: 1

🤖 Fix all issues with AI agents
In `@examples/voice_agents/session_close_callback.py`:
- Around line 33-34: The on_enter async method calls
self.session.generate_reply(...) which returns an awaitable SpeechHandle but is
not awaited; update on_enter to await the result (i.e., await
self.session.generate_reply(...)) so the greeting completes before the handler
continues, ensuring the speech is enqueued and executed as intended.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 38a19e4 and 0c6b434.

📒 Files selected for processing (1)
  • examples/voice_agents/session_close_callback.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:

  • examples/voice_agents/session_close_callback.py
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: type-check (3.13)
  • GitHub Check: type-check (3.9)
  • GitHub Check: livekit-plugins-cartesia
  • GitHub Check: livekit-plugins-inworld
  • GitHub Check: livekit-plugins-elevenlabs
  • GitHub Check: livekit-plugins-openai
  • GitHub Check: livekit-plugins-deepgram
  • GitHub Check: unit-tests

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

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: 1

🤖 Fix all issues with AI agents
In `@examples/voice_agents/session_close_callback.py`:
- Line 5: Remove the unused RunContext import from the import list in
session_close_callback.py: edit the line importing from livekit.agents (which
currently includes Agent, AgentServer, AgentSession, CloseEvent, JobContext,
RunContext, cli) and delete RunContext so the import list only includes used
symbols (Agent, AgentServer, AgentSession, CloseEvent, JobContext, cli); this
will resolve the Ruff F401 unused-import failure.
♻️ Duplicate comments (1)
examples/voice_agents/session_close_callback.py (1)

27-28: Await generate_reply so the greeting actually runs.
Line 28 returns an awaitable; without await, the greeting may never execute.

✅ Proposed fix
-        self.session.generate_reply(instructions="say hello to the user")
+        await self.session.generate_reply(instructions="say hello to the user")
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0c6b434 and 9b1e72b.

📒 Files selected for processing (2)
  • examples/voice_agents/session_close_callback.py
  • livekit-agents/livekit/agents/beta/tools/end_call.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:

  • examples/voice_agents/session_close_callback.py
  • livekit-agents/livekit/agents/beta/tools/end_call.py
🧬 Code graph analysis (1)
examples/voice_agents/session_close_callback.py (4)
livekit-agents/livekit/agents/voice/agent.py (5)
  • Agent (34-642)
  • tools (107-113)
  • session (635-642)
  • instructions (99-104)
  • tts (534-544)
livekit-agents/livekit/agents/voice/agent_session.py (7)
  • AgentSession (135-1324)
  • tools (429-430)
  • generate_reply (904-957)
  • tts (1264-1265)
  • start (442-453)
  • start (456-467)
  • start (469-715)
livekit-agents/livekit/agents/voice/events.py (3)
  • CloseEvent (221-225)
  • RunContext (32-81)
  • session (48-49)
livekit-agents/livekit/agents/beta/tools/end_call.py (2)
  • tools (84-85)
  • EndCallTool (26-85)
🪛 GitHub Actions: CI
examples/voice_agents/session_close_callback.py

[error] 5-5: F401 'livekit.agents.RunContext' imported but unused

🪛 GitHub Check: ruff
examples/voice_agents/session_close_callback.py

[failure] 5-5: Ruff (F401)
examples/voice_agents/session_close_callback.py:5:86: F401 livekit.agents.RunContext imported but unused

🔇 Additional comments (6)
livekit-agents/livekit/agents/beta/tools/end_call.py (4)

27-53: Clean EndCallTool wiring and config flow.
The constructor cleanly captures configuration and builds the tool description.


55-67: End-call execution flow looks solid.
The try/finally ensures shutdown even if end instructions fail.


69-81: Session-close handling is clear and safe.
Room deletion is correctly gated and shutdown reason is propagated.


83-85: Tools exposure is straightforward.
Returning a single configured tool here is appropriate.

examples/voice_agents/session_close_callback.py (2)

21-25: EndCallTool integration looks good.
Using end_instructions here aligns with the new API.


36-43: Verify ElevenLabs TTS configuration for this example.
Line 39 switches to tts="elevenlabs"—please confirm the provider/credentials are available in your dev environment so the example doesn’t fail at runtime.

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

longcw and others added 2 commits January 20, 2026 16:20
Co-authored-by: David Zhao <dz@livekit.io>
@longcw longcw requested a review from davidzhao January 20, 2026 08:23
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: 1

🤖 Fix all issues with AI agents
In `@livekit-agents/livekit/agents/beta/tools/end_call.py`:
- Around line 30-55: The initializer uses undefined names: change the parameter
extra_description to extra_instructions (or update all uses to match the
declared name) so self._extra_instructions is set from the actual arg, and
replace or define the missing END_CALL_INSTRUCTIONS constant—either
import/rename to the existing module-level constant or add a new
END_CALL_INSTRUCTIONS string constant—and use that in the function_tool
description; update the docstring param name to match the final parameter name
and ensure self._end_call_tool uses the corrected identifiers (references:
__init__, self._extra_instructions, self._end_call_tool, END_CALL_INSTRUCTIONS).
♻️ Duplicate comments (1)
examples/voice_agents/session_close_callback.py (1)

27-28: Await the greeting generation to ensure it actually runs.

generate_reply is awaitable; without await, the greeting may not complete before the handler continues. (Duplicate of earlier feedback.)

✅ Proposed fix
     async def on_enter(self) -> None:
-        self.session.generate_reply(instructions="say hello to the user")
+        await self.session.generate_reply(instructions="say hello to the user")
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64c66e7 and 4fd8b99.

📒 Files selected for processing (2)
  • examples/voice_agents/session_close_callback.py
  • livekit-agents/livekit/agents/beta/tools/end_call.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:

  • examples/voice_agents/session_close_callback.py
  • livekit-agents/livekit/agents/beta/tools/end_call.py
🧬 Code graph analysis (2)
examples/voice_agents/session_close_callback.py (4)
livekit-agents/livekit/agents/voice/agent.py (5)
  • Agent (34-642)
  • tools (107-113)
  • on_enter (206-208)
  • session (635-642)
  • instructions (99-104)
livekit-agents/livekit/agents/voice/agent_session.py (2)
  • tools (429-430)
  • generate_reply (904-957)
livekit-agents/livekit/agents/voice/events.py (2)
  • CloseEvent (221-225)
  • session (48-49)
livekit-agents/livekit/agents/beta/tools/end_call.py (2)
  • tools (86-87)
  • EndCallTool (26-87)
livekit-agents/livekit/agents/beta/tools/end_call.py (3)
livekit-agents/livekit/agents/job.py (4)
  • get_job_context (56-63)
  • delete_room (423-447)
  • _delete_room (431-442)
  • add_shutdown_callback (358-375)
livekit-agents/livekit/agents/llm/tool_context.py (2)
  • Toolset (41-46)
  • info (142-143)
livekit-agents/livekit/agents/voice/events.py (2)
  • CloseEvent (221-225)
  • RunContext (32-81)
🪛 GitHub Check: ruff
livekit-agents/livekit/agents/beta/tools/end_call.py

[failure] 54-54: Ruff (F821)
livekit-agents/livekit/agents/beta/tools/end_call.py:54:53: F821 Undefined name extra_instructions


[failure] 54-54: Ruff (F821)
livekit-agents/livekit/agents/beta/tools/end_call.py:54:28: F821 Undefined name END_CALL_INSTRUCTIONS


[failure] 48-48: Ruff (F821)
livekit-agents/livekit/agents/beta/tools/end_call.py:48:36: F821 Undefined name extra_instructions

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: unit-tests
  • GitHub Check: livekit-plugins-groq
  • GitHub Check: livekit-plugins-deepgram
  • GitHub Check: livekit-plugins-openai
  • GitHub Check: livekit-plugins-inworld
  • GitHub Check: livekit-plugins-cartesia
  • GitHub Check: type-check (3.13)
  • GitHub Check: type-check (3.9)

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

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: 2

🤖 Fix all issues with AI agents
In `@livekit-agents/livekit/agents/beta/tools/end_call.py`:
- Around line 73-75: The fire-and-forget task created and stored in
self._shutdown_session_task for _delayed_session_shutdown(ctx) can swallow
exceptions; update the creation to attach exception handling (e.g., use
task.add_done_callback to log exceptions from task.exception()) or run it inside
an asyncio.TaskGroup/await it during shutdown. Locate the code that sets
self._shutdown_session_task and either wrap the coroutine in a try/except and
schedule that wrapper, add a done-callback that logs task.exception()
(referencing _delayed_session_shutdown and self._shutdown_session_task), or
migrate to asyncio.TaskGroup to ensure exceptions are surfaced and the task is
cancelled/awaited on cleanup.
- Around line 63-76: In _end_call's inner function _on_speech_done, stop
accessing the private attribute ctx.session._activity.llm and use the public
property ctx.session.llm instead; update the conditional that checks
isinstance(llm, RealtimeModel) and llm.capabilities.auto_tool_reply_generation
to reference the new llm variable, leaving the rest of the logic (creating
self._shutdown_session_task via self._delayed_session_shutdown(ctx) or calling
ctx.session.shutdown()) unchanged.
♻️ Duplicate comments (2)
examples/voice_agents/session_close_callback.py (2)

49-51: Missing await on generate_reply call in RealtimeAgent.on_enter.

Same issue as MyAgent - the generate_reply call should be awaited.

🔧 Proposed fix
     async def on_enter(self) -> None:
-        self.session.generate_reply(instructions="say hello to the user")
+        await self.session.generate_reply(instructions="say hello to the user")

32-34: Missing await on generate_reply call.

generate_reply returns an awaitable SpeechHandle. Without await, the greeting may not execute as intended. This applies to both MyAgent.on_enter (line 33) and RealtimeAgent.on_enter (line 50).

🔧 Proposed fix
     async def on_enter(self) -> None:
-        self.session.generate_reply(instructions="say hello to the user")
+        await self.session.generate_reply(instructions="say hello to the user")
🧹 Nitpick comments (1)
livekit-agents/livekit/agents/beta/tools/end_call.py (1)

41-47: Line exceeds 100 characters.

Per coding guidelines, line 43 exceeds the 100-character maximum.

🔧 Proposed fix
         Args:
             extra_description: Additional description to add to the end call tool.
-            delete_room: Whether to delete the room when the user ends the call. deleting the room disconnects all remote users, including SIP callers.
+            delete_room: Whether to delete the room when the user ends the call. Deleting
+                the room disconnects all remote users, including SIP callers.
             end_instructions: Instructions to generate a reply when the user ends the call if provided.
             on_tool_called: Callback to call when the tool is called.
             on_tool_completed: Callback to call when the tool is completed.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 395614d and 884be1d.

📒 Files selected for processing (3)
  • examples/voice_agents/session_close_callback.py
  • livekit-agents/livekit/agents/beta/tools/end_call.py
  • livekit-agents/livekit/agents/llm/tool_context.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/beta/tools/end_call.py
  • livekit-agents/livekit/agents/llm/tool_context.py
  • examples/voice_agents/session_close_callback.py
🧬 Code graph analysis (1)
livekit-agents/livekit/agents/llm/tool_context.py (1)
livekit-agents/livekit/agents/voice/events.py (1)
  • RunContext (32-81)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: livekit-plugins-deepgram
  • GitHub Check: livekit-plugins-openai
  • GitHub Check: type-check (3.9)
  • GitHub Check: type-check (3.13)
  • GitHub Check: unit-tests
🔇 Additional comments (5)
livekit-agents/livekit/agents/llm/tool_context.py (2)

24-31: LGTM! Proper use of TYPE_CHECKING to avoid circular imports.

The forward reference pattern for RunContext is correctly implemented, preventing runtime circular dependencies while maintaining type safety for static analysis.


45-53: LGTM! Well-structured event dataclasses.

The nested ToolCalledEvent and ToolCompletedEvent dataclasses provide a clean interface for tool lifecycle hooks with appropriate type annotations.

livekit-agents/livekit/agents/beta/tools/end_call.py (2)

98-114: LGTM! Proper timeout and cleanup handling.

The _delayed_session_shutdown method correctly handles the timeout case, cleans up the event handler in the finally block, and ensures the session is always shut down.


116-129: LGTM! Clean session close handling.

The _on_session_close method properly orchestrates room deletion and job shutdown with the callback pattern.

examples/voice_agents/session_close_callback.py (1)

36-47: LGTM! Clean demonstration of on_tool_completed callback.

The RealtimeAgent class effectively demonstrates the new callback feature, using it to provide a custom goodbye message when the tool completes.

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

Comment on lines +73 to +75
self._shutdown_session_task = asyncio.create_task(
self._delayed_session_shutdown(ctx)
)
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

Fire-and-forget task may silently swallow exceptions.

The task created here is stored but never awaited or cancelled. If _delayed_session_shutdown raises an exception, it will be silently lost. Consider adding exception handling or using asyncio.TaskGroup if available.

🔧 Proposed fix - add exception logging
+            async def _shutdown_with_error_handling() -> None:
+                try:
+                    await self._delayed_session_shutdown(ctx)
+                except Exception:
+                    logger.exception("error during delayed session shutdown")
+
             else:
                 self._shutdown_session_task = asyncio.create_task(
-                    self._delayed_session_shutdown(ctx)
+                    _shutdown_with_error_handling()
                 )
🤖 Prompt for AI Agents
In `@livekit-agents/livekit/agents/beta/tools/end_call.py` around lines 73 - 75,
The fire-and-forget task created and stored in self._shutdown_session_task for
_delayed_session_shutdown(ctx) can swallow exceptions; update the creation to
attach exception handling (e.g., use task.add_done_callback to log exceptions
from task.exception()) or run it inside an asyncio.TaskGroup/await it during
shutdown. Locate the code that sets self._shutdown_session_task and either wrap
the coroutine in a try/except and schedule that wrapper, add a done-callback
that logs task.exception() (referencing _delayed_session_shutdown and
self._shutdown_session_task), or migrate to asyncio.TaskGroup to ensure
exceptions are surfaced and the task is cancelled/awaited on cleanup.

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: 1

🤖 Fix all issues with AI agents
In `@livekit-agents/livekit/agents/beta/tools/end_call.py`:
- Line 65: Replace the private call to current_agent._get_activity_or_raise()
with the public AgentSession property: use ctx.session.llm instead of
ctx.session.current_agent._get_activity_or_raise().llm; update the reference in
end_call.py (the line assigning llm_v) to read from ctx.session.llm so you rely
on the public LLM resolution API and avoid coupling to the private
_get_activity_or_raise() method.
♻️ Duplicate comments (1)
livekit-agents/livekit/agents/beta/tools/end_call.py (1)

76-78: Fire-and-forget task may silently swallow exceptions.

The task is stored but never awaited or given a done-callback, so any exception raised in _delayed_session_shutdown will be silently lost. Consider adding exception handling.

Proposed fix - add exception logging
             else:
+                async def _shutdown_with_logging() -> None:
+                    try:
+                        await self._delayed_session_shutdown(ctx)
+                    except Exception:
+                        logger.exception("error during delayed session shutdown")
+
                 self._shutdown_session_task = asyncio.create_task(
-                    self._delayed_session_shutdown(ctx)
+                    _shutdown_with_logging()
                 )
🧹 Nitpick comments (1)
livekit-agents/livekit/agents/beta/tools/end_call.py (1)

41-46: Docstring line exceeds 100-character limit.

Line 43 is ~147 characters. Per coding guidelines, wrap it to stay within 100 characters.

Suggested fix
         Args:
             extra_description: Additional description to add to the end call tool.
-            delete_room: Whether to delete the room when the user ends the call. deleting the room disconnects all remote users, including SIP callers.
+            delete_room: Whether to delete the room when the user ends the call. Deleting the
+                room disconnects all remote users, including SIP callers.
             end_instructions: Instructions to generate a reply when the user ends the call if provided.
             on_tool_called: Callback to call when the tool is called.
             on_tool_completed: Callback to call when the tool is completed.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 884be1d and c7f7a32.

📒 Files selected for processing (1)
  • livekit-agents/livekit/agents/beta/tools/end_call.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/beta/tools/end_call.py
🧬 Code graph analysis (1)
livekit-agents/livekit/agents/beta/tools/end_call.py (5)
livekit-agents/livekit/agents/job.py (1)
  • get_job_context (56-63)
livekit-agents/livekit/agents/llm/tool_context.py (11)
  • Tool (34-35)
  • Toolset (44-59)
  • function_tool (189-194)
  • function_tool (198-203)
  • function_tool (207-213)
  • function_tool (217-223)
  • function_tool (226-268)
  • ToolCalledEvent (46-48)
  • ToolCompletedEvent (51-53)
  • info (155-156)
  • tools (57-59)
livekit-agents/livekit/agents/voice/events.py (3)
  • CloseEvent (221-225)
  • RunContext (32-81)
  • speech_handle (52-53)
livekit-agents/livekit/agents/voice/speech_handle.py (2)
  • SpeechHandle (16-251)
  • add_done_callback (163-164)
livekit-agents/livekit/agents/voice/agent.py (1)
  • _get_activity_or_raise (346-351)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: unit-tests
🔇 Additional comments (5)
livekit-agents/livekit/agents/beta/tools/end_call.py (5)

1-9: LGTM!

Imports are well-organized and all appear to be used in the implementation.


11-25: LGTM!

The description provides clear, structured guidance for the LLM on when to invoke the end call tool.


118-131: LGTM!

The session close handler correctly manages job context lifecycle—conditionally scheduling room deletion and properly propagating the close reason.


133-135: LGTM!

The tools property correctly exposes the function tool as required by the Toolset interface.


100-116: Code is correct as written.

The delayed shutdown logic is well-structured with proper cleanup in the finally block. SpeechHandle implements __await__, making line 111's await speech_handle valid and correct.

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

Copy link
Member

@chenghao-mou chenghao-mou left a comment

Choose a reason for hiding this comment

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

I think there might be a race condition when using a realtime model:

    12:07:43.460 DEBUG  livekit.agents     executing tool {"function": "end_call", "arguments": "{}", "speech_id": "speech_253993d3812c"}
    12:07:43.462 DEBUG  livekit.agents     end_call tool called  
    12:07:43.463 DEBUG  livekit.agents     tools execution completed {"speech_id": "speech_253993d3812c"}
    12:07:43.652 DEBUG  livekit.…ns.google send task finished.  
Agent Session closed, reason: CloseReason.USER_INITIATED
====================
Chat History:
agent_handoff: None -> my_agent
assistant: Hello! How can I help you today?
function_call: end_call, arguments: {}
user: Hey, I'm just testing the to and call two. Oh, so I'm just going to say goodbye.
end_call: 'thanks the user for calling and tell them goodbye'
assistant: Let me check
====================

I didn't hear anything about the last message when using Google Realtime model (1/3 tests)

and if I tell the realtime model to "say goodbye and call the end call tool", it will get stuck:

    12:22:31.971 DEBUG  livekit.agents     executing tool {"function": "end_call", "arguments": "{}", "speech_id": "speech_a6eed8cff32f"}
    12:22:31.975 DEBUG  livekit.agents     end_call tool called  
    12:22:31.977 DEBUG  livekit.agents     tools execution completed {"speech_id": "speech_a6eed8cff32f"}
    12:22:36.980 WARNI… livekit.agents     tool reply timed out, shutting down session  

ctx.session.off("speech_created", _on_speech_created)
ctx.session.shutdown()

def _on_session_close(self, ev: CloseEvent) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

maybe we can clean up the _shutdown_session_task here?

This is the final action the agent can take.
Once called, no further interaction is possible with the user.
Don't generate any other text or response when the tool is called.
Copy link
Member

Choose a reason for hiding this comment

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

This line might not be needed if we allow tool reply?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is to prevent a text message alongside the tool call, the tool response shouldn't be affected.

@longcw
Copy link
Contributor Author

longcw commented Jan 22, 2026

@chenghao-mou

I think there might be a race condition when using a realtime model

I saw the same issue sometimes when using gemini realtime model, the tool reply is > assistant: Let me check without audio. It seems to be a bug of the model. I checked the message received from the server, it indeed only replied this text message then ended the turn. when you talk more before end, it can generate tool reply properly.

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.

4 participants