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

kaxil pushed a commit to branch durable-toolsets-without-id
in repository https://gitbox.apache.org/repos/asf/airflow.git

commit f5d4cd4fbd1c0b9ffd361c61c2bb72db6e74d203
Author: Kaxil Naik <[email protected]>
AuthorDate: Wed Nov 11 07:30:09 2026 +0000

    Rename the durable callable toolset id helper and tidy step naming
    
    Rename callable_toolset_id to get_callable_toolset_id. The step namer
    now rewrites only the call_tool name: the validate_args name it also
    rewrote is never journaled, since argument validation runs live.
    
    Say in the tests why they fail if pydantic-ai stops reading the private
    FunctionToolset._id that durable runs write on a copy, and elide the
    memory address in the sample error in the durable execution docs.
---
 providers/common/ai/docs/durable_execution.rst              |  2 +-
 .../src/airflow/providers/common/ai/durable/capability.py   | 13 +++++++------
 .../ai/tests/unit/common/ai/durable/test_capability.py      | 13 +++++++++----
 3 files changed, 17 insertions(+), 11 deletions(-)

diff --git a/providers/common/ai/docs/durable_execution.rst 
b/providers/common/ai/docs/durable_execution.rst
index 1e485079fa2..01e6dc07408 100644
--- a/providers/common/ai/docs/durable_execution.rst
+++ b/providers/common/ai/docs/durable_execution.rst
@@ -107,7 +107,7 @@ So does a callable that has no ``__qualname__`` to name it 
by, such as a
 
 .. code-block:: text
 
-    ValueError: durable=True names the steps of a toolset built per run after 
the function that builds it, but functools.partial(<function carrier_tools at 
0xffff89c904a0>, carrier='dhl') has no __qualname__. Pass it as 
DynamicToolset(function, id=...).
+    ValueError: durable=True names the steps of a toolset built per run after 
the function that builds it, but functools.partial(<function carrier_tools at 
0x...>, carrier='dhl') has no __qualname__. Pass it as DynamicToolset(function, 
id=...).
 
 Two different toolsets with the same id are refused, since their calls would 
be filed
 under one name, and so are two functions with the same qualified name, such as 
two
diff --git 
a/providers/common/ai/src/airflow/providers/common/ai/durable/capability.py 
b/providers/common/ai/src/airflow/providers/common/ai/durable/capability.py
index f3adf01aa08..0433ef47f11 100644
--- a/providers/common/ai/src/airflow/providers/common/ai/durable/capability.py
+++ b/providers/common/ai/src/airflow/providers/common/ai/durable/capability.py
@@ -105,7 +105,7 @@ else:
 _DAG_MODULE_PREFIX = re.compile(r"^unusual_prefix_[0-9a-f]{40}_")
 
 
-def callable_toolset_id(function: object) -> str | None:
+def get_callable_toolset_id(function: object) -> str | None:
     """
     Return the id of a toolset that ``function`` builds per run: its module 
and qualified name.
 
@@ -157,12 +157,12 @@ def give_durable_ids(
     it has an id. An id-less ``FunctionToolset`` gets one numbered in the 
order the toolsets
     are given, which its step names leave out (a function tool's step is named 
after the
     tool); a function that builds a toolset per run, or an id-less 
``DynamicToolset``, gets
-    :func:`callable_toolset_id`. A leaf that already has an id, and an 
``MCPToolset``, are
+    :func:`get_callable_toolset_id`. A leaf that already has an id, and an 
``MCPToolset``, are
     left as they are. Leaves are copied, not changed, and ``given`` maps each 
leaf to its
     copy so a toolset listed twice stays one toolset to pydantic-ai.
     """
     if not isinstance(toolset, AbstractToolset):
-        return DynamicToolset(toolset, id=callable_toolset_id(toolset))
+        return DynamicToolset(toolset, id=get_callable_toolset_id(toolset))
     if not any(_lacks_id(leaf) for leaf in iter_toolsets(toolset)):
         # Rebuilding wrappers via visit_and_replace breaks wrapper subclasses 
