Repository navigation
Description
Description
When using the Handoff Builder for specialist-to-specialist handoffs, I observed an unexpected control flow behavior when an agent returns a finish_reason: "stop".
Ideally, even when an agent (let’s call it agent_a) finishes with "stop", the control should remain with the same agent (agent_a) unless an explicit handoff is performed.
However, based on telemetry traces, this does not appear to be the case. The control is incorrectly shifting, resulting in multiple “dropped target mismatch” messages in the pipeline.
Observed Behavior
From the telemetry logs:
agent_areturns withfinish_reason: "stop".- Despite the
"stop"reason (and no explicit handoff), the Handoff Coordinator sends 4 messages toagent_a, all of which are marked as “dropped target mismatch”. - The Handoff Coordinator then sends a message to
handoff-user-input, which again results in a dropped target mismatch when checked against the edge group. - The control effectively leaves
agent_a, even though it should have remained with it.
This suggests that the coordinator is rerouting messages prematurely or that the "stop" state is being treated as a terminal state rather than a “pause-and-wait” for the same agent.
Expected Behavior
-
When an agent returns
finish_reason: "stop", control should remain with the same agent (agent_a) unless:- An explicit handoff occurs, or
- The conversation/session ends intentionally.
-
This behavior is consistent with AutoGen, where even if an agent returns
"stop", it continues to own the turn unless explicitly delegated.
Steps to Reproduce
-
Create a handoff builder with specialist-to-specialist handoff configuration.
-
Run a conversation where
agent_areaches a"stop"state. -
Observe telemetry events:
handoff-coordinator→agent_a→ dropped target mismatchhandoff-coordinator→handoff-user-input→ dropped target mismatch
-
Verify that control incorrectly transfers away from
agent_a.
Telemetry Evidence
- Four
"dropped target mismatch"messages observed fromhandoff-coordinator→agent_a. - One additional
"dropped target mismatch"message fromhandoff-coordinator→handoff-user-input. - No explicit handoff request logged between these events.
Questions / Clarifications
- Is this behavior (control leaving after
"stop") intentional in MAF’s handoff coordinator logic? - If yes, what’s the reasoning behind treating
"stop"as terminal for specialist agents? - If not, could this be an oversight in how the handoff coordinator interprets the agent’s stop event?
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
droideronline commentedon Nov 6, 2025
CC - Eric Zhu (@ekzhu) Evan Mattson (@moonbox3)
droideronline commentedon Nov 6, 2025
droideronline commentedon Nov 6, 2025
I think I’ve identified the root cause of this behavior.
In the
BaseChatClientimplementation of:the
tool_choiceparameter defaults to"auto".This value takes precedence during the
_merge_chat_options()call — effectively overriding any tool choice defined at the chat agent level during initialization.Because of that, even if a higher-level agent (e.g. in a workflow or builder) is explicitly configured with a
tool_choice="required", it gets replaced with"auto"once theget_responsemethod runs.Proposed Change
We should consider changing the default value of
tool_choicehere from"auto"→None, so that the value provided at the agent or chat options level can persist.That would make the function’s behavior more predictable and respect upstream configuration.
Additional Thoughts
While I think this change would make sense globally, there’s a nuance worth discussing:
tool_choice="auto"as the default is reasonable — it allows the model to decide when to call tools naturally."auto"can lead to inconsistent control behavior — since the model might terminate early (finish_reason="stop") instead of invoking a required tool call.So, we may need to think about handling this more explicitly in the handoff builder context — possibly by enforcing tool selection logic there, even if the base client keeps
"auto"as default.moonbox3 commentedon Nov 7, 2025
Thanks for filing, Dineshsuriya D (@droideronline). I will look into this.
droideronline commentedon Nov 8, 2025
I know this is intentional, but in the handoff builder, should the agent provide a response with a stop reason so the handoff tool can check and execute? Would it be better if we didn’t wait for the agent to finish and instead triggered the handoff tool as soon as it’s called from the service? I think we could still handle this through middleware.
Also, speaking of middleware, would it be nice if we could dynamically register middleware at runtime?
droideronline commentedon Nov 8, 2025
Currently, when tool_choice is set to "auto", the agent may produce a regular response mid-conversation because it can decide to stop on its own. This behavior disrupts the Handoff Builder’s expected flow.
On the other hand, when tool_choice is set to "required" — although this setting is currently not functioning correctly — the agent continuously issues tool calls and never produces a final agent response, causing the framework to miss registering an AgentResponse event.
Both of these cases need to be handled properly to ensure consistent control flow and predictable agent behavior during handoffs.
droideronline commentedon Nov 13, 2025
Hi Evan Mattson (@moonbox3) , I was testing the new
enable_return_to_previous()method onHandoffBuilder, and here’s the scenario. I have 5 agents. In the middle of the conversation one of the agents (the coder agent) returns astop_reasonofstop. Control then shifts back to user input, and after that I see a bunch ofhandoff-coordinatormessages trying to send things to the coder. Then at one pointexecutor.processsays it was delivered to the coder. But the coder responds withhandoff-user-input, and I start seeing a bunch of edge group runner messages fromhandoff-coordinatortohandoff-user-input. Eventually I end up with_UserInputGateway, as shown in the last screenshot.Are we supposed to provide any input to the agent here? My application is autonomous, so I won’t be giving it any inputs. Please check the behavior and let me know your thoughts.
droideronline commentedon Nov 14, 2025
Evan Mattson (@moonbox3) - I wasn't able to debug it, but let me know if you need anything from me. I can open a new issue if necessary.
moonbox3 commentedon Nov 14, 2025
Dineshsuriya D (@droideronline)
That's how this handoff is currently designed - if no handoff is triggered, it's expected to return control back to the user.
droideronline commentedon Nov 14, 2025
Makes sense, but in the case of specialist-to-specialist, is user input needed? Ideally, as per our design, it shouldn’t ask for user input when we use add_handoff(), if I’m not wrong.