Skip to content

Cancel the agent run when a common.ai LLM or agent task is killed - #73495

Merged
pankajkoti merged 2 commits into
apache:mainfrom
astronomer:cancel-agent-run-when-task-killed
Sep 23, 2026
Merged

pankajkoti merged 2 commits into
apache:mainfrom
astronomer:cancel-agent-run-when-task-killed

Conversation

@pankajkoti

@pankajkoti pankajkoti commented Sep 21, 2026

Copy link
Copy Markdown
Member

Killing a running common.ai task (LLMOperator, AgentOperator, or an operator deriving
from them) used to absorb the SIGTERM grace window. No operator implemented on_kill, so
the pydantic-ai run kept issuing model requests and its toolsets never unwound, leaving a
provisioned sandbox running until SIGKILL, where it leaked.

The operators now hold the run's pydantic-ai CancellationToken and cancel it from
on_kill, so a killed run stops and tears down cleanly within the grace window instead of
running dead until SIGKILL. The cancel is issued from a short-lived thread, because
on_kill runs in the Task SDK's SIGTERM handler on the same thread that drives run_sync,
where an inline cancel does not interrupt the blocked run (it only takes effect once the
in-flight await returns). The run-cancellation APIs (CancellationToken and RunCancelled)
need pydantic-ai 2.26.0. The provider already requires 2.33.0 or newer, so this change
carries no dependency bump.

Cancellation relies on the Task SDK calling on_kill on SIGTERM, which is Airflow 3.0.4+
and 3.1+. On 3.0.0 to 3.0.3 a kill falls back to the pre-existing behaviour (the run
continues until SIGKILL).

This does not change the SIGKILL case. A task killed with SIGKILL runs no Python, so a
sandbox killed that way still depends on the backend's own backstop.

Test plan

Manual end-to-end (not covered by CI): on a breeze start-airflow stack, trigger a
common.ai agent/LLM task and, while it is running, mark it failed or clear it from the UI.
The task log shows on_kill firing and the run raising RunCancelled within the grace
window. Verified for AgentOperator (live UI) and LLMOperator (RunCancelled ~1ms after
on_kill, versus running dead to SIGKILL without the change).


Was generative AI tooling used to co-author this PR?
  • Yes, Claude Code (Opus 4.8)

Generated-by: Claude Code (Opus 4.8) following the guidelines

🤖 Generated with Claude Code

No common.ai operator implemented on_kill, so killing a running task absorbed the
SIGTERM grace window. The pydantic-ai run carried on issuing model requests and its
toolsets never unwound, leaving a provisioned sandbox running until SIGKILL, where it
leaked. Killing a task should stop the work promptly and let it tear down cleanly.

The operators now hold the run's CancellationToken and cancel it from on_kill, from a
separate thread so the cancel actually interrupts a run driven by run_sync on the
signal-handler thread. Requires pydantic-ai 2.26.0 for first-party run cancellation.
@pankajkoti
pankajkoti force-pushed the cancel-agent-run-when-task-killed branch from 112a455 to 6f76fb0 Compare September 22, 2026 12:09
@pankajkoti

Copy link
Copy Markdown
Member Author

On kill logs
Screenshot 2026-09-22 at 4 51 19 PM

@pankajkoti
pankajkoti marked this pull request as ready for review September 22, 2026 13:32
Comment thread providers/common/ai/src/airflow/providers/common/ai/mixins/cancellable_run.py Outdated
Comment thread providers/common/ai/src/airflow/providers/common/ai/operators/agent.py Outdated

self._cancellation_token = CancellationToken()
try:
return agent.run_sync(user_prompt, cancellation_token=self._cancellation_token, **run_kwargs)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One consequence to consider: RunCancelled is a RuntimeError, so once it propagates out of execute the runner routes it through _handle_current_task_failed and the task's retry policy. With LLMRetryPolicy that means a fresh, uncancellable agent.run_sync classification call inside whatever is left of the 5 s grace window, and a rules policy could classify the cancel as retryable. Sandbox teardown has already finished by then, so the PR's goal holds either way. Is that acceptable, or should the kill map to something the runner treats as terminal without consulting the policy?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed: RunCancelled is a RuntimeError, so it hits the final except BaseException in _run_task_and_map_outcome and flows through _handle_current_task_failed into the retry policy, exactly as you describe.

My inclination here, and I'd welcome your view, is to leave it flowing through the policy rather than mapping the kill to a terminal exception. My thinking is that mapping it terminal (AirflowFailException / AirflowTaskTerminated) would skip retries entirely, which I worry would regress the standard "evicted worker retries the task" behaviour, and retry-vs-terminal feels like the retry policy's call rather than something the mixin should force. On the grace-window cost, as far as I can tell, if the classifier outlasts the window it gets SIGKILLed mid-call and retry falls back to the default count (the pre-fix behaviour), so I don't think it regresses there.

If you'd prefer kills to skip the LLM classifier, I think the cleanest place is retry.py (short-circuiting RunCancelled to the fallback) rather than this PR, which doesn't touch retry.py. I'm happy to take that on in a follow-up PR whenever you'd like, and of course happy to go whichever way you think is best.

Comment thread providers/common/ai/tests/unit/common/ai/mixins/test_cancellable_run.py Outdated
- Move the CancellationToken import to module scope. pydantic-ai is already
  imported at module top through PydanticAIHook, so the lazy import saved nothing.
- Drop the cancel-path partial-transcript push. A retry clears the task
  instance's XCom before it starts, so nothing consumes it. A kill now just
  propagates RunCancelled to fail the task.
- Document the Task SDK floor in the mixin docstring: on_kill is called on
  SIGTERM from Airflow 3.0.4 and 3.1.0 onward, below which a kill falls back
  to the pre-existing behaviour.
- Qualify the sbx leak note in agent_security.rst to SIGKILL.
- Assert the test forwards the exact held cancellation token, not just any token.
@pankajkoti
pankajkoti merged commit 04e285b into apache:main Sep 23, 2026
156 of 157 checks passed
@pankajkoti
pankajkoti deleted the cancel-agent-run-when-task-killed branch September 23, 2026 07:12
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