Skip to content

fix(mcp): treat a Human Input Card reply as the current user request - #5426

Merged
WillemJiang merged 4 commits into
bytedance:mainfrom
BlueX888:fix/hitl-reply-mcp-routing
Sep 14, 2026
Merged

WillemJiang merged 4 commits into
bytedance:mainfrom
BlueX888:fix/hitl-reply-mcp-routing

Conversation

@BlueX888

Copy link
Copy Markdown
Contributor

Fixes #5425

Why

When the agent asks a clarifying question through a Human Input Card, the user's answer arrives as a hidden HumanMessage (hide_from_ui: true) carrying a valid human_input_response payload. That reply is the user's current request, but McpRoutingMiddleware._latest_user_message selected the latest user message with is_real_user_message, which rejects any hide_from_ui message with no carve-out for a valid human-input reply. When the routing keyword appeared only in the answer, the middleware walked past the card reply and matched nothing, so the deferred MCP tool was never auto-promoted and the model had to call tool_search by hand. The identical answer sent as a visible message promoted the tool, so the behaviour depended only on how the answer was transported.

is_genuine_user_message already encodes the correct rule (skip hidden messages unless they carry a valid human_input_response), and summarization_middleware.py was switched to it for exactly this reason in #5416.

What changed

  • McpRoutingMiddleware._latest_user_message now uses is_genuine_user_message instead of is_real_user_message, so a Human Input Card reply counts as the current user request for MCP routing. The import moves from deerflow.utils.messages to deerflow.agents.middlewares.message_utils.
  • A regression test covers a hidden card reply whose text alone names the datasource: it auto-promotes the matching tool.

No change to visible-message behaviour, tool filtering, or the routing index. tests/test_mcp_routing_auto_promote.py::test_matching_uses_latest_real_human_message_only still passes — it covers a hidden message carrying no human_input_response, which stays skipped.

Surface area

  • Frontend UI
  • Backend API
  • Agents / LangGraph — middleware turn-detection predicate
  • Sandbox
  • Skills
  • Dependencies
  • Default behavior change — a hidden Human Input Card reply now drives MCP routing auto-promotion
  • Docs / tests / CI only

Bug fix verification

  • Test path that reproduces the bug: backend/tests/test_mcp_routing_auto_promote.py::test_matching_uses_a_hidden_human_input_card_reply_as_the_latest_message
  • Did it go red on main and green on this branch? yes — 1 failed on the unfixed tree (AssertionError at the assert), 2 passed for the new test plus the existing skip test after the fix.

Validation

cd backend && PYTHONPATH=. .venv/bin/python -m pytest tests/test_mcp_routing_auto_promote.py -q
19 passed

cd backend && PYTHONPATH=. .venv/bin/python -m pytest tests/test_mcp_routing_auto_promote.py tests/test_input_sanitization_middleware.py -q
191 passed

ruff check  + ruff format --check on both changed files: All checks passed / 2 files already formatted

AI assistance

Tool(s) used: Claude Code

How you used it: used it to trace the two turn-detection predicates and draft the regression test; I reviewed the reasoning against summarization_middleware.py's #5416 change and the existing skip test, and take responsibility for the diff.

  • I've read and understand every line of this change and take responsibility for it — it's not unreviewed AI output.

McpRoutingMiddleware picked the latest user message with
is_real_user_message, which rejects every hide_from_ui message. A Human
Input Card reply is hidden but is still the user's current request, so
when the routing keyword lived only in the clarification answer the
deferred MCP tool was never auto-promoted and the model had to call
tool_search by hand.

Switch _latest_user_message to is_genuine_user_message, the same
predicate summarization_middleware already uses for this reason (bytedance#5416).

Fixes bytedance#5425
@github-actions github-actions Bot added area:agents Agents, subagents, graph wiring, prompts, langgraph.json needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines labels Sep 14, 2026

@willem-bd willem-bd 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.

Verified empirically: the regression test fails on main's source and passes at this head (19/19 in tests/test_mcp_routing_auto_promote.py), the predicate swap matches the rule summarization_middleware/tool_receipt_middleware/context_snapshot already use, the existing hidden-message skip test still holds (no human_input_response payload stays skipped per read_human_input_response validation), and ruff check/format are clean. One non-blocking consistency note below.

"""
for message in reversed(messages):
if is_real_user_message(message):
if is_genuine_user_message(message):

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.

Non-blocking follow-up: with this swap, a hidden Human Input Card reply now drives MCP routing but still not slash-skill activation — skill_activation_middleware._is_user_activation_target (line 89) still uses is_real_user_message, so a card reply whose answer text starts with /skill-name ... will promote the MCP tool yet fail to activate the skill. Same transport-dependent behavior class this PR fixes. Also, is_real_user_message's docstring in deerflow/utils/messages.py still lists MCP routing as a consumer. Worth a tracking issue or a sibling change rather than expanding this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked both points.

Docstring — agreed, is_real_user_message's docstring still lists MCP routing as a consumer, and this swap makes that line stale. Leaving it out of this PR per your note.

Slash activation — I ran it before changing anything, and the predicate swap alone is a no-op on a card reply. The text the activation path parses is the transport wrapper, not the answer:

_is_user_activation_target(reply)                          -> False
is_genuine_user_message(reply)                             -> True
activation input for the card reply                        -> 'For your clarification "Which skill should I run?", my answer is: /release-notes summarize v2'
parse_slash_skill_reference(that input)                    -> None
parse_slash_skill_reference('/release-notes summarize v2') -> SlashSkillReference(name='release-notes', remaining_text='summarize v2')

_SLASH_SKILL_RE is ^-anchored (skills/slash.py:4), and get_original_user_content_text does not unwrap a card reply: original_user_content is only set by input_sanitization_middleware / uploads_middleware, so for a card reply it returns the wrapped body verbatim. The raw answer survives only in additional_kwargs["human_input_response"]["value"].

So a card reply cannot reach slash activation by the same route MCP routing uses — even after swapping _is_user_activation_target to is_genuine_user_message, the parsed text still does not begin with /. A real fix is the predicate swap plus reading the answer out of the payload, and whether a card answer should activate a skill at all is a behavior decision rather than a consistency fix. Not expanding this PR.

Happy to file a tracking issue with the repro above; shout if you'd rather have the sibling change and I'll take it separately.

@WillemJiang
WillemJiang merged commit ba998a9 into bytedance:main Sep 14, 2026
16 checks passed
@WillemJiang WillemJiang added this to the 2.1.0 milestone Sep 14, 2026
@BlueX888
BlueX888 deleted the fix/hitl-reply-mcp-routing branch September 14, 2026 11:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agents Agents, subagents, graph wiring, prompts, langgraph.json needs-validation Touches front/back contract surface; needs real-path validation risk:high High risk: backend API, agents, sandbox, auth, deps, CI size/S PR changes 20-100 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Human Input Card reply is ignored by MCP routing auto-promotion (is_real_user_message drops it)

3 participants