From b8381b9c735c1ebce22f53f8818b5d68eccc69b2 Mon Sep 17 00:00:00 2001 From: Joao Moura Date: Wed, 8 Jul 2026 12:38:40 -0700 Subject: [PATCH] fix(cli): run human-feedback declarative flows on the terminal, not the TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two review follow-ups, both rooted in @human_feedback methods: - Paused flow marked complete (Cursor): async human feedback makes kickoff RETURN a HumanFeedbackPending marker (not raise), which _run_flow_worker would stringify and report as a successful completion with exit 0. - Sync feedback breaks TUI (Cursor): default (sync) @human_feedback collects input via the flow runtime's Rich console.print + blocking input(), which interleaves with Textual and leaves the user unable to review output or submit feedback. run_declarative_flow now routes any flow whose declarative definition declares human feedback (_flow_uses_human_feedback) to the terminal path, where blocking input and Rich prompts work natively — regardless of interactivity. Non-feedback flows still get the TUI. Tests: test_flow_uses_human_feedback_detection, test_human_feedback_flow_uses_terminal_even_when_interactive. Fully interactive human feedback inside the TUI remains a separate follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01RBYGqJHC2TMC6fonFziuuh --- .../src/crewai_cli/run_declarative_flow.py | 21 +++++++++- lib/cli/tests/test_run_declarative_flow.py | 42 +++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/cli/src/crewai_cli/run_declarative_flow.py b/lib/cli/src/crewai_cli/run_declarative_flow.py index d263d9fa0..58f7ad307 100644 --- a/lib/cli/src/crewai_cli/run_declarative_flow.py +++ b/lib/cli/src/crewai_cli/run_declarative_flow.py @@ -69,7 +69,11 @@ def run_declarative_flow(definition: str | Path, inputs: str | None = None) -> N # The TUI is the interactive default. Headless contexts run directly on the # terminal: deploy/CREWAI_DMN, piped output, CI — anything without an # interactive TTY. is_interactive() already folds in the CREWAI_DMN check. - if is_interactive(): + # Human-feedback flows also run on the terminal: their methods collect input + # via the flow runtime's blocking input()/Rich prompts (and async feedback + # returns a pending marker rather than completing), neither of which the + # Textual TUI can handle correctly. + if is_interactive() and not _flow_uses_human_feedback(flow): _run_declarative_flow_tui(flow, resolved_inputs or None) return @@ -140,6 +144,21 @@ def _run_declarative_flow_tui(flow: Any, resolved_inputs: dict[str, Any] | None) return app._crew_result +def _flow_uses_human_feedback(flow: Any) -> bool: + """True if any declarative method declares ``@human_feedback``. + + Such flows need the flow runtime's interactive stdin / Rich prompts, which + don't compose with Textual — so they run on the terminal, not the TUI. + """ + try: + methods = getattr(getattr(flow, "_definition", None), "methods", None) or {} + return any( + getattr(m, "human_feedback", None) is not None for m in methods.values() + ) + except Exception: + return False + + def _flow_method_types(flow: Any) -> dict[str, str]: """Map each declarative method name to its ``call`` type (crew/agent/…). diff --git a/lib/cli/tests/test_run_declarative_flow.py b/lib/cli/tests/test_run_declarative_flow.py index 10167c820..378156725 100644 --- a/lib/cli/tests/test_run_declarative_flow.py +++ b/lib/cli/tests/test_run_declarative_flow.py @@ -553,6 +553,48 @@ def test_run_declarative_flow_tui_enables_flow_events( assert flow.suppress_flow_events is False +def test_flow_uses_human_feedback_detection() -> None: + hf_flow = SimpleNamespace( + _definition=SimpleNamespace( + methods={ + "ask": SimpleNamespace(human_feedback=SimpleNamespace(emit=None)), + "plain": SimpleNamespace(human_feedback=None), + } + ) + ) + assert run_declarative_flow_module._flow_uses_human_feedback(hf_flow) is True + + no_hf = SimpleNamespace( + _definition=SimpleNamespace( + methods={"a": SimpleNamespace(human_feedback=None)} + ) + ) + assert run_declarative_flow_module._flow_uses_human_feedback(no_hf) is False + # No definition → False, no error. + assert run_declarative_flow_module._flow_uses_human_feedback(SimpleNamespace()) is False + + +def test_human_feedback_flow_uses_terminal_even_when_interactive( + tmp_path: Path, capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch +) -> None: + # A human-feedback flow must run on the terminal (blocking input / Rich + # prompts) even in an interactive session, never on the TUI. + monkeypatch.setattr(run_declarative_flow_module, "is_interactive", lambda: True) + monkeypatch.setattr( + run_declarative_flow_module, "_flow_uses_human_feedback", lambda flow: True + ) + monkeypatch.setattr( + run_declarative_flow_module, + "_run_declarative_flow_tui", + lambda *a, **k: pytest.fail("human-feedback flow must run on the terminal"), + ) + path = _write(tmp_path, FLOW_YAML) + + run_declarative_flow_module.run_declarative_flow(str(path), '{"topic":"AI"}') + + assert capsys.readouterr().out == "AI\n" + + def test_flow_method_types_from_definition() -> None: flow = SimpleNamespace( _definition=SimpleNamespace(