Skip to content

fix(flow): stop replaying previous turn's intent when route_turn() returns falsy#6505

Open
joaomdmoura wants to merge 1 commit into
mainfrom
joao/epd-176-route_turn-falsy-return-silently-reuses-the-previous-turns
Open

fix(flow): stop replaying previous turn's intent when route_turn() returns falsy#6505
joaomdmoura wants to merge 1 commit into
mainfrom
joao/epd-176-route_turn-falsy-return-silently-reuses-the-previous-turns

Conversation

@joaomdmoura

@joaomdmoura joaomdmoura commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes EPD-176: in the experimental Conversational Flow, an overridden route_turn() returning a falsy value (e.g. None for an unhandled input) silently fell back to the sticky state.last_intent from the previous turn, re-running the prior turn's handler with no error, warning, or event — invisible misrouting, hit by an enterprise customer in production migration.

Root cause

The last_intent fallback in route_conversation() exists to serve the legacy default_intents path, where receive_user_message() classifies an intent fresh at the start of every turn and stashes it in state.last_intent. The field couldn't distinguish "classified this turn" from "left over from a previous turn", so custom route_turn() overrides returning falsy picked up stale routing state. A side effect: the can_answer_from_history check was dead after the first turn, since the sticky intent always won.

Fix

  • Track the freshly classified intent in a per-turn instance attribute (_turn_classified_intent), set by receive_user_message() and cleared on every turn reset.
  • route_conversation() now routes on that per-turn value; a falsy route_turn() with no fresh classification falls through to the built-in answer_from_history / converse defaults and never replays a previous turn's intent. A debug log notes when a stale intent is ignored.
  • Documented the falsy-return contract in route_turn()'s docstring.

Testing

  • The customer's clean-room repro from EPD-176 now routes turn 2 to converse instead of re-running the previous turn's handler.
  • New regression tests in TestFalsyRouteTurnFallback: the repro scenario, ConversationRouteSelectedEvent emission on fallback (with previous_intent still reported), and the legacy default_intents multi-turn path routing on fresh per-turn classification, including intent changes between turns.
  • test_flow_conversation.py (45 passed) plus the broader flow suite (357 passed), ruff, and mypy are green.

🤖 Generated with Claude Code


Note

Medium Risk
Changes conversational turn routing in experimental flows—a behavior bugfix with targeted tests, but incorrect routing logic could still affect production chat flows using custom route_turn() overrides.

Overview
Fixes silent misrouting in experimental conversational flows when a custom route_turn() returns a falsy value (e.g. None for unhandled input). Previously route_conversation() fell back to sticky state.last_intent from the prior turn, re-running the wrong @listen handler with no warning.

Routing now uses a per-turn _turn_classified_intent (set when receive_user_message() classifies via legacy default_intents, cleared on each turn reset). After a truthy route_turn() decision, only that turn’s classified intent is reused; otherwise the flow falls through to answer_from_history / converse and logs when a stale last_intent is ignored. The falsy route_turn() contract is documented on route_turn().

Adds TestFalsyRouteTurnFallback regression tests (EPD-176 repro, route events on fallback, legacy multi-turn classification).

Reviewed by Cursor Bugbot for commit 0094863. Bugbot is set up for automated code reviews on this repo. Configure here.

…turns falsy

In conversational flows, a falsy return from an overridden route_turn()
fell back to the sticky state.last_intent from a previous turn, silently
re-running the prior turn's handler for an unhandled input.

The fallback exists for the legacy default_intents path, where
receive_user_message() classifies the intent fresh each turn. Track that
per-turn classification in _turn_classified_intent (cleared on every turn
reset) and route on it instead, so a falsy route_turn() now falls through
to the built-in answer_from_history/converse defaults and never reuses
stale routing state.

Fixes EPD-176.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@linear

linear Bot commented Jul 10, 2026

Copy link
Copy Markdown

EPD-176

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b81a0c8-af91-478e-945b-eacb9d1b47e8

📥 Commits

Reviewing files that changed from the base of the PR and between 7baf8f9 and 0094863.

📒 Files selected for processing (2)
  • lib/crewai/src/crewai/experimental/conversational_mixin.py
  • lib/crewai/tests/test_flow_conversation.py

📝 Walkthrough

Walkthrough

Conversational routing now tracks intent classification per turn, clears it between turns, and uses it instead of persisted intent for route selection. Regression tests cover falsy route results, fallback behavior, route events, and changing legacy intent classifications.

Changes

Conversational routing

Layer / File(s) Summary
Turn-scoped intent routing
lib/crewai/src/crewai/experimental/conversational_mixin.py
Adds per-turn intent storage, initializes and clears it across execution boundaries, uses it for route selection, and documents falsy routing behavior.
Falsy route fallback coverage
lib/crewai/tests/test_flow_conversation.py
Verifies that stale intents are not replayed, fallback events preserve prior intent for visibility, and legacy routing follows newly classified intents.

Suggested reviewers: vinibrsl, greysonlalonde

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main fix: preventing previous-turn intent replay when route_turn() returns falsy.
Description check ✅ Passed The description is directly about the same conversational routing bug and its fix, so it is clearly on-topic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch joao/epd-176-route_turn-falsy-return-silently-reuses-the-previous-turns

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant