Skip to content

Add Strands Agents support for common.ai toolsets - #73587

Merged
kaxil merged 3 commits into
apache:mainfrom
astronomer:commonai-strands-tools
Sep 28, 2026
Merged

kaxil merged 3 commits into
apache:mainfrom
astronomer:commonai-strands-tools

Conversation

@kaxil

@kaxil kaxil commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Common AI's toolsets (SQLToolset, HookToolset, ...) are pydantic-ai toolsets, so today only AgentOperator and pydantic-ai agents can use them. Teams that already build agents with Strands Agents have to rewrite the agent for AgentOperator or 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 Strands Agent, with their own model, loop and Strands features, and Airflow supplies connection-backed tools:

agent = Agent(model=model, tools=as_strands_tools(SQLToolset(db_conn_id="warehouse")))

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 explicit is_error), and ToolProvider.airflow_tools(), which SQLToolset and HookToolset now implement. Both the interface and the adapter are marked experimental. Strands is an optional dependency imported only by the adapter module, and nothing changes for AgentOperator users.

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 @tool in a scheduler-run task. AirflowTool.call turns exceptions into error results and runs redact() 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 any AbstractToolset? The bridge calls the toolset's call_tool with an inert RunContext, as the existing LangChain bridge does. That is only correct for toolsets that ignore the context, which SQLToolset and HookToolset do. Opting toolsets in one at a time keeps the contract true. MCPToolset would reconnect on every call, and a custom toolset that reads ctx.model or ctx.messages would misbehave.

Calls into one toolset instance are serialised. Both toolsets share one hook doing blocking I/O, and SQLToolset reads hook.last_description after each query, which is why their pydantic-ai tool definitions are marked sequential. 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.py so 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 called list_tables, then query, and returned the correct row counts in 7.5 s. The screenshot filters the log to the tool, hook and operator sources:

Task log of example_strands_agent: tool call groups for list_tables and query, then the agent's answer

Known issues

  • The Strands adapter tests skip in CI. strands-agents pins mcp<2.2, and adding it to the dev group downgrades mcp from 2.2.0 to 2.1.1 in the workspace lock, so it is not added. test_strands.py uses pytest.importorskip. Locally, with strands-agents==1.56.0 installed, all touched suites pass (172 tests).
  • Resolving strands-agents with no lower bound can pick the 0.0.1 placeholder release. The docs tell users to install strands-agents>=1.56.0.
  • Strands' own anthropic extra requires anthropic<1, which conflicts with this provider's anthropic extra. AnthropicModel works with anthropic 1.x at runtime, so the docs point users at this provider's extra.

Follow-ups

  • Build the LangChain bridge on AirflowTool so its tools are masked too.
  • Mask the retry message SQLToolset returns to pydantic-ai agents, which today carries the database's error text unmasked.

  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {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.

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
kaxil force-pushed the commonai-strands-tools branch from dfe5ca6 to bae8856 Compare September 23, 2026 18:41
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>
@kaxil
kaxil marked this pull request as ready for review September 28, 2026 17:27
@kaxil
kaxil merged commit ce614ba into apache:main Sep 28, 2026
109 of 119 checks passed
@kaxil
kaxil deleted the commonai-strands-tools branch September 28, 2026 20:06
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants