Tool call logging

Note

Experimental: the constructor and the behaviour of ToolLoggingCapability can change or be removed in a minor release of this provider. See Stable and experimental features.

ToolLoggingCapability wraps the assembled function toolset so that each tool invocation is logged in real time. Logging covers tools supplied through toolsets=, agent_params={"tools": [...]}, and capabilities such as factory-backed toolsets, nested capabilities, and MCP toolsets. Output tools such as final_result and provider-native tools, including native MCP, are not covered by Airflow’s real-time tool-call logging. Tools added by another capability through its own wrapper toolset, such as ToolSearch’s search_tools and CodeMode’s run_code, are also not covered. A tool hidden with .defer_loading() is logged once the model finds it, and its result is masked like any other (see Progressive tool disclosure).

AgentOperator adds ToolLoggingCapability automatically when enable_tool_logging=True. If capabilities= already contains one, including inside a combined or wrapper capability, the operator does not add another, so the logger on the supplied instance is used.

For an agent you build yourself, pass the capability to any pydantic-ai Agent:

from pydantic_ai import Agent

from airflow.providers.common.ai.toolsets.logging import ToolLoggingCapability
from airflow.providers.common.ai.toolsets.sql import SQLToolset

agent = Agent(
    "openai:gpt-5",
    toolsets=[SQLToolset(db_conn_id="my_db")],
    capabilities=[ToolLoggingCapability()],
)

Each tool call logs its name and timing at INFO, inside a collapsible ::group:: block in the task log, and its arguments at DEBUG. Control-flow signals that let the model retry, report a failed tool result, defer or skip a call, or wait for approval are logged at INFO and re-raised. Other exceptions, including SkipToolValidation raised by a tool body, are logged at ERROR with a traceback and re-raised. Pass logger to ToolLoggingCapability to send the lines to a logger other than the module’s own.

Was this entry helpful?