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 2aa4ae811d8 Rewrite the OpenAI Agents SDK guide to stand on its own 
(#76340)
2aa4ae811d8 is described below

commit 2aa4ae811d89ce380028aaec06cecb50a04615ec
Author: Kaxil Naik <[email protected]>
AuthorDate: Wed Nov 4 12:29:20 2026 +0000

    Rewrite the OpenAI Agents SDK guide to stand on its own (#76340)
    
    The guide sent readers to the Strands page for what AirflowTools accepts,
    how tool failures reach the model and why results are masked, and it put
    installation last. It now covers, on one page: installing, trying the
    tools from a script, running the agent in a task, giving it bounded
    access to a warehouse and object storage, what the model sees when a call
    is refused, MCP servers, tracing, durable retries, and which SDK features
    (sessions, needs_approval, streaming) Airflow does not handle.
    
    Adds example_openai_agents_data_access, an agent limited to two warehouse
    tables and one bucket prefix.
---
 .../common/ai/docs/frameworks/openai_agents.rst    | 629 +++++++++++++++++----
 .../example_openai_agents_data_access.py           | 101 ++++
 2 files changed, 616 insertions(+), 114 deletions(-)

diff --git a/providers/common/ai/docs/frameworks/openai_agents.rst 
b/providers/common/ai/docs/frameworks/openai_agents.rst
index a704a4298d9..23f3d6908fa 100644
--- a/providers/common/ai/docs/frameworks/openai_agents.rst
+++ b/providers/common/ai/docs/frameworks/openai_agents.rst
@@ -22,97 +22,471 @@ OpenAI Agents SDK
 
 You have an agent built with the `OpenAI Agents SDK
 <https://openai.github.io/openai-agents-python/>`__ and you want it to run as 
an Airflow
-task, reading data through connections your deployment already manages. Keep 
the agent
-as it is, and add Airflow's toolsets to its tools with
-:class:`~airflow.providers.common.ai.tools.openai_agents.AirflowTools`.
+task: on a schedule, after an upstream task, or when an asset updates, reading 
data
+through connections your deployment already manages. The agent, its runner, 
handoffs,
+guardrails and your own function tools stay plain OpenAI Agents SDK. Airflow 
adds these,
+each of which you can use without the others:
+
+- :func:`~airflow.providers.common.ai.durable.openai_agents.run_durably_sync` 
runs the
+  agent so that a task retry replays the model and tool calls the failed try 
completed,
+  instead of paying for them again.
+- :class:`~airflow.providers.common.ai.tools.openai_agents.AirflowTools` gives 
the agent
+  access to your warehouse, object storage and other systems through Airflow
+  connections, limited by each toolset, with Airflow's secret masker applied 
to every
+  result.
+- 
:class:`~airflow.providers.common.ai.tools.openai_agents.AirflowTraceProcessor`
+  records the SDK's traces as OpenTelemetry spans under the task's span 
instead of
+  sending them to OpenAI.
+
+If you are not tied to the SDK,
+:class:`~airflow.providers.common.ai.operators.agent.AgentOperator` runs a 
Pydantic AI
+agent with the same toolsets, and adds a human review step and durable 
execution
+switched on with one argument. See :ref:`howto/operator:agent`.
 
 .. note::
 
-    ``AirflowTools`` is experimental: it may change in a minor release of this 
provider.
-    The framework-neutral tool interface it builds on is stable.
-    See :ref:`howto/stability`.
+    ``AirflowTools``, ``AirflowTraceProcessor`` and ``run_durably`` are 
experimental:
+    they may change in a minor release of this provider. The tool interface 
they build
+    on, :class:`~airflow.providers.common.ai.tools.AirflowTool` and
+    :class:`~airflow.providers.common.ai.tools.ToolProvider`, is stable. See 
:ref:`howto/stability`.
 
-Run an OpenAI Agents SDK agent in a task
-----------------------------------------
+Installation
+------------
+
+Install the provider with the ``sql`` extra, which the examples' SQL toolset 
needs, and
+the SDK:
+
+.. code-block:: bash
+
+    pip install "apache-airflow-providers-common-ai[sql]>=1.0.0" 
"openai-agents>=0.20.0"
+
+``openai-agents`` 0.21 and later require ``openai`` 3. Where ``openai`` is 
held below 3,
+the resolver picks ``openai-agents`` 0.20, the last release that accepts 
``openai`` 2.
+``AirflowTools``, ``AirflowTraceProcessor`` and ``run_durably`` work with both.
+
+Try the tools from a script
+---------------------------
+
+``AirflowTools`` needs no Dag, scheduler or metadata database. A Python script 
that can
+see an Airflow connection can give an agent a toolset's tools, so you can try 
an agent
+on your machine before you put it in a task. Define the connection in an 
environment
+variable:
+
+.. code-block:: bash
+
+    export AIRFLOW_CONN_CRM='{"conn_type": "sqlite", "host": "/tmp/crm.db"}'
 
-The agent below answers a question about a database with a
-:class:`~airflow.providers.common.ai.toolsets.sql.SQLToolset`. Its model's API 
key, and
-optionally the base URL of another OpenAI-compatible endpoint, come from an 
Airflow
-connection:
+Then build the agent as you would without Airflow, with the toolset's tools in
+``tools``. This one uses the SDK's default OpenAI client, which reads
+``OPENAI_API_KEY``:
+
+.. code-block:: python
+
+    from agents import Agent, Runner
+
+    from airflow.providers.common.ai.tools.openai_agents import AirflowTools
+    from airflow.providers.common.ai.toolsets.sql import SQLToolset
+
+    agent = Agent(
+        name="analyst",
+        model="gpt-5-mini",
+        instructions="Use list_tables, get_schema and query to answer.",
+        tools=AirflowTools(SQLToolset(db_conn_id="crm")),
+    )
+    print(Runner.run_sync(agent, "Which customer has the highest 
ARR?").final_output)
+
+The script expects ``/tmp/crm.db`` to be a SQLite database the agent can 
explore, such as
+one with a ``customers`` table.
+
+On Airflow 3, a script reads connections from environment variables and from a
+configured secrets backend. It does not see a connection that exists only in 
the
+metadata database, such as one created with ``airflow connections add``. 
``SQLToolset``
+hands that failure to the model like any other database error, so the model 
sees this
+for a connection ID the script cannot find, and the run fails once the tool's 
retry limit
+is used up:
+
+.. code-block:: text
+
+    {"error": "The list_tables tool failed: The conn_id `crm_missing` isn't 
defined\nUse the list_tables and get_schema tools to inspect the database, then 
fix the query and try again."}
+
+Inside a task, every connection your deployment defines is available.
+
+Run the agent in a task
+-----------------------
+
+In a Dag, build the agent inside the ``@task`` function, so connections are 
read when
+the task runs rather than when the Dag file is parsed. Here the model's API 
key, and
+optionally the base URL of another OpenAI-compatible endpoint such as a 
gateway or a
+self-hosted server, come from an Airflow connection too:
 
 .. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_openai_agents_agent.py
     :language: python
     :start-after: [START example_openai_agents_agent]
     :end-before: [END example_openai_agents_agent]
 
-Everything except ``AirflowTools`` is plain OpenAI Agents SDK: the agent, its 
runner,
-handoffs, guardrails and your own function tools work as the SDK documentation
-describes. ``AirflowTools`` is a list of ``FunctionTool`` objects, so add your 
own tools
-next to them with ``tools=[*AirflowTools(warehouse), my_tool]``.
-
-What the tools give the agent
------------------------------
-
-``AirflowTools`` takes the same arguments as the Strands plugin (see 
:doc:`strands`):
-toolsets that implement 
:class:`~airflow.providers.common.ai.tools.ToolProvider`, such
-as ``SQLToolset`` and ``HookToolset``, and individual
-:class:`~airflow.providers.common.ai.tools.AirflowTool` objects. 
``MCPToolset`` is the
-exception: attach an MCP server with the SDK's own ``mcp_servers=``. Each tool 
keeps the
-toolset tool's name, description and argument schema, runs through the 
toolset, and
-passes its result through Airflow's secret masker.
-
-- The schema goes to the model as it is, with ``strict_json_schema=False``. 
The SDK's
-  strict mode would mark every argument required, including the ones a toolset 
leaves
-  optional.
-- A failure the model can correct, such as a rejected SQL statement or an 
invalid
-  argument, reaches it as ``{"error": ...}``. The tool's retry limit bounds 
how many
-  times in a row the model can try again, counted afresh on every run of the 
agent.
-- A refusal the tool reports as final, such as a file that does not exist, 
also comes
-  back as ``{"error": ...}`` without counting against that limit, and so do 
arguments
-  that are not a JSON object. The ``max_turns`` argument of ``Runner.run`` 
bounds how
-  long a model can keep asking.
-- Any other failure raises
-  :class:`~airflow.providers.common.ai.tools.ToolCallError` out of 
``Runner.run`` and
-  ``Runner.run_sync``, with the message masked. The task fails and Airflow's 
own retry
-  takes over.
-
-Outside ``AgentOperator``, a toolset's connection ID is used as written: it is 
not
-rendered as a template.
-
-An agent used as a tool
------------------------
+The example reads two connections: ``openai_default``, whose password is the 
API key and
+whose host, if set, is the base URL, and ``sql_default``, a database 
connection. Set the
+``LLM_CONN_ID`` and ``DB_CONN_ID`` environment variables to use others.
+
+The example uses ``OpenAIChatCompletionsModel``, so the connection's host can 
point at
+a server that implements only the Chat Completions API. Against OpenAI itself,
+``OpenAIResponsesModel`` with the same client uses the Responses API, the 
SDK's default.
+
+``AirflowTools`` is a list of ``FunctionTool`` objects, so your own tools go 
next to it:
+``tools=[*AirflowTools(SQLToolset(db_conn_id="crm")), my_tool]``.
+
+.. _howto/frameworks:openai-agents-data-access:
+
+Give the agent access to your data
+----------------------------------
+
+An agent in a data pipeline needs the warehouse, the buckets and the internal 
APIs your
+other tasks already reach. Give it those through Airflow toolsets rather than 
tools you
+write yourself. Each toolset logs in through an Airflow connection resolved in 
the
+worker, so the credential is not part of the prompt or the tool schema. If a 
tool result
+echoes it anyway, the secret masker replaces it with ``***``, as it does for 
every secret
+Airflow has registered. Each toolset also limits what the agent can do with its
+connection.
+
+This agent answers a revenue question from two tables of a warehouse and one 
prefix of a
+bucket:
+
+.. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_openai_agents_data_access.py
+    :language: python
+    :start-after: [START example_openai_agents_data_access]
+    :end-before: [END example_openai_agents_data_access]
+
+To run it, install ``apache-airflow-providers-amazon[s3fs]`` for the ``s3://`` 
path, and
+create three connections: ``openai_default`` for the model, 
``warehouse_agent_reader``
+for a ``DbApiHook`` database such as Postgres, and ``aws_reports_reader`` for 
AWS.
+
+What each toolset gives the agent, and what limits it:
+
+.. list-table::
+    :header-rows: 1
+    :widths: 22 40 38
+
+    * - Toolset
+      - Tools the model gets
+      - What you limit it with
+    * - :class:`~airflow.providers.common.ai.toolsets.sql.SQLToolset`
+      - ``list_tables``, ``get_schema``, ``query`` and ``check_query`` against 
any
+        database with a ``DbApiHook``
+      - ``allowed_tables``, ``allow_writes`` (off by default), ``max_rows``,
+        ``max_result_bytes``
+    * - 
:class:`~airflow.providers.common.ai.toolsets.object_storage.ObjectStorageToolset`
+      - ``list_files``, ``get_file_info`` and ``read_file`` under one path in 
S3, GCS,
+        Azure Blob Storage or any store ``ObjectStoragePath`` opens. It cannot 
write.
+      - The root path, ``max_files``, ``max_read_bytes``, ``max_output_bytes``
+    * - 
:class:`~airflow.providers.common.ai.toolsets.datafusion.DataFusionToolset`
+        (experimental)
+      - ``list_tables``, ``get_schema`` and ``query``: SQL over Parquet, CSV 
and Avro
+        files and Iceberg tables, run in the worker
+      - The tables you register, ``allow_writes`` (off by default), 
``max_rows``
+    * - :class:`~airflow.providers.common.ai.toolsets.hook.HookToolset`
+      - The methods you list from any Airflow hook, one tool per method
+      - ``allowed_methods`` (required), ``pinned_arguments``; see
+        :ref:`howto/frameworks:openai-agents-hooks`
+
+A query that names a table outside ``allowed_tables``, or a path outside the 
storage
+root, never reaches the database or the store. The model gets the reason back, 
and can
+try again. Limits such as ``max_rows`` and ``max_read_bytes`` work 
differently: the query
+or read still runs, and the result is cut to fit. With the example's two 
toolsets over a
+SQLite database that also holds a ``salaries`` table, and a local directory 
with a
+``payroll`` folder beside ``finance``, these calls were refused with these 
messages:
+
+``query`` with ``SELECT employee, salary FROM salaries``
+    .. code-block:: text
+
+        {"error": "The query tool failed: Query references tables that are not 
in the allowed tables list: salaries. Use list_tables to see the allowed 
tables.\nUse the list_tables and get_schema tools to inspect the database, then 
fix the query and try again."}
+
+``read_file`` with ``../payroll/salaries.csv``
+    .. code-block:: text
+
+        {"error": "'../payroll/salaries.csv' leaves the storage root; '..' is 
not allowed."}
+
+The SQL refusal counts against the tool's retry limit, one correction in a row 
by
+default, so a model that sends a second refused query in a row fails the task 
(see
+:ref:`howto/frameworks:openai-agents-failures`). The object storage refusal 
does not count,
+so bound repeated refused reads with ``max_turns``.
+
+Back each check with the connection's own permissions: a database role granted
+``SELECT`` on the two tables, and credentials that can read only the reports 
prefix. The
+database and the store enforce those themselves, including for a query or path 
that a
+toolset's check lets through.
+
+``SQLToolset`` and ``DataFusionToolset`` refuse statements that change data 
unless you
+pass ``allow_writes=True``, and ``ObjectStorageToolset`` cannot write at all. 
To store what
+the agent produced, write it from your own task code, ideally in the next 
task, as
+:ref:`howto/frameworks:openai-agents-durable` shows.
+
+.. _howto/frameworks:openai-agents-hooks:
+
+Other systems through their hooks
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+For a system no toolset covers, ``HookToolset`` turns methods of its Airflow 
hook into
+tools. It is only as narrow as the methods you list, so check each one before 
you expose
+it:
+
+- What it does. A ``HookToolset`` can do whatever its listed methods do, 
writes and
+  deletes included; nothing in it is read-only by default.
+- Which of its arguments choose the resource. Pin those with 
``pinned_arguments``, so the
+  model never sees them and a call that sets one is refused. Then check that 
no argument
+  you leave open can widen the call again. ``HttpHook.run`` fails this test: 
pinning
+  ``endpoint`` still leaves ``extra_options``, which can set a proxy or turn 
off TLS
+  verification for the request.
+- What it returns. The model sees the result serialized as JSON, or as 
``str()`` of an
+  object that is not JSON-compatible. ``HttpHook.run`` returns a 
``requests.Response``,
+  which the model would see as ``<Response [200]>``.
+
+:ref:`hook-toolset-restricted` shows a hook that passes these checks: 
``S3Hook`` with
+``list_keys`` and ``read_key`` and the bucket pinned. The same toolset works in
+``AirflowTools(...)``.
+
+``.filtered()`` and ``.approval_required()``, which pydantic-ai toolsets 
offer, do not
+reach the SDK through ``AirflowTools``. They return wrappers that 
``AirflowTools`` cannot
+read, and building it fails:
+
+.. code-block:: text
+
+    AttributeError: 'FilteredToolset' object has no attribute 'airflow_tools'
+
+Choose the tools with ``allowed_tables``, ``allowed_methods`` and 
``pinned_arguments``
+instead. There is no per-call approval on this path; see
+:ref:`howto/frameworks:openai-agents-limits`. Each toolset's page covers the 
rest of its
+options: :ref:`howto/toolsets`.
+
+Tools
+-----
+
+Where an agent's tools come from decides how they behave in a task:
+
+.. list-table::
+    :header-rows: 1
+    :widths: 25 20 30 25
+
+    * - Tool
+      - Result masked
+      - A failure the model can correct
+      - Replayed on a durable retry
+    * - ``AirflowTools``
+      - Yes
+      - Returned to the model as ``{"error": ...}``; for a toolset's tools, up 
to a retry limit
+      - Yes, an ``{"error": ...}`` result too; a call that raised runs again
+    * - Your own ``@function_tool``
+      - No
+      - As its ``failure_error_function`` decides
+      - Yes, unless it raised, even when ``failure_error_function`` turned the
+        exception into text
+    * - ``mcp_servers``
+      - No
+      - As the SDK handles it
+      - No, runs live
+    * - Hosted tools (web search, file search), shell and computer tools
+      - No
+      - As the SDK handles it
+      - No, runs live
+
+Toolsets for ``AirflowTools``
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``AirflowTools`` accepts any toolset that implements
+:class:`~airflow.providers.common.ai.tools.ToolProvider`, such as
+:class:`~airflow.providers.common.ai.toolsets.sql.SQLToolset`,
+:class:`~airflow.providers.common.ai.toolsets.hook.HookToolset` and
+:class:`~airflow.providers.common.ai.toolsets.object_storage.ObjectStorageToolset`,
 and
+adds one function tool per toolset tool. Each tool:
+
+- Keeps the toolset tool's name, description and argument schema. The schema 
goes to
+  the model as it is, with ``strict_json_schema=False``: the SDK's strict mode 
would
+  mark every argument required, including the ones a toolset leaves optional.
+- Runs through the toolset itself, so connection resolution, SQL validation,
+  ``allowed_tables``, ``allowed_methods`` and result bounds behave as each 
toolset's
+  page describes. See :ref:`howto/toolsets`.
+- Passes every result through Airflow's secret masker before it goes back to 
the
+  model.
+- Writes a ``Tool call: <name>`` group to the task log with the call's 
duration.
+
+A toolset's connection ID is used as written: it is not rendered as a Jinja 
template,
+so pass the value itself.
+
+A :class:`~airflow.providers.common.ai.toolsets.sandbox.SandboxToolset` gives 
the agent
+a sandbox to run code in, and its tools work only inside ``with sandbox:``, 
which
+destroys the sandbox when the block ends. A call outside the block fails with
+``SandboxToolset is not open``. Build and run the agent inside it:
+
+.. code-block:: python
+
+    from agents import Agent, Runner
+
+    from airflow.providers.common.ai.sandbox.modal import ModalSandboxBackend
+    from airflow.providers.common.ai.tools.openai_agents import AirflowTools
+    from airflow.providers.common.ai.toolsets import SandboxToolset
+
+    with SandboxToolset(ModalSandboxBackend()) as sandbox:
+        agent = Agent(name="coder", model="gpt-5-mini", 
tools=AirflowTools(sandbox))
+        answer = Runner.run_sync(agent, "Plot last month's 
signups.").final_output
+
+See :ref:`sandbox-other-frameworks` for the backends and what the sandbox 
contains.
+
+The SDK routes a call by tool name, so two toolsets that define the same name 
raise
+``ValueError`` when you build ``AirflowTools``:
+
+.. code-block:: text
+
+    ValueError: More than one tool is named check_query, get_schema, 
list_tables, query. Give the toolsets different tool_prefix values, where they 
take one, so the model can call each of them.
+
+.. _howto/frameworks:openai-agents-failures:
+
+What the model sees when a call fails
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+A failure the model can correct, such as a query that names a column the table 
does not
+have, reaches it as ``{"error": ...}`` with the database's message, and the 
model can
+try again. This is what the model received for a query against a ``customers`` 
table
+that has an ``arr`` column but no ``revenue`` column:
+
+.. code-block:: text
+
+    {"error": "The query tool failed: no such column: revenue\nUse the 
list_tables and get_schema tools to inspect the database, then fix the query 
and try again."}
+
+``SQLToolset`` refuses statements that change data unless it was built with
+``allow_writes=True``, and the refusal goes back to the model the same way:
+
+.. code-block:: text
+
+    {"error": "The query tool failed: Statement type 'Delete' is not allowed. 
Allowed types: Select, Union, Intersect, Except, Describe, Show\nUse the 
list_tables and get_schema tools to inspect the database, then fix the query 
and try again."}
+
+A toolset's tool may fail this way once in a row by default; pass 
``max_retries`` to the
+toolset to allow more. The count starts over after a call that succeeds and on 
every run
+of the agent. A tool still failing after that raises
+:class:`~airflow.providers.common.ai.tools.ToolCallError` out of 
``Runner.run`` and
+``Runner.run_sync``, so the task fails and Airflow's own retry takes over. 
Catch it as
+``ToolCallError``; the traceback names a private subclass of it. For the same 
query sent
+twice in a row, the message is:
+
+.. code-block:: text
+
+    query kept failing after 1 correction(s): The query tool failed: no such 
column: revenue
+    Use the list_tables and get_schema tools to inspect the database, then fix 
the query and try again.
+
+A refusal the tool reports as final, such as a file that does not exist, comes 
back as
+``{"error": ...}`` without counting against that limit, and so do arguments 
that are not
+a JSON object. Bound those with the ``max_turns`` argument of ``Runner.run``. 
Any other
+failure, such as a hook method raising, also raises ``ToolCallError`` with its 
message
+masked, and the task fails.
+
+A function tool you write yourself is handled by the SDK instead. When it 
raises,
+``openai-agents`` releases before 0.23 send the exception's text to the model 
unmasked.
+From 0.23.1 they send ``An error occurred while running the tool. Please try 
again.``
+with no detail, which gives the model nothing to correct.
+
+Your own functions as Airflow tools
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Results of a tool you write with the SDK's ``@function_tool`` do not pass 
through the
+masker. A result that carries a registered secret, such as an API response 
that echoes a
+connection's token, would then send it to the model, the model provider, the 
SDK's
+traces, and possibly the final answer, even though the task log shows ``***``. 
To give
+one of your own functions the same treatment as a toolset, wrap it as an
+:class:`~airflow.providers.common.ai.tools.AirflowTool` and pass it to 
``AirflowTools``
+next to the toolsets:
+
+.. code-block:: python
+
+    import asyncio
+
+    from agents import Agent
+
+    from airflow.providers.common.ai.tools import AirflowTool, ToolResult
+    from airflow.providers.common.ai.tools.openai_agents import AirflowTools
+    from airflow.providers.common.ai.toolsets.sql import SQLToolset
+
+
+    async def lookup_customer(arguments: dict) -> ToolResult:
+        # crm_client is your own client; its get() blocks, so run it off the 
event loop.
+        customer = await asyncio.to_thread(crm_client.get, 
arguments["customer_id"])
+        return ToolResult(content=customer)
+
+
+    lookup = AirflowTool(
+        name="lookup_customer",
+        description="Look up one customer in the CRM by id.",
+        parameters={
+            "type": "object",
+            "properties": {"customer_id": {"type": "integer"}},
+            "required": ["customer_id"],
+        },
+        function=lookup_customer,
+    )
+    warehouse = SQLToolset(db_conn_id="warehouse")
+    agent = Agent(name="support", model="gpt-5-mini", 
tools=AirflowTools(warehouse, lookup))
+
+Return ``ToolResult(content=..., is_error=True)`` for a failure the model can 
correct,
+and raise for anything else. An ``AirflowTool`` you build has no retry limit 
of its own,
+so only ``max_turns`` bounds how often the model retries it. The masker knows 
the
+secrets Airflow has registered, such as connection passwords and sensitive 
connection
+extras. It does not recognize a credential that exists only in the data a tool 
returns,
+such as an API key stored in a table row. To keep one from the agent, deny the
+connection's database role or cloud identity access to it.
+
+Agents as tools and handoffs
+----------------------------
+
+Give each agent the ``AirflowTools`` it needs. An agent that takes over 
through a
+handoff runs its own tools, and they behave as described above.
 
 ``Agent.as_tool()`` turns any exception from the inner agent's run into text 
for the
-outer model by default, so a tool failure that should fail the task would only 
reach the
-orchestrating model as a message. When the inner agent holds ``AirflowTools``, 
pass
-``failure_error_function=None``:
+outer model by default, so a tool failure that should fail the task would 
reach the
+orchestrating model only as a message. When the inner agent holds 
``AirflowTools``,
+pass ``failure_error_function=None``:
 
 .. code-block:: python
 
-    analyst = Agent(name="analyst", model=model, tools=AirflowTools(warehouse))
+    analyst = Agent(
+        name="analyst",
+        model="gpt-5-mini",
+        tools=AirflowTools(SQLToolset(db_conn_id="warehouse")),
+    )
     lead = Agent(
         name="lead",
-        model=model,
+        model="gpt-5-mini",
         tools=[analyst.as_tool("ask_analyst", "Ask the SQL analyst.", 
failure_error_function=None)],
     )
 
 The ``ToolCallError`` then fails the outer run, and with it the task. The same 
applies to
-a function tool of your own that runs another agent: let the exception 
propagate. A
-nested run shares the outer run's retry count for each tool.
+a function tool of your own that runs another agent: let the exception 
propagate.
+
+MCP servers
+-----------
+
+Connect an MCP server with the SDK's own ``mcp_servers=`` and one of its 
``MCPServer``
+classes, such as ``MCPServerStreamableHttp``. ``MCPToolset`` does not work 
through
+``AirflowTools``, because an MCP session belongs to the event loop that opened 
it.
+``AirflowTools(MCPToolset(...))`` raises:
+
+.. code-block:: text
+
+    NotImplementedError: MCPToolset works in Pydantic AI agents and through 
the LangChain bridge, not through the framework-neutral tools. Connect your 
framework's own MCP client to the server instead, such as Strands' MCPClient or 
ADK's McpToolset.
+
+Results from ``mcp_servers`` do not pass through Airflow's secret masker, and 
under
+``run_durably`` their calls run again on every retry.
 
 Tracing
 -------
 
 The SDK has a tracing system of its own rather than OpenTelemetry. By default 
it sends
 every run's traces to OpenAI's Traces backend using ``OPENAI_API_KEY`` from the
-environment, whatever model the agent runs, including a self-hosted one, and 
the traces
-hold the prompts and model output. Tool results are masked before the model 
sees them;
-nothing else in those traces is.
-
-:class:`~airflow.providers.common.ai.tools.openai_agents.AirflowTraceProcessor`
 is a trace
-processor for the SDK that records each trace as OpenTelemetry spans under the 
task's
-span instead. The example replaces the SDK's processors with it, so nothing 
goes to
-OpenAI, and runs the agent inside
+environment, whatever model the agent runs, including a self-hosted one. The 
traces
+hold the prompts and the model's output. When the worker has no 
``OPENAI_API_KEY``, as
+when the key comes from a connection, the SDK logs ``OPENAI_API_KEY is not 
set, skipping
+trace export`` instead. Tool results from ``AirflowTools`` are masked
+before the model sees them; nothing else in those traces is.
+
+``AirflowTraceProcessor`` records each trace as OpenTelemetry spans under the 
task's
+span instead. Replace the SDK's processors with it, so nothing goes to OpenAI, 
and run
+the agent inside
 :func:`~airflow.providers.common.ai.tools.tracing.agent_framework_tracing`, so 
the spans
 carry the task's identity:
 
@@ -129,18 +503,20 @@ carry the task's identity:
 
 ``set_trace_processors`` replaces every processor registered with the SDK. To 
keep the
 export to OpenAI, or a processor another instrumentation registered, add this 
one with
-``agents.add_trace_processor`` instead. The spans use the names of the 
OpenTelemetry GenAI
-conventions (``invoke_agent``, ``chat`` with the model and token counts,
-``execute_tool``), and the SDK's other spans are named 
``openai_agents.<kind>``. Prompts, completions, and tool arguments and results 
are left
-out unless ``[common.ai] otel_export_enabled`` and ``capture_content`` are 
both on, and pass
-through Airflow's secret masker when they are recorded; see 
:doc:`../observability`.
+``agents.add_trace_processor`` instead.
 
-Use ``AirflowTraceProcessor`` when span content should follow ``[common.ai] 
capture_content``
-and Airflow's secret masker.
+The spans use the names of the OpenTelemetry GenAI conventions: 
``invoke_agent`` for an
+agent, ``chat`` for a model call with its model and token counts, and 
``execute_tool``
+for a tool call. The SDK's other spans, such as one per turn of the agent loop 
or one per
+handoff, are named ``openai_agents.<kind>``. Prompts, completions, and tool 
arguments and
+results are left out unless ``[common.ai] otel_export_enabled`` and 
``capture_content``
+are both on, and pass through Airflow's secret masker when they are recorded; 
see
+:doc:`../observability`.
 
-To turn the SDK's tracing off altogether, call 
``agents.set_tracing_disabled(True)`` or set
-``OPENAI_AGENTS_DISABLE_TRACING=1`` in the worker's environment. That stops
-``AirflowTraceProcessor`` from receiving anything too.
+To turn the SDK's tracing off altogether, call 
``agents.set_tracing_disabled(True)`` or
+set ``OPENAI_AGENTS_DISABLE_TRACING=1`` in the worker's environment, and
+``AirflowTraceProcessor`` then receives nothing either. To turn it off for one 
run, pass
+``run_config=RunConfig(tracing_disabled=True)``, as the durable example below 
does.
 
 .. _howto/frameworks:openai-agents-durable:
 
@@ -149,12 +525,13 @@ Durable execution
 
 When a task that calls ``Runner.run`` fails and Airflow retries it, the agent 
starts
 again from the prompt: every model call is paid for again, and every tool call 
happens
-again. On a task with ``retries``, run the agent with
-:func:`~airflow.providers.common.ai.durable.openai_agents.run_durably_sync`, or
+again. On a task with ``retries``, run the agent with ``run_durably_sync``, or
 :func:`~airflow.providers.common.ai.durable.openai_agents.run_durably` from 
async code,
 instead. They take the same arguments as ``Runner.run_sync`` and 
``Runner.run``, except
-``previous_response_id``, ``conversation_id`` and 
``auto_previous_response_id``, which they
-refuse: with them OpenAI keeps the conversation, and a replayed call never 
reaches it.
+``previous_response_id``, ``conversation_id`` and 
``auto_previous_response_id``, which
+they refuse with ``ValueError`` inside a task: with them OpenAI keeps the 
conversation,
+and a replayed call never reaches it. This Dag runs the agent from the task 
example
+with ``run_durably_sync``:
 
 .. exampleinclude:: 
/../../ai/src/airflow/providers/common/ai/example_dags/example_openai_agents_durable.py
     :language: python
@@ -162,22 +539,46 @@ refuse: with them OpenAI keeps the conversation, and a 
replayed call never reach
     :end-before: [END example_openai_agents_durable]
 
 Each model call and function tool call is recorded in the task instance's 
durable
-journal as it completes, the journal ``AgentOperator(durable=True)`` uses (see
-:doc:`../durable_execution`). On Airflow 3.3 and later the journal is kept in 
the task
-state store; on earlier versions, set ``[common.ai] durable_cache_path`` to 
where it is
-kept. Once the run succeeds, what was recorded is deleted, so clearing the 
task later
-starts over. Outside a task there is nothing to replay into, and the agent 
runs as
-``Runner.run`` runs it.
+journal as it completes (see :doc:`../durable_execution`). On Airflow 3.3 and 
later the
+journal is kept in the task state store. On earlier versions, set ``[common.ai]
+durable_cache_path`` to where it is kept; without it the call raises 
``ValueError:
+durable=True requires [common.ai] durable_cache_path to be set.`` Outside a 
task there
+is nothing to replay into, and the agent runs as ``Runner.run`` runs it.
+
+What was recorded is deleted when the agent's run succeeds, so clearing the 
task later
+starts over. That happens when ``run_durably_sync`` returns, not when the task
+succeeds: if the task fails after the agent has answered, for example while 
writing the
+answer somewhere, the retry runs the whole agent again. Write the answer in a
+downstream task, so that a failed write retries on its own. Here 
``build_agent()``
+stands for building the agent as the task example does:
+
+.. code-block:: python
+
+    from airflow.providers.common.ai.durable.openai_agents import 
run_durably_sync
+    from airflow.sdk import ObjectStoragePath, task
+
+
+    @task(retries=2)
+    def answer(question: str) -> str:
+        return run_durably_sync(build_agent(), question).final_output
+
+
+    @task(retries=5)
+    def publish(text: str) -> None:
+        ObjectStoragePath("s3://reports/answer.md", 
conn_id="aws_default").write_text(text)
+
+
+    publish(answer("Which customer has the highest ARR?"))
 
 What a retry replays
 ^^^^^^^^^^^^^^^^^^^^
 
 On a retry the agent runs again from the start, and each call that asks what 
the failed
-try asked, in the same order, returns what it returned then: the calls of 
every model
-given by name or in ``run_config``, the model and function tools of the 
starting agent
-and of the agents in its ``handoffs`` list, and an agent used through 
``as_tool``, as one
-call. Recorded tool results pass through Airflow's secret masker before they 
are saved,
-and a replayed model call reports no token usage.
+try asked, in the same order, returns what it returned then. That covers the 
calls of
+every model given by name or in ``run_config``, the model and function tools 
of the
+starting agent and of the agents in its ``handoffs`` list, and an agent used 
through
+``as_tool``, which replays as one call. Recorded tool results pass through 
Airflow's
+secret masker before they are saved, and a replayed model call reports no 
token usage.
 
 From the first call that differs, that call and every call after it run live:
 
@@ -193,38 +594,38 @@ What always runs live
 
 - A call that was running when the try failed, so tools that change data must 
still be
   safe to repeat.
-- A tool call that failed, including one whose exception 
``failure_error_function`` turned
-  into text for the model. A ``ModelBehaviorError`` that a tool raises, such 
as arguments
-  that fail validation with ``failure_error_function=None``, counts as the 
tool's failure:
-  the tool call runs again, but the answer that asked for it replays.
-- Tools other than function tools: hosted tools such as web search, the tools 
of
-  ``mcp_servers``, and shell and computer tools.
-- A function tool whose ``needs_approval`` is a function, since the SDK checks 
the
-  arguments it approved only through its own invoker.
+- A tool call that raised, including one whose exception 
``failure_error_function``
+  turned into text for the model. An ``{"error": ...}`` result from 
``AirflowTools`` is
+  not a raise: it is recorded, and the retry replays it. A 
``ModelBehaviorError`` that a tool raises, such as
+  arguments that fail validation with ``failure_error_function=None``, counts 
as the
+  tool's failure: the tool call runs again, but the answer that asked for it 
replays.
+- Tools other than function tools: hosted tools, the tools of ``mcp_servers``, 
and shell
+  and computer tools.
+- A function tool whose ``needs_approval`` is a function: the SDK checks that 
the
+  arguments a call runs with are the ones it approved, and a replayed result 
would skip
+  that check.
 - The function tools of an agent reached through ``handoff()``, and its model 
too unless
   the agent's ``model`` is a model name or not set.
-- A streamed run.
+- A streamed run (``Runner.run_streamed``).
 - Every call, when the run has a ``session``: the SDK adds the failed try's 
prompt to it
   before the first model call, so that call no longer asks what it asked then.
 
-A ``SandboxToolset`` without ``attach_to`` is destroyed when the try ends, so 
the retry
-starts with a new, empty sandbox that the replayed conversation does not 
describe.
+A ``SandboxToolset`` without ``attach_to`` is destroyed when its ``with`` 
block ends,
+which a failed try also ends, so the retry starts with a new, empty sandbox 
that the
+replayed conversation does not describe.
 
-Differences from ``AgentOperator``
-----------------------------------
+.. _howto/frameworks:openai-agents-limits:
 
-- A task retry runs the agent again from the start unless it runs through
-  ``run_durably``; see :ref:`howto/frameworks:openai-agents-durable`.
-- There is no human review step like 
``AgentOperator(enable_hitl_review=True)``, and the
-  SDK's own ``needs_approval`` interruptions are not connected to Airflow's 
approval UI.
-
-Installation
-------------
-
-.. code-block:: bash
+Sessions, approvals and streaming
+---------------------------------
 
-    pip install "apache-airflow-providers-common-ai[sql]" 
"openai-agents>=0.20.0"
+These SDK features interact with Airflow in ways the SDK documentation does 
not cover:
 
-The SDK requires ``openai>=3`` from 0.21.0 on. Airflow's constraints file pins 
``openai``
-below 3, so with that file the command above installs ``openai-agents`` 0.20, 
the last
-release that accepts ``openai`` 2.x. ``AirflowTools`` works with both.
+- A ``session`` works, but under ``run_durably`` it makes every retry run 
live, as
+  above.
+- A tool's ``needs_approval`` is not connected to Airflow's approval UI. The 
run stops
+  at the first call that needs approval and returns with ``final_output`` set 
to
+  ``None`` and the pending call in ``result.interruptions``, and nothing in 
Airflow
+  resumes it. To have a person approve the agent's answer, add the standard
+  provider's ``ApprovalOperator`` as the next task.
+- A streamed run (``Runner.run_streamed``) is not recorded for durable retries.
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_openai_agents_data_access.py
 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_openai_agents_data_access.py
new file mode 100644
index 00000000000..dd71915dd6c
--- /dev/null
+++ 
b/providers/common/ai/src/airflow/providers/common/ai/example_dags/example_openai_agents_data_access.py
@@ -0,0 +1,101 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""
+Give an OpenAI Agents SDK agent read access to two warehouse tables and one 
prefix of a bucket.
+
+Each toolset logs in through an Airflow connection the agent never sees, and 
limits what
+the agent can reach with it: ``SQLToolset`` to two tables, read-only, and
+``ObjectStorageToolset`` to one prefix of a bucket.
+:class:`~airflow.providers.common.ai.tools.openai_agents.AirflowTools` hands 
both to the
+agent as function tools.
+
+Before running:
+
+1. Install the SDK and the provider the bucket needs:
+   ``pip install "openai-agents>=0.20.0" 
"apache-airflow-providers-amazon[s3fs]"``.
+2. Create a connection (``LLM_CONN_ID``, default ``openai_default``) whose 
password is
+   an OpenAI API key. Set its host to the base URL of any other 
OpenAI-compatible
+   endpoint, such as a gateway or a self-hosted server.
+3. Create the two data connections: ``warehouse_agent_reader`` (a ``DbApiHook``
+   database, such as Postgres) and ``aws_reports_reader`` (AWS). Give each 
credentials
+   that can read only what the agent needs.
+"""
+
+from __future__ import annotations
+
+import os
+
+from airflow.providers.common.compat.sdk import BaseHook, dag, task
+
+LLM_CONN_ID = os.environ.get("LLM_CONN_ID", "openai_default")
+LLM_MODEL = os.environ.get("LLM_MODEL", "gpt-5.6-luna")
+
+DEFAULT_QUESTION = "Why did EMEA revenue drop in October?"
+
+
+# [START example_openai_agents_data_access]
+@dag(tags=["example"])
+def example_openai_agents_data_access():
+    """Answer a revenue question from the warehouse and the finance reports."""
+
+    @task
+    def explain_revenue(question: str = DEFAULT_QUESTION) -> str:
+        # The SDK is an optional dependency, so it is imported when the task 
runs, not at parse time.
+        from agents import Agent, OpenAIChatCompletionsModel, RunConfig, Runner
+        from openai import AsyncOpenAI
+
+        from airflow.providers.common.ai.tools.openai_agents import 
AirflowTools
+        from airflow.providers.common.ai.toolsets import ObjectStorageToolset, 
SQLToolset
+
+        warehouse = SQLToolset(
+            # A role granted SELECT on these two tables only.
+            db_conn_id="warehouse_agent_reader",
+            # Refuse a query that reaches any other table.
+            allowed_tables=["orders", "customers"],
+            max_rows=100,
+        )
+        reports = ObjectStorageToolset(
+            # Every path the model asks for is relative to this prefix.
+            "s3://acme-reports/finance/",
+            conn_id="aws_reports_reader",
+            max_read_bytes=1024 * 1024,
+        )
+
+        llm = BaseHook.get_connection(LLM_CONN_ID)
+        agent = Agent(
+            name="revenue_analyst",
+            model=OpenAIChatCompletionsModel(
+                model=LLM_MODEL,
+                openai_client=AsyncOpenAI(api_key=llm.password, 
base_url=llm.host or None),
+            ),
+            instructions=(
+                "Answer revenue questions. Query the warehouse for the 
numbers, and read "
+                "the finance reports for context."
+            ),
+            tools=AirflowTools(warehouse, reports),
+        )
+        # The SDK sends traces to OpenAI by default; see the guide's tracing 
section.
+        result = Runner.run_sync(agent, question, 
run_config=RunConfig(tracing_disabled=True))
+        return result.final_output
+
+    explain_revenue()
+
+
+# [END example_openai_agents_data_access]
+
+
+example_openai_agents_data_access()

Reply via email to