with their own __init__.
         return toolset
@@ -176,7 +176,7 @@ def give_durable_ids(
                 copied = DynamicToolset(
                     leaf.toolset_func,
                     per_run_step=leaf.per_run_step,
-                    id=callable_toolset_id(leaf.toolset_func),
+                    id=get_callable_toolset_id(leaf.toolset_func),
                 )
             else:
                 numbered = sum(isinstance(other, FunctionToolset) for _, other 
in given.values())
@@ -508,13 +508,14 @@ class _AirflowOperationNamer(JournalOperationNamer):
     A function tool's step name ends in the tool's name, so the id Airflow 
numbered the
     toolset by adds nothing to it and would only make the name harder to read 
in the task
     log. An id the Dag author chose stays in the name, so steps recorded under 
it before
-    this naming existed replay as they did.
+    this naming existed replay as they did. Only a tool call is journaled 
under its name:
+    ``execute`` runs argument validation live.
     """
 
     def operation_name(self, operation_id: DurableOperationId) -> str:
         name = super().operation_name(operation_id)
         if (
-            isinstance(operation_id, ToolsetCallToolId | 
ToolsetValidateToolArgumentsId)
+            isinstance(operation_id, ToolsetCallToolId)
             and operation_id.toolset_kind == "function"
             and operation_id.toolset_id.startswith(_ASSIGNED_ID_PREFIX)
         ):
diff --git 
a/providers/common/ai/tests/unit/common/ai/durable/test_capability.py 
b/providers/common/ai/tests/unit/common/ai/durable/test_capability.py
index e951c59bf43..482ca00604b 100644
--- a/providers/common/ai/tests/unit/common/ai/durable/test_capability.py
+++ b/providers/common/ai/tests/unit/common/ai/durable/test_capability.py
@@ -55,7 +55,7 @@ from airflow.providers.common.ai.durable.base import RUNS_KEY
 from airflow.providers.common.ai.durable.capability import (
     GivenIds,
     _JournalCodec,
-    callable_toolset_id,
+    get_callable_toolset_id,
     give_durable_ids,
 )
 from airflow.providers.common.ai.durable.journal import DurableJournal, 
Unrecordable, journal_scope
@@ -1205,6 +1205,11 @@ class TestToolsetIds:
         assert self.tool_step_names(memory_storage) == 
["analyst__function_toolset__db.call_tool:query"]
 
     def test_gives_ids_to_copies_and_one_copy_to_a_toolset_listed_twice(self):
+        """
+        ``give_durable_ids`` writes ``FunctionToolset``'s private ``_id``, 
which only its constructor sets.
+
+        This test and the replay tests above fail if pydantic-ai stops reading 
the id from there.
+        """
         shared = FunctionToolset()
         given: GivenIds = {}
 
@@ -1241,14 +1246,14 @@ class TestToolsetIds:
         assert (given.id, given.per_run_step) == (f"{__name__}._build_tools", 
False)
 
 
-class TestCallableToolsetId:
+class TestGetCallableToolsetId:
     def test_leaves_out_the_path_hash_airflow_puts_in_a_dag_files_module(self):
         def build_tools(ctx: RunContext[None]) -> FunctionToolset[None]:
             return FunctionToolset()
 
         build_tools.__module__ = f"unusual_prefix_{'0123456789abcdef' * 
2}01234567_orders_dag"
 
-        assert callable_toolset_id(build_tools) == 
f"orders_dag.{build_tools.__qualname__}"
+        assert get_callable_toolset_id(build_tools) == 
f"orders_dag.{build_tools.__qualname__}"
 
     @pytest.mark.parametrize(
         "function",
@@ -1258,7 +1263,7 @@ class TestCallableToolsetId:
         ],
     )
     def test_none_for_a_callable_without_a_qualified_name(self, function):
-        assert callable_toolset_id(function) is None
+        assert get_callable_toolset_id(function) is None
 
 
 class Answer(BaseModel):

Reply via email to