proud-lawyer-39815
12/26/2025, 9:57 PMtall-airline-16756
12/26/2025, 9:58 PMtall-airline-16756
12/26/2025, 9:58 PMtall-airline-16756
12/26/2025, 9:58 PMfrom livekit.agents import function_tool, ToolError
@function_tool()
async def lookup_weather(location: str):
try:
result = await external_api_call(location)
return result
except Exception as e:
# Makes the failure unrecoverable and disables retries
raise ToolError(f"Failed to lookup weather: {e}")
This behavior is supported in both Python and Node.js implementations. If you want, tell me which language and framework you’re using and I’ll provide a concrete snippet for that environment.
Sources: Tool definition and use | LiveKit docsproud-lawyer-39815
12/26/2025, 10:11 PMtall-airline-16756
12/26/2025, 10:12 PM@function_tool. If your decorator is applied after @function_tool (or the tool wrapper bypasses your decorator), the agent never sees the raised ToolError.
• ToolError must be raised (not wrapped) so the agent runtime can detect it. Wrapping the exception inside a different exception type or passing it as a kwarg can be missed.
Suggestions to fix — pick one:
1) Apply your decorator inside the function tool itself (wrap the actual tool handler the agent registers):
from functools import wraps
from livekit.agents import function_tool, ToolError
def unrecoverable():
def _decorator(func):
@wraps(func)
async def _wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except Exception as e:
# Raise ToolError directly so the agent treats it as unrecoverable
raise ToolError(f"Unrecoverable: {e}") from e
return _wrapper
return _decorator
@function_tool()
@unrecoverable() # ensure this wraps the actual callable the agent registers
async def my_function_tool(...):
...
Make sure @unrecoverable() appears after or in the correct order so the function that @function_tool registers will raise ToolError.
2) Alternatively, raise ToolError inside the function body (most foolproof):
@function_tool()
async def my_function_tool(...):
try:
...
except Exception as e:
raise ToolError(f"Unrecoverable: {e}")
What I need from you if this still doesn’t work:
• The exact order of decorators on your function.
• Any agent logs or error messages showing retries.
• Which language/runtime (Python/Node) and the LiveKit agent version.
Share those and I’ll give the precise fix.proud-lawyer-39815
12/26/2025, 10:25 PMtall-airline-16756
12/26/2025, 10:25 PMToolError is raised.
Quick checklist and targeted questions to pinpoint the cause:
1) Confirm decorator/order
• Is your function decorated as @function_tool() then @unrecoverable() or the reverse? Paste the exact decorator order/definition for the function. The wrapper that raises ToolError must be the callable the agent registers.
2) Show the raised ToolError shape
• Paste the exact exception you see in logs (traceback and the exception message). Is it an instance of ToolError (not wrapped inside another exception)? Example we expect: raise ToolError("...") with ToolError as the top-level exception in the traceback.
3) Agent runtime behavior
• Are you running a custom agent loop or using a built-in session handler? If you have agent logs showing the agent receiving the tool failure and then continuing (including the agent's decision or reply), paste that section of the log.
4) Version + language
• Which language (Python or Node) and LiveKit agent version are you using?
What to try immediately
• Ensure the ToolError is raised from the function that @function_tool registers (apply @unrecoverable() below @function_tool() so the registered callable is the wrapped one), or raise ToolError directly inside the tool body.
• Confirm the traceback shows ToolError as the exception type at the top level (not nested).
Send the decorator order, a short code snippet of the actual decorated function, the traceback, and your agent version/language and I’ll pinpoint the exact fix.proud-lawyer-39815
12/26/2025, 10:34 PMtall-airline-16756
12/26/2025, 10:34 PMToolError still lets the agent “talk” or retry.
Short answer: ToolError is not an unrecoverable kill-switch. It returns an error to the LLM so the model can decide to retry, switch tools, or reply. To stop the agent immediately, you must terminate the session or signal the agent externally.
What you can do (pick one):
• Terminate the session immediately from the tool:
from livekit.agents import function_tool, ToolError, get_job_context
@function_tool()
async def my_tool(...):
try:
...
except Exception as e:
ctx = get_job_context()
await ctx.session.terminate() # stops the agent immediately
raise ToolError("Unrecoverable failure") # optional: record the failure
• Use a sentinel + external abort: return a special error value (or set a flag in shared context) and have your session controller check that value and stop processing outside the LLM loop.
Why ToolError alone doesn’t stop the agent:
• ToolError is explicitly designed to send an error back to the LLM so it can handle or recover. The LLM can re-call the tool or respond instead of halting.
If you want, send:
• Your minimal tool definition (exact decorator order),
• Which SDK (Python/Node) and agent version,
• A short log showing the agent’s subsequent message,
and I’ll give a precise code example for terminating the run in your environment.
Sources: Tool definition and use | LiveKit docsproud-lawyer-39815
12/26/2025, 10:35 PMtall-airline-16756
12/26/2025, 10:36 PMsession.on.
Do this two ways together:
1) Fail silently inside the tool
• Raise ToolError (and do NOT call session.say() or generate_reply()).
• Optionally set a sentinel in session state so your event handler can find the error.
Example:
from livekit.agents import function_tool, ToolError, get_job_context
@function_tool()
async def my_tool(self, context, arg):
try:
return await external_api_call(arg)
except Exception as e:
# store error info for event handler
ctx = get_job_context()
ctx.session.userdata["last_tool_error"] = str(e)
# raise ToolError so the agent does not speak
raise ToolError(f"Tool failed: {e}")
2) Capture the failure in a session event and handle it (remain silent)
• Listen for conversation / item events and detect failed function calls or check the sentinel in session.userdata. Do not emit any reply from the handler if you want silence; perform your error handling there.
Example:
@session.on("conversation_item_added")
async def on_conversation_item_added(item):
# detect a failed function call entry or check userdata
if item.type == "function_call" and item.status == "failed":
error_msg = item.output
# handle error (log, metrics, external notify), but do NOT call session.say()
handle_error(error_msg)
# or
if session.userdata.get("last_tool_error"):
handle_error(session.userdata.pop("last_tool_error"))
Notes and important behavior
• ToolError does not forcibly "kill" the agent or prevent the LLM from trying again; it simply returns an error to the LLM. Silence is achieved by ensuring you (and your code paths) do not call any speech/generate methods after the error.
• To forcibly stop the session immediately, call await ctx.session.terminate() inside the tool before/after raising ToolError — but terminating will end the session rather than just emit an event.
If you want, tell me:
• Your SDK (Python/Node) and agent version, and
• Whether you prefer keeping the session alive (silent + event) or terminating it,
and I’ll provide a minimal runnable example tailored to that environment.
Sources: Tool definition and use | LiveKit docsproud-lawyer-39815
12/26/2025, 10:38 PMtall-airline-16756
12/26/2025, 10:38 PMRunContext has no terminate and you want silence + an error event.
Short answers:
• RunContext does not have terminate.
• Do NOT rely on ToolError alone to stop the LLM from responding; ToolError sends an error back to the LLM (which may try to recover).
• To keep the agent silent and surface the failure to your session handlers, raise ToolError from the tool and emit/record the failure so your session.on handler can act. Use session.shutdown() from session-level APIs (not RunContext) if you must end the session programmatically.
Concrete pattern (Python):
1) In the tool: raise ToolError and store failure info in session userdata
from livekit.agents import function_tool, ToolError, get_job_context
@function_tool()
async def my_tool(self, context, ...):
try:
return await external_api_call(...)
except Exception as e:
# Save error for session handlers; do NOT call session.say()
ctx = get_job_context()
ctx.session.userdata["last_tool_error"] = str(e)
raise ToolError(f"Tool failed: {e}")
2) In your session event handler: detect the failure and handle it (no speech)
@session.on("conversation_item_added")
async def on_conversation_item_added(item):
if item.type == "function_call" and item.status == "failed":
# silent handling
handle_error(item.output)
elif session.userdata.get("last_tool_error"):
handle_error(session.userdata.pop("last_tool_error"))
3) If you need to stop the session completely (not just remain silent), call await session.shutdown() from a session-level handler (not from RunContext). Example:
@session.on("error")
async def on_error(err):
await session.shutdown()
If you want, tell me:
• Python or Node.js,
• Agent SDK version,
and I’ll provide the exact minimal runnable snippet wired to your session lifecycle.
Sources: Tool definition and use | LiveKit docs | Job lifecycle | LiveKit docsproud-lawyer-39815
12/26/2025, 10:40 PMtall-airline-16756
12/26/2025, 10:41 PMerror event on the AgentSession with an ErrorEvent (or compatible payload) from inside the tool — ToolError alone will not trigger the session-level error handler.
Quick pattern (Python):
1) In the tool: catch the failure, optionally record context, then emit the session error and raise ToolError for LLM visibility (or just emit if you don't want any LLM-visible error).
from livekit.agents import function_tool, ToolError, ErrorEvent, get_job_context
@function_tool()
async def my_tool(self, context, arg):
try:
return await external_api_call(arg)
except Exception as e:
ctx = get_job_context() # get RunContext
# store details for handlers if useful
ctx.session.userdata["last_tool_error"] = str(e)
# emit session-level error so @session.on("error") fires
await ctx.session.emit("error", ErrorEvent(error=e))
# optionally still raise ToolError so the LLM sees an error
raise ToolError("Critical failure")
2) In the session handler: implement your error listener to receive ErrorEvent
@self._agent_session.on("error")
async def on_error(event: ErrorEvent):
# event.error contains the original exception or message
handle_error(event.error) # logging, metrics, cleanup, etc.
# remain silent: do NOT call session.say() or generate_reply()
Notes
• Use session.emit("error", ErrorEvent(...)) (or session.event_emitter.emit(...) if your SDK version requires it) — prefer emitting on the session instance.
• Emitting is asynchronous; await session.emit(...) when called inside async tools.
• If you want the agent completely stopped after emitting, call await session.shutdown() from the session-level handler (not RunContext).
• Keep tool code from calling session.say() or generate_reply() to ensure silence.
If you share your SDK (Python/Node) and version and a minimal tool snippet, I’ll give a runnable example tailored to your environment.
Sources: Tool definition and use | LiveKit docs