Skip to content

fix(tools): sanitize the ToolCallEnded message for handoff results - #7925

Open
BlueX888 wants to merge 1 commit into
livekit:mainfrom
BlueX888:fix/tool-call-ended-message-repr
Open

BlueX888 wants to merge 1 commit into
livekit:mainfrom
BlueX888:fix/tool-call-ended-message-repr

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

fix(tools): sanitize the ToolCallEnded message for handoff results

What breaks

For the documented handoff form return (agent, ToolResult(...)), the public ToolCallEnded.message event exposed a raw Python repr — including the non-deterministic Agent memory address:

message="(<livekit.agents.voice.agent.Agent object at 0x000001EA71398B60>, ToolResult(output='Transferred', reply_required=False))"

_ToolExecutor._on_done() (livekit-agents/livekit/agents/voice/tool_executor.py) unwrapped only a bare ToolResult and mapped a bare Agent/None/StopResponse to message=None; every other result — handoff tuples included — fell through to str(output).

Dispatch sanitizes the same result through generation.make_tool_output() (output.agent_task + fnc_call_out.output), so chat history and FunctionToolsExecutedEvent correctly recorded "Transferred" while the public tool_execution_updated event — 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), taking fnc_call_out.output (or None). The error, cancellation, StopResponse, bare-Agent and None branches are untouched.

Behavior

Every success result shape now yields the same message as dispatch/chat history: str/int/dict outputs, bare ToolResult, (agent, ToolResult(...)), (agent, str), list handoffs, tuples/sets, Agent objects, and invalid/multi-Agent outputs. Empty-string output maps to None (consistent with the event docs and remote_session.py).

Tests

New test in tests/test_tools.py returns (Agent(...), ToolResult("Transferred", reply_required=False)) through a real _ToolExecutor and asserts ToolCallEnded(message="Transferred", status="done"), with parity against make_tool_output(...).fnc_call_out.output.

  • On base: fails with the exact repr leak above.
  • With the fix: tests/test_tools.py 142 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.

_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.
@BlueX888
BlueX888 requested a review from a team as a code owner October 18, 2026 14:19

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

Devin Review

Comment on lines +462 to +463
out = run_ctx._make_update_output(output)
status, message = "done", out.fnc_call_out.output or None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

This branch has not been deployed

No deployments
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.

1 participant