narrow-chef-71655
12/30/2025, 5:16 AMclass FunctionToolsExecutedEvent(BaseModel):
type: Literal["function_tools_executed"] = "function_tools_executed"
function_calls: list[FunctionCall]
function_call_outputs: list[FunctionCallOutput | None]
created_at: float = Field(default_factory=time.time)
_reply_required: bool = PrivateAttr(default=False)
_handoff_required: bool = PrivateAttr(default=False)
def zipped(self) -> list[tuple[FunctionCall, FunctionCallOutput | None]]:
return list(zip(self.function_calls, self.function_call_outputs))
def cancel_tool_reply(self) -> None:
self._reply_required = False
def cancel_agent_handoff(self) -> None:
self._handoff_required = False
@property
def has_tool_reply(self) -> bool:
return self._reply_required
@property
def has_agent_handoff(self) -> bool:
return self._handoff_required
@model_validator(mode="after")
def verify_lists_length(self) -> Self:
if len(self.function_calls) != len(self.function_call_outputs):
raise ValueError("The number of function_calls and function_call_outputs must match.")
return self
can you explain what is cancel_tool_reply and _handoff_required and how it works ?tall-airline-16756
12/30/2025, 5:16 AMtall-airline-16756
12/30/2025, 5:16 AMcancel_tool_reply and the _handoff_required attribute on FunctionToolsExecutedEvent.
What cancel_tool_reply does
• cancel_tool_reply() sets the private flag _reply_required to False.
• That flag is exposed by the has_tool_reply property. If _reply_required is False, has_tool_reply will return False.
• Effect: calling cancel_tool_reply() tells the surrounding workflow that no LLM-generated reply is required after the tools run (useful when tool side-effects are sufficient and you don’t want a follow-up message).
What _handoff_required is and how it works
• _handoff_required is a private boolean flag that indicates whether an agent handoff should occur after tool execution.
• The has_agent_handoff property exposes that flag. If _handoff_required is True, has_agent_handoff returns True.
• cancel_agent_handoff() sets _handoff_required to False, cancelling a pending handoff.
• Effect: when _handoff_required is True the framework can perform a handoff (transfer control to another agent); setting it False prevents that transfer.
How these flags are used together
• After tools run, the workflow checks has_tool_reply and has_agent_handoff to decide next steps:
◦ If has_agent_handoff is True → perform handoff.
◦ Else if has_tool_reply is True → generate an LLM reply from tool outputs.
◦ If both are False → no automatic reply or handoff; wait for further input or other logic.
If you want, tell me whether you’re seeing unexpected behavior (e.g., a reply still being generated or a handoff occurring) and I’ll help you trace where those flags are being set.
Sources: Tool definition and use | LiveKit docsnarrow-chef-71655
12/30/2025, 5:21 AMtall-airline-16756
12/30/2025, 5:21 AM_reply_required or _handoff_required. They remain whatever the code set them to during or after the tool run.
• Typical behavior:
◦ If _handoff_required is True, the framework will proceed with the handoff even if a tool errored (unless cancel_agent_handoff() is called afterward).
◦ If _reply_required is True, the framework will attempt to generate an LLM reply that can include the error details (unless cancel_tool_reply() is called).
◦ If both flags are False, no automatic reply or handoff occurs regardless of the error; the system will wait for further input or other logic.
If you want to change behavior on error, you can:
• Clear or set these flags in your tool error handling (call cancel_tool_reply() or set _handoff_required as appropriate).
• Add conditional logic after tool execution to inspect errors and decide whether to request a reply or perform a handoff.
Do you want an example of where to set/clear the flags in error handling code?narrow-chef-71655
12/30/2025, 3:51 PMtall-airline-16756
12/30/2025, 3:51 PMmax_tool_steps, and tools can be interrupted, but per‑tool timeout config is not provided.
What to do instead (two common approaches)
• Python (async): wrap the tool call with asyncio.wait_for() to enforce a timeout for that specific tool.
• Node.js (async): use Promise.race() with a timeout promise to enforce a per‑tool timeout.
Quick examples
• Python:
import asyncio
async def run_tool_with_timeout(tool_coro, timeout_seconds):
return await asyncio.wait_for(tool_coro, timeout_seconds)
• Node.js:
function runToolWithTimeout(toolPromise, ms) {
const timeout = new Promise((_, rej) => setTimeout(() => rej(new Error('timeout')), ms));
return Promise.race([toolPromise, timeout]);
}
Follow-up question
• Are you using the Python or Node.js SDK, and do you want the timeout enforced on the agent side or the MCP server side? I can provide a concrete code example integrated into your tool-calling flow. Sources: Tool definition and use | LiveKit docs | Using MCP with LiveKit Agents | Agent session | LiveKit docs | Agent speech and audio | LiveKit docs