Repository navigation
Conversation
dabla
marked this pull request as ready for review
October 6, 2026 08:16
dabla
requested review from
Lee-W,
ashb,
gopidesupavan and
kaxil
as code owners
October 6, 2026 08:16
dabla
marked this pull request as draft
October 6, 2026 08:16
dabla
force-pushed
the
dabla/agent-async/01-task-agent-aexecute
branch
from
October 14, 2026 21:42
f55607b to
d091857
Compare
get_conn() resolves the hook's connection and every fallback connection with get_connection() and reads extra_dejson, whose secret masking sends to the supervisor synchronously; inside an async task with another async SDK call in flight both raise DeadlockImminentError, although the agent the hook builds is async-native. aget_conn() fetches the connection and its fallbacks with get_async_connection() (extra from Connection.aextra_dejson(), #71890, Airflow 3.3.2+, or the raw extra via json.loads on older versions as in #72130), seeds the existing connection cache and builds the model with the unchanged get_conn(); acreate_agent() does the same before create_agent(), spec_file included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the local aextra_dejson fallback with the common.compat helper added in #74147, which masks the extra's secrets asynchronously on Airflow 3.3.2+ and reads extra_dejson from a worker thread on older versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Prefetching the connections only covered the base hook's own lookups: a subclass's _get_provider_kwargs (Snowflake's builds a SnowflakeHook) still made synchronous Task SDK calls on the event loop, and on Airflow 3.1 to 3.3.1 get_async_connection itself reads extra_dejson synchronously. aget_conn() and acreate_agent() now run the unchanged get_conn() / create_agent() in asyncio.to_thread: from any thread other than the loop's, a synchronous send() waits for the in-flight asend() to release the comms lock instead of raising DeadlockImminentError. This drops the prefetch, the prefetched fallback cache and the extra parameter of _seed_connection. aget_hook() is the async counterpart of get_hook(), with the same connection type check, so an async operator can dispatch to the pydanticai* subclasses. get_hook() now caches the connection it fetched on the hook it returns, so the hook's first get_conn() does not fetch it again. The tests refuse the synchronous lookups only on the event loop, drive a subclass that reads another connection as primary and as fallback, and check that every argument of acreate_agent reaches the agent. The hook docs gain a section on using it from an async task, and uv.lock is back to main's.
`@task.agent` could not be used with an `async def` function: the function was called without being awaited, so the task failed on the coroutine it got back as its prompt, while the operator already reported `is_async` through `DecoratedOperator`. An agent run could therefore not await async hooks to build its prompt, nor share the event loop of an async task with other work, although the pydantic-ai agent behind it is async-native. `AgentOperator` is now a `BaseAsyncOperator` with an `aexecute()` that awaits the agent run on the event loop and makes no blocking call to the supervisor on it: the usage budget, the tool approval transcript and the XComs of a successful run go through their async accessors. What has no async counterpart (the connection lookup, the task state store before Airflow 3.4, the usage report of a failed run, the pause for a tool approval) runs the synchronous code in a worker thread. `durable=True` and `enable_hitl_review=True` do blocking I/O during the run and are rejected with an async function for now.
dabla
force-pushed
the
dabla/agent-async/01-task-agent-aexecute
branch
from
October 18, 2026 09:57
d091857 to
df446fb
Compare
This branch has not been deployed
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.
@task.agentcannot be used with anasync deffunction today. The decorator calls the function without awaiting it, so the task fails invalidate_prompton the coroutine it gets back, while the operator already reportsis_asyncthroughDecoratedOperator. An agent task therefore cannot await async hooks to build its prompt, and cannot share the event loop of an async task with other work, although the pydantic-ai agent behind it is async-native.This makes
AgentOperatoraBaseAsyncOperator, the wayPythonOperatoris one:@task.agenton anasync deffunction awaits it for the prompt and runs the agent on the task's event loop. A regular function keeps the synchronous path unchanged.AgentOperator.is_asyncisFalse; on the decorator it follows the function.AgentOperator.aexecute()awaits the agent run on the event loop (await agent.run()throughCancellableAgentRunMixin.run_agent_async, same cancellation token as the sync run) and makes no blocking call to the supervisor on it: the hook comes fromPydanticAIHook.aget_hook()and the agent fromacreate_agent()(Add async aget_conn and acreate_agent to PydanticAIHook #74150), and the usage budget (aload/asave/aclear), the tool approval transcript and the XComs go through their async accessors.aget_hook(), the task state store before Airflow 3.4 (its async accessors are on main only, Add async task store accessors for async tasks #68232), the usage report of a failed run and the pause for a tool approval. Resuming after an approval stays synchronous.durable=Trueandenable_hitl_review=Truedo blocking I/O during the run (the durable storage inside the model and tool calls, the review poll loop) and are rejected with anasync deffunction, at parse time on the decorator. They can follow in a later PR.axcom_pushexists since Airflow 3.3.0; on 3.2 the XComs are pushed withxcom_pushfrom a worker thread.What it changes for performance. One agent run is not faster, its duration is the model's. The gain is in running many of them in one task process. Measured with pydantic-ai alone and a fake model that sleeps 1 s, one run per cell: a thread pool calling
run_syncversus one event loop awaitingrun.run_syncrunTests. New tests cover
run_agent_async(token held and cleared,on_killcancelling an awaited run), the async usage budget methods and their fallback to the sync store before Airflow 3.4,UsageBudgetMixin._arun_tracked,aexecutewith mocks and with a realFunctionModelrun where every blocking state store call andxcom_pushfails the test, the failed-run and tool-approval branches, the rejected features, the decorator with anasync deffunction through bothaexecuteandexecute, rendering off the event loop, and eight runs overlapping on one loop.Run locally: the whole
providers/common/ai/tests/unitsuite (3104 passed, 13 skipped), and after the last round the touched modules again,mypyon the changed source files, andprekon the changed files. A backport of the same design to common.ai 0.9 also ran on a Celery worker with Airflow 3.3.2: one async@task.agenttask, and eight agent runs sharing one event loop inside a single task, against an Azure OpenAI model, without aDeadlockImminentError.Was generative AI tooling used to co-author this PR?
Generated-by: Claude Code (Fable 5.1) following the guidelines