This is an automated email from the ASF dual-hosted git repository.

kaxil pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new fab25d9fd04 Report the Airflow version error first when HITL review 
needs 3.1+ (#73052)
fab25d9fd04 is described below

commit fab25d9fd0456c201d45ed910729b5a86bb84702
Author: Kaxil Naik <[email protected]>
AuthorDate: Sun Sep 13 13:55:58 2026 +0100

    Report the Airflow version error first when HITL review needs 3.1+ (#73052)
---
 .../airflow/providers/common/ai/operators/agent.py | 13 +++++---
 .../tests/unit/common/ai/operators/test_agent.py   | 39 ++++++++++++++++++++++
 2 files changed, 47 insertions(+), 5 deletions(-)

diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/operators/agent.py 
b/providers/common/ai/src/airflow/providers/common/ai/operators/agent.py
index dac5e5b2830..7a40493428e 100644
--- a/providers/common/ai/src/airflow/providers/common/ai/operators/agent.py
+++ b/providers/common/ai/src/airflow/providers/common/ai/operators/agent.py
@@ -285,6 +285,14 @@ class AgentOperator(BaseOperator, HITLReviewMixin):
         self._durable_storage: DurableStorageProtocol | None = None
         self._durable_counter: DurableStepCounter | None = None
 
+        # Checked ahead of the combination rules below. On a core older than 
3.1 the core
+        # version is the real blocker, and reporting a combination error first 
would send the
+        # user to drop an argument that was never the problem -- they would 
hit this anyway.
+        if enable_hitl_review and not AIRFLOW_V_3_1_PLUS:
+            raise AirflowOptionalProviderFeatureException(
+                "Human in the loop functionality needs Airflow 3.1+."
+            )
+
         if durable and enable_hitl_review:
             raise ValueError("durable=True and enable_hitl_review=True cannot 
be used together.")
 
@@ -309,11 +317,6 @@ class AgentOperator(BaseOperator, HITLReviewMixin):
         self.hitl_timeout = hitl_timeout
         self.hitl_poll_interval = hitl_poll_interval
 
-        if self.enable_hitl_review and not AIRFLOW_V_3_1_PLUS:
-            raise AirflowOptionalProviderFeatureException(
-                "Human in the loop functionality needs Airflow 3.1+."
-            )
-
     @cached_property
     def llm_hook(self) -> PydanticAIHook:
         """Return PydanticAIHook for the configured LLM connection."""
diff --git a/providers/common/ai/tests/unit/common/ai/operators/test_agent.py 
b/providers/common/ai/tests/unit/common/ai/operators/test_agent.py
index 41470724d60..72bc089ca8b 100644
--- a/providers/common/ai/tests/unit/common/ai/operators/test_agent.py
+++ b/providers/common/ai/tests/unit/common/ai/operators/test_agent.py
@@ -877,6 +877,9 @@ class TestAgentOperatorMessageHistory:
         assert kwargs["usage_limits"] is limits
         assert kwargs["message_history"] == []
 
+    @pytest.mark.skipif(
+        not AIRFLOW_V_3_1_PLUS, reason="Human in the loop is only compatible 
with Airflow >= 3.1.0"
+    )
     def test_message_history_with_hitl_review_raises(self):
         """message_history cannot be combined with HITL review (post-review 
transcript is lost)."""
         with pytest.raises(ValueError, match="message_history and 
enable_hitl_review"):
@@ -914,3 +917,39 @@ class TestAgentOperatorMessageHistory:
 
         passed = mock_agent.run_sync.call_args.kwargs["message_history"]
         assert len(passed) == 2
+
+
+class TestAgentOperatorHITLArgumentChecks:
+    """The order in which __init__ reports conflicting HITL arguments."""
+
+    @pytest.mark.skipif(
+        not AIRFLOW_V_3_1_PLUS, reason="Human in the loop is only compatible 
with Airflow >= 3.1.0"
+    )
+    def test_durable_with_hitl_review_raises(self):
+        """Durable replay cannot be combined with HITL review."""
+        with pytest.raises(ValueError, match="durable=True and 
enable_hitl_review"):
+            AgentOperator(task_id="t", prompt="run", llm_conn_id="c", 
durable=True, enable_hitl_review=True)
+
+    @pytest.mark.parametrize(
+        "conflicting_kwargs",
+        [
+            pytest.param({"message_history": []}, id="message_history"),
+            pytest.param({"durable": True}, id="durable"),
+        ],
+    )
+    @patch("airflow.providers.common.ai.operators.agent.AIRFLOW_V_3_1_PLUS", 
False)
+    def test_version_gate_reported_before_combination_errors(self, 
conflicting_kwargs):
+        """On a core older than 3.1 the core version is the blocker, so it is 
what is reported.
+
+        Dropping the conflicting argument would not make the operator work 
there, so reporting
+        the combination error first sends the user to the wrong knob. This 
ordering is also why
+        the combination tests above carry a 3.1 skipif: on an older core they 
raise this instead.
+        """
+        with pytest.raises(AirflowOptionalProviderFeatureException, 
match="Airflow 3.1"):
+            AgentOperator(
+                task_id="t",
+                prompt="run",
+                llm_conn_id="c",
+                enable_hitl_review=True,
+                **conflicting_kwargs,
+            )

Reply via email to