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 1f4fe796933 Keep the pydantic-ai first-run banner out of Common AI
task logs (#74900)
1f4fe796933 is described below
commit 1f4fe7969334be3592db6509b36a38e4782dcffc
Author: Kaxil Naik <[email protected]>
AuthorDate: Sat Oct 17 13:36:39 2026 +0100
Keep the pydantic-ai first-run banner out of Common AI task logs (#74900)
---
providers/common/ai/docs/frameworks/pydantic_ai.rst | 6 ++++++
.../airflow/providers/common/ai/hooks/pydantic_ai.py | 5 +++++
.../ai/tests/unit/common/ai/hooks/test_pydantic_ai.py | 18 ++++++++++++++++++
3 files changed, 29 insertions(+)
diff --git a/providers/common/ai/docs/frameworks/pydantic_ai.rst
b/providers/common/ai/docs/frameworks/pydantic_ai.rst
index 0ae4bd8cf98..67e6f6b27c6 100644
--- a/providers/common/ai/docs/frameworks/pydantic_ai.rst
+++ b/providers/common/ai/docs/frameworks/pydantic_ai.rst
@@ -60,6 +60,12 @@ supplied the tool. To get that masking on an agent you build
with ``Agent(...)``
agent = Agent(model, toolsets=[my_toolset],
capabilities=[MaskingCapability()])
+Pydantic AI 2.43 and later print a first-run banner to stderr when Airflow was
started from
+a coding agent such as Claude Code, Cursor or Codex, and a task's stderr lands
in its log
+as ``ERROR`` lines. Importing ``PydanticAIHook``, as the example above does,
turns the
+banner off for the task process; for an agent run without the hook, set
+``PYDANTIC_AI_NO_BANNER=1`` in the worker environment.
+
What you get, and what you do not
---------------------------------
diff --git
a/providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py
b/providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py
index 3a6a91404d8..146560881e4 100644
--- a/providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py
+++ b/providers/common/ai/src/airflow/providers/common/ai/hooks/pydantic_ai.py
@@ -20,6 +20,7 @@ import re
from pathlib import Path
from typing import TYPE_CHECKING, Any, TypeVar, overload
+import pydantic_ai
from pydantic_ai import Agent
from pydantic_ai.exceptions import ModelAPIError
from pydantic_ai.models import infer_model
@@ -31,6 +32,10 @@ from airflow.providers.common.ai.utils.toolset_base import
MaskingCapability
from airflow.providers.common.compat.module_loading import import_string
from airflow.providers.common.compat.sdk import BaseHook
+# An Airflow task is never interactive, and pydantic-ai's first-run banner on
stderr would
+# land in the task log as ERROR lines.
+pydantic_ai.BANNER_ENABLED = False
+
OutputT = TypeVar("OutputT")
# Sentinel distinguishing "caller did not pass ``instrument``" from an explicit
diff --git a/providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py
b/providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py
index 098408e02d6..3aa297c3bd4 100644
--- a/providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py
+++ b/providers/common/ai/tests/unit/common/ai/hooks/test_pydantic_ai.py
@@ -1338,6 +1338,24 @@ class TestPydanticAIHookCreateAgent:
assert agent.run_sync("hi").output == "api key: ***"
+ def test_agent_run_from_a_coding_agent_shell_prints_no_banner(self,
monkeypatch, capsys):
+ """pydantic-ai shows its first-run banner to a coding agent; a task's
stderr goes to the task log."""
+ # The banner, and the module holding its once-per-process claim,
arrived in pydantic-ai 2.43.
+ pydantic_ai_display = pytest.importorskip("pydantic_ai._display")
+ monkeypatch.setenv("CLAUDECODE", "1")
+ # pydantic-ai suppresses the banner by itself under pytest and in CI.
+ monkeypatch.delenv("PYTEST_VERSION", raising=False)
+ monkeypatch.delenv("CI", raising=False)
+ monkeypatch.delenv("PYDANTIC_AI_NO_BANNER", raising=False)
+ monkeypatch.setattr(pydantic_ai_display, "_banner_displayed", False)
+
+ hook = PydanticAIHook(llm_conn_id="test_conn")
+ with patch.object(hook, "get_conn", autospec=True,
return_value=TestModel()):
+ agent = hook.create_agent(instructions="hi")
+ agent.run_sync("hi")
+
+ assert capsys.readouterr().err == ""
+
def test_create_agent_without_instructions_or_spec_file_raises(self):
hook = PydanticAIHook(llm_conn_id="test_conn",
model_id="openai:gpt-5.6-sol")
with pytest.raises(ValueError, match="instructions is required"):