Allow templated connection IDs in agent toolsets - #73578
Merged
Merged
Conversation
kaxil
force-pushed
the
common-ai-templated-toolsets
branch
from
September 22, 2026 22:24
991ef1b to
b7bd4e8
Compare
phanikumv
approved these changes
Sep 23, 2026
kaxil
force-pushed
the
common-ai-templated-toolsets
branch
from
September 23, 2026 10:46
b7bd4e8 to
10f085b
Compare
SQLToolset.db_conn_id, MCPToolset.mcp_conn_id and a HookToolset's hook connection ID (the attribute its conn_name_attr names) are now rendered by AgentOperator / @task.agent, so a mapped agent task can give each map index its own connection (e.g. one database per customer). Each task instance renders a copy, leaving the Dag-level toolset and hook untouched, and logs the rendered toolset id.
kaxil
force-pushed
the
common-ai-templated-toolsets
branch
from
September 23, 2026 18:56
10f085b to
0604127
Compare
kaxil
marked this pull request as ready for review
September 23, 2026 21:47
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.
The connection IDs of Common AI agent toolsets are now Jinja templates, rendered for each task instance just before it runs. One toolset definition can then reach a different system depending on the run:
SQLToolset(db_conn_id="warehouse_{{ var.value.environment }}").@task.agentgives each map index its own connection, e.g. one per customer for customer-facing analytics, where each customer's rows sit behind their own database role:Until now toolsets took their connection when the Dag was parsed.
@task.llm_sqlalready templatesdb_conn_id, but an agent's toolsets could not, so every mapped agent shared one connection.What is templated:
SQLToolset.db_conn_id,MCPToolset.mcp_conn_id, and aHookToolset's hook connection ID.HookToolsetreads the attribute the hook'sconn_name_attrnames (postgres_conn_id, ...), and falls back toconn_idfor hooks such asWasbHookthat keep it there. Only connection IDs are templated.Design rationale
Why not add
toolsetstoAgentOperator.template_fields? Template fields are serialized into the Dag. A toolset's repr is what gets serialized, and pydantic-ai's wrappers (.prefixed(),.filtered()) are dataclasses whose repr can embed function addresses, so the Dag hash would change on every parse.toolsetsstays out oftemplate_fields, and the operator renders the toolsets itself.Each task instance renders a copy.
MappedOperator.unmappasses the partial's toolset objects straight to every unmapped task, anddag.test()runs every task in one process. Rendering in place would hand map index 0's connection to map index 1. Each opt-in leaf toolset is copied before rendering (forHookToolset, the hook too). Wrappers andToolsetcapabilities are walked with pydantic-ai'svisit_and_replace, and only when something in them is templated, so an untemplated custom wrapper is never rebuilt.The opt-in attribute is
agent_template_fields, nottemplate_fields. Airflow's templater renders any object that carriestemplate_fieldsin place, wherever it is nested inside another template field.agent_paramsis a template field, andagent_params["toolsets"]is a supported way to pass toolsets, so the familiar name would bring the leak back through that path. Third-party toolsets opt in by declaring the same attribute.Rendering hangs off
_do_render_template_fields. A mapped task never callsrender_template_fields:MappedOperatorrenders the unmapped task through_do_render_template_fields.KubernetesPodOperatorhooks the same method for the same reason.The rendered connection is not recorded anywhere else, so each task instance logs it once, e.g.
Rendered toolset sql-analytics_acme.Screenshots
A two-customer demo on a real scheduler and API server, each customer with its own SQLite connection, and a
testmodel that calls every tool. Map index 0 renders theacmeconnection for both the SQL toolset and the hook toolset; map index 1 rendersglobex:Each agent's tool results come from its own customer's database:
Gotchas
paramsordag_run.conf. A task can read any connection it names, so a template driven by trigger input lets whoever triggers the Dag pick the database (for an MCPstdioconnection, the command that runs on the worker). The docs say so next to each example.{{ customer }}does not work: the task's arguments are not template variables. Use{{ task.op_kwargs.customer }}. WithAgentOperator.partial(...).expand(prompt=...), the connection has to come from the map index; the docs show that form and its ordering caveat.HookToolset.idnow includes the connection ID (hook-PostgresHook-analytics_acme, previouslyhook-PostgresHook). The toolset id is part of the durable-execution step fingerprint, so adurable=Truetask that retries across the upgrade misses its cache once.allowed_tables(validated when the toolset is created, so a template stays a literal),DataFusionToolset, aToolsetcapability built from a callable, and hooks that keep their connection ID under some other attribute. A hook that looks its connection up in__init__fails at Dag parse time, because the template is not a connection ID yet.{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.