Repository navigation
Conversation
_on_done() stringified the raw task result, so the documented `return (agent, ToolResult(...))` handoff surfaced a Python repr (including the Agent's memory address) in the public tool_execution_updated event, while dispatch recorded the sanitized text in chat history. Derive the terminal message through the same make_tool_output sanitizer used by dispatch, so container results report their recorded output instead of a repr.
| out = run_ctx._make_update_output(output) | ||
| status, message = "done", out.fnc_call_out.output or None |
There was a problem hiding this comment.
🟡 Wrapped tuple results change their message
For ToolResult(("a", "b")), _on_done emits ['a', 'b'] instead of dispatch's ('a', 'b'). The earlier ToolResult unwrap lets make_tool_output treat the tuple as an unwrapped container.
Learn more
Dispatch passes the intact tool return to make_tool_output, which unwraps ToolResult only after its container-handling branch. The terminal callback already unwraps ToolResult before this new sanitizer call. Thus a tuple inside ToolResult becomes a list in the terminal event but stays a tuple in dispatch; a wrapped container with an Agent is also interpreted as a handoff by the terminal callback but rejected as invalid by dispatch.
Example: Returning ToolResult(("a", "b")) records ("a", "b") in the function-call output but emits ['a', 'b'] as the terminal message. Returning ToolResult([agent, "Transferred"]) records an invalid-output error while the terminal event says Transferred.
Recommended fix: Pass the intact tool return to run_ctx._make_update_output for terminal text while retaining the existing handling of cancellation, exceptions, bare Agent, and None. Test wrapped tuples and wrapped containers containing Agents against dispatch output.
Was this helpful? React with 👍 or 👎 to provide feedback.
fix(tools): sanitize the ToolCallEnded message for handoff results
What breaks
For the documented handoff form
return (agent, ToolResult(...)), the publicToolCallEnded.messageevent exposed a raw Python repr — including the non-deterministicAgentmemory address:_ToolExecutor._on_done()(livekit-agents/livekit/agents/voice/tool_executor.py) unwrapped only a bareToolResultand mapped a bareAgent/None/StopResponsetomessage=None; every other result — handoff tuples included — fell through tostr(output).Dispatch sanitizes the same result through
generation.make_tool_output()(output.agent_task+fnc_call_out.output), so chat history andFunctionToolsExecutedEventcorrectly recorded"Transferred"while the publictool_execution_updatedevent — forwarded verbatim to remote clients — carried the repr and the address. Two views of the same call disagreed, and the address changed every run.Fix
Derive the success-path message through the same sanitizer dispatch uses (
run_ctx._make_update_output(output), i.e.make_tool_output), takingfnc_call_out.output(orNone). The error, cancellation,StopResponse, bare-AgentandNonebranches are untouched.Behavior
Every success result shape now yields the same message as dispatch/chat history:
str/int/dictoutputs, bareToolResult,(agent, ToolResult(...)),(agent, str), list handoffs, tuples/sets,Agentobjects, and invalid/multi-Agent outputs. Empty-string output maps toNone(consistent with the event docs andremote_session.py).Tests
New test in
tests/test_tools.pyreturns(Agent(...), ToolResult("Transferred", reply_required=False))through a real_ToolExecutorand assertsToolCallEnded(message="Transferred", status="done"), with parity againstmake_tool_output(...).fnc_call_out.output.tests/test_tools.py142 passed, plus the tool-output/lifecycle/session-host neighbors (72) — 227 passed total across the suite set, no deviations at base apart from the one new test. Ruff clean.