Repository navigation
Add Strands Agents support for common.ai toolsets - #73587
Merged
Merged
Conversation
Give SQLToolset and HookToolset tools to a native Strands agent with as_strands_tools(), built on a small framework-neutral tool interface (AirflowTool, ToolResult, ToolProvider.airflow_tools()) that applies Airflow's secret masker to every tool result and error before the model sees it. The interface is experimental.
List which agent frameworks the provider supports and what it provides for each (Pydantic AI, Strands Agents, LangChain, LlamaIndex), and move the Strands guide under it.
kaxil
force-pushed
the
commonai-strands-tools
branch
from
September 23, 2026 18:41
dfe5ca6 to
bae8856
Compare
The tool tests imported the Task SDK secret masker at module level, and it does not exist on Airflow 3.0, so collection failed in the 3.0 compat job. A version-gated register_secret fixture in conftest now registers secrets, and the tests that need one are skipped before Airflow 3.1. Co-Authored-By: Claude <noreply@anthropic.com>
vatsrahul1001
approved these changes
Sep 28, 2026
kaxil
marked this pull request as ready for review
September 28, 2026 17:27
kaxil
added a commit
that referenced
this pull request
Sep 30, 2026
…3898) The Strands adapter from #73587 built plain function tools, and Strands hands any exception such a tool raises back to the model, so a tool that kept raising ModelRetry looped without end. AirflowTools is now a Strands plugin whose AfterToolCallEvent hook re-raises ToolCallError out of the run, and failures the model can correct are bounded by the tool's max_retries: counted once per tool per model turn and afresh on every run, as pydantic-ai counts them. Tools a toolset marks sequential, such as the sandbox's, run one at a time in the order the model called them. AirflowTools in tools.adk gives an ADK agent the same tools. airflow_tools() moves to the shared toolset base, collect_tools() flattens what an adapter is given and refuses duplicate tool names, and the LangChain bridge runs on the same interface. SandboxToolset can own its sandbox from synchronous code with a with block, and MCPToolset refuses the native path, since an MCP session belongs to the Pydantic AI run that opens it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Common AI's toolsets (
SQLToolset,HookToolset, ...) are pydantic-ai toolsets, so today onlyAgentOperatorand pydantic-ai agents can use them. Teams that already build agents with Strands Agents have to rewrite the agent forAgentOperatoror hand-write tools around hooks and lose the toolsets' SQL validation, table allowlists and bounded results.This adds
as_strands_tools(), which turns those toolsets into Strands tools. The user keeps a plain StrandsAgent, with their own model, loop and Strands features, and Airflow supplies connection-backed tools:It sits on a small framework-neutral interface in
airflow.providers.common.ai.tools:AirflowTool(name, description, JSON Schema, async call),ToolResult(JSON-compatible content plus an explicitis_error), andToolProvider.airflow_tools(), whichSQLToolsetandHookToolsetnow implement. Both the interface and the adapter are marked experimental. Strands is an optional dependency imported only by the adapter module, and nothing changes forAgentOperatorusers.The docs gain an Agent frameworks guide next to Toolsets. It lists each framework the provider supports and what it does for each one: for Pydantic AI the operators run the agent, while for Strands, LangChain and LlamaIndex the user runs the agent and the provider supplies tools, models or operators. The Strands guide sits under it, and the page ends with how to use a framework that is not listed.
Design rationale
Why not a
StrandsAgentOperator? An operator would own the agent loop, and every Strands feature would then need an Airflow release to reach users. The adapter only converts tools, so the agent stays native and Strands releases reach users without an Airflow change.Masking lives in the one path every adapter shares. Airflow's secret masker protects task logs, not tool results. A Strands tool whose client library puts a credentialed URL in an error message sends the raw password to the model provider, into the agent's answer and into its traces, while the task log shows
***. I reproduced this with a native Strands@toolin a scheduler-run task.AirflowTool.callturns exceptions into error results and runsredact()on every result and error before anything reaches the framework, matching values in nested JSON up to 32 levels deep rather than the masker's default of 5. An adapter for another framework only maps the three types.Why an explicit
airflow_tools()per toolset instead of accepting anyAbstractToolset? The bridge calls the toolset'scall_toolwith an inertRunContext, as the existing LangChain bridge does. That is only correct for toolsets that ignore the context, whichSQLToolsetandHookToolsetdo. Opting toolsets in one at a time keeps the contract true.MCPToolsetwould reconnect on every call, and a custom toolset that readsctx.modelorctx.messageswould misbehave.Calls into one toolset instance are serialised. Both toolsets share one hook doing blocking I/O, and
SQLToolsetreadshook.last_descriptionafter each query, which is why their pydantic-ai tool definitions are markedsequential. Agent frameworks can run tool calls concurrently, and several agents can share one toolset, so the bridge keeps one lock per toolset instance and runs the hook call off the framework's event loop.The adapter copies the input schema. Strands fills in missing property types and descriptions in place when it registers a tool, and the source schema is a module-level constant the pydantic-ai path also uses.
The LangChain bridge's private coroutine helper moves to
utils/coroutines.pyso both bridges share it. It is unchanged.The example Dag in this PR ran unmodified as a scheduler-run task on Airflow main with Postgres against
claude-sonnet-5. The agent calledlist_tables, thenquery, and returned the correct row counts in 7.5 s. The screenshot filters the log to the tool, hook and operator sources:Known issues
strands-agentspinsmcp<2.2, and adding it to the dev group downgradesmcpfrom 2.2.0 to 2.1.1 in the workspace lock, so it is not added.test_strands.pyusespytest.importorskip. Locally, withstrands-agents==1.56.0installed, all touched suites pass (172 tests).strands-agentswith no lower bound can pick the0.0.1placeholder release. The docs tell users to installstrands-agents>=1.56.0.anthropicextra requiresanthropic<1, which conflicts with this provider'santhropicextra.AnthropicModelworks withanthropic1.x at runtime, so the docs point users at this provider's extra.Follow-ups
AirflowToolso its tools are masked too.SQLToolsetreturns to pydantic-ai agents, which today carries the database's error text unmasked.{pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.