Description
What happens
HandoffAgentExecutor broadcasts only the cleaned agent response to the other agents and then sends the handoff target a request with no messages:
# _handoff.py:386
cleaned_response = clean_conversation_for_handoff(response.messages)
...
# _handoff.py:395
await self._broadcast_messages(cleaned_response, ctx)
...
# _handoff.py:410-413
await ctx.send_message(
AgentExecutorRequest(messages=[], should_respond=True),
target_id=handoff_target,
)
clean_conversation_for_handoff drops every message that has no text content (_orchestrator_helpers.py:41-42). So a response consisting only of tool-control content — function calls, function results, approval payloads — cleans to an empty list.
When that happens the broadcast carries nothing, the handoff target's AgentExecutor cache is left empty (it is cleared after each run, _handoff.py:377), and the target is then invoked with no messages at all.
This is especially reachable in the handoff pattern because the handoff itself is a function call: an agent that hands off without also emitting text produces exactly the tool-only response that cleans away to nothing.
Why it matters
AgentExecutor tolerates an empty cache and only logs a warning, so most chat agents survive. Agents that require input do not — A2AAgent.run raises:
ValueError: At least one message is required when starting a new task (no continuation_token).
which aborts the whole workflow.
Relationship to #7456 / #7549
This is the same root cause as #7456, but in a different orchestrator. PR #7549 fixes the group-chat orchestrators (GroupChatOrchestrator and AgentBasedGroupChatOrchestrator) by supplying a continuation instruction when the selected speaker would otherwise receive no messages. The handoff path was deliberately left out of that PR to keep it scoped to the linked issue.
Suggested fix
Mirror the group-chat fix: when cleaned_response is empty, include an instruction message in the AgentExecutorRequest sent to the handoff target instead of messages=[], so the target is never invoked with empty input. _handoff.py already has a precedent for this shape in _autonomous_mode_prompt (_handoff.py:432), which injects a nudge message for the same class of reason.
Steps to reproduce
- Build a handoff workflow where the handing-off agent responds with only a handoff function call and no text content.
- Make the handoff target an
A2AAgent (or any agent that rejects empty input).
- Run the workflow and observe the
ValueError when the target is invoked.
Description
What happens
HandoffAgentExecutorbroadcasts only the cleaned agent response to the other agents and then sends the handoff target a request with no messages:clean_conversation_for_handoffdrops every message that has no text content (_orchestrator_helpers.py:41-42). So a response consisting only of tool-control content — function calls, function results, approval payloads — cleans to an empty list.When that happens the broadcast carries nothing, the handoff target's
AgentExecutorcache is left empty (it is cleared after each run,_handoff.py:377), and the target is then invoked with no messages at all.This is especially reachable in the handoff pattern because the handoff itself is a function call: an agent that hands off without also emitting text produces exactly the tool-only response that cleans away to nothing.
Why it matters
AgentExecutortolerates an empty cache and only logs a warning, so most chat agents survive. Agents that require input do not —A2AAgent.runraises:which aborts the whole workflow.
Relationship to #7456 / #7549
This is the same root cause as #7456, but in a different orchestrator. PR #7549 fixes the group-chat orchestrators (
GroupChatOrchestratorandAgentBasedGroupChatOrchestrator) by supplying a continuation instruction when the selected speaker would otherwise receive no messages. The handoff path was deliberately left out of that PR to keep it scoped to the linked issue.Suggested fix
Mirror the group-chat fix: when
cleaned_responseis empty, include an instruction message in theAgentExecutorRequestsent to the handoff target instead ofmessages=[], so the target is never invoked with empty input._handoff.pyalready has a precedent for this shape in_autonomous_mode_prompt(_handoff.py:432), which injects a nudge message for the same class of reason.Steps to reproduce
A2AAgent(or any agent that rejects empty input).ValueErrorwhen the target is invoked.