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,
+ )