how can I disable retries in function calls? I'd l...
# ask-ai
p
how can I disable retries in function calls? I'd like the agent to try once, and if it doesn't work, throw a unrecoverable event
t
Thanks for reaching out, Mario. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Hello! Our team is operating on reduced capacity over the holidays, so our responses may be delayed. If your issue is critical/urgent, please react to your original message above with the ‼️ emoji and your issue will be escalated with our team. Happy Holidays!
Thanks — I’m responding about how to disable retries so a function is tried only once and failures become unrecoverable. Use the agent’s tool error mechanism: raise a ToolError from your function tool when the call fails. Raising ToolError immediately surfaces an unrecoverable tool failure to the LLM and prevents any automatic retry. Example (Python):
Copy code
from 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 docs
p
I added this decorator to my function tools, and still it doesn't work def unrecoverable(): """ Decorator to mark a function tool as unrecoverable. If an exception is raised, it will be wrapped in a ToolError, which propagates an unrecoverable error to the agent session. Usage: @unrecoverable() async def my_function_tool(*args, **kwargs): pass """ def _decorator(func: Awaitable[Callable]) -> Awaitable[Callable]: @wraps(func) async def _wrapper(*args, **kwargs) -> Any: try: return await func(*args, **kwargs) except Exception as e: raise ToolError(error=e) from e return _wrapper return _decorator
t
Thanks — I’m responding about why your decorator isn’t preventing retries and how to fix it. Two likely issues: • The agent sees the original tool as the callable registered with
@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):
Copy code
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):
Copy code
@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.
p
I can see the toolerror is raised, but the agent still talks about it
t
Thanks — I’m responding about why you still see the agent talking after a
ToolError
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.
p
ToolError doesn't really throw an enreunrecoverable error. It seems like the agent still tries to understand whether to handle it or not
t
Thanks — I’m responding to your point that raising
ToolError
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:
Copy code
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 docs
p
If the tool fails, I want the agent to remain silent. Then, I want an erroe event to be triggered, so that I can handle it in session.on
t
Thanks — I’m responding to your request: make the agent stay silent when a tool fails, and surface an error event you can handle in
session.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:
Copy code
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:
Copy code
@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 docs
p
the is no terminate on the run context
t
Thanks — I’m responding about stopping the agent when
RunContext
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
Copy code
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)
Copy code
@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:
Copy code
@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 docs
p
how do I propagate an error event to @self._agent_session.on("error") def on_error(event: ErrorEvent):
t
You're right to want the tool to fail silently and have that failure surface to your session-level error handler. Do this by emitting an
error
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).
Copy code
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
Copy code
@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