Task Cancellation¶
Subagents for Pydantic AI provides two cancellation mechanisms: soft cancel and hard cancel.
Soft Cancellation¶
Soft cancel requests cooperative cancellation. The subagent receives a signal and should stop gracefully.
How It Works¶
- Parent calls
soft_cancel_task() - A
CANCEL_REQUESTmessage is sent to the subagent - Subagent should check for cancellation and stop gracefully
- Subagent returns partial results or cancellation acknowledgment
- Task status becomes
CANCELLED
When to Use¶
- Task is taking too long
- Requirements have changed
- Parent wants to try a different approach
- Graceful shutdown needed
Hard Cancellation¶
Hard cancel forces immediate termination.
How It Works¶
- Parent calls
hard_cancel_task() - Task is immediately terminated
- No cleanup or partial results
- Task status becomes
CANCELLED
When to Use¶
- Soft cancel didn't work
- Urgent need to stop
- Task is stuck or unresponsive
- Resource constraints
Use with Caution
Hard cancellation may leave resources in an inconsistent state. Prefer soft cancellation when possible.
Implementing Cancellation Handling¶
Guide subagents to handle cancellation gracefully:
SubAgentConfig(
name="researcher",
instructions="""You are a research assistant.
## Cancellation Handling
If you receive a cancellation request:
1. Stop your current work
2. Save any partial results
3. Return what you have with a note that work was cancelled
Example response for cancelled task:
"Research was cancelled. Partial findings:
- Found 3 relevant sources
- Initial analysis suggests [...]
- Further investigation was not completed"
""",
)
Cancellation States¶
| State | Description |
|---|---|
PENDING |
Can be cancelled (never started) |
RUNNING |
Can be cancelled (soft or hard) |
WAITING_FOR_ANSWER |
Can be cancelled |
COMPLETED |
Cannot be cancelled (already done) |
FAILED |
Cannot be cancelled (already done) |
CANCELLED |
Already cancelled |
Example: Cancellation Workflow¶
# Start a long-running task
task(
description="Comprehensive market research",
subagent_type="researcher",
mode="async",
)
# Returns: "Task started with ID: research-123"
# Check progress
check_task(task_id="research-123")
# Returns: "Task is running"
# Decide to cancel
soft_cancel_task(task_id="research-123")
# Returns: "Cancellation requested for task research-123"
# Check final status
check_task(task_id="research-123")
# Returns: "Task cancelled. Partial results: [...]"
Handling Multiple Tasks¶
When cancelling multiple tasks:
# List active tasks
list_active_tasks()
# Returns list of task IDs and statuses
# Cancel specific tasks
soft_cancel_task(task_id="task-a")
soft_cancel_task(task_id="task-b")
# Or hard cancel if needed
hard_cancel_task(task_id="task-c")
Best Practices¶
1. Prefer Soft Cancellation¶
Always try soft cancel first:
# First attempt
soft_cancel_task(task_id="abc123")
# Wait and check
check_task(task_id="abc123")
# Only if still running after reasonable time
hard_cancel_task(task_id="abc123")
2. Design for Cancellation¶
Structure subagent work so it can be stopped at checkpoints:
instructions="""
Work in phases:
1. Gather sources (checkpoint)
2. Initial analysis (checkpoint)
3. Deep dive (checkpoint)
4. Synthesis
At each checkpoint, check for cancellation and save progress.
If cancelled, return results from completed phases.
"""
3. Communicate Cancellation Clearly¶
Return useful information when cancelled:
instructions="""
If cancelled, return:
- What was completed
- What was in progress
- What wasn't started
- Any preliminary findings
"""
When the parent run ends¶
SubAgentCapability cancels every background task the run started, in a
finalizer, so nothing outlives its parent: a delegation still working against
deps the application has torn down is a bug, and one blocked in ask_parent
waits for an answer that can never arrive.
That cleanup waits for each cancelled task to unwind, but only up to
cancel_grace_seconds (5 s by default). The bound matters because the wait
happens in a finally. A CancelledError can be caught, and a subagent's
toolset is arbitrary code -- an HTTP client with its own shield, a slow cleanup,
a contextlib.suppress a little too wide. An unbounded wait on one of those
never returns, so the parent run's teardown never returns either, and the
application hangs with nothing logged.
A task still alive after the grace period is logged with its task id and left to
the event loop. A leaked task that is reported is recoverable; a finally that
never finishes is not.
SubAgentCapability(default_model="openai:gpt-4.1", subagents=subagents, cancel_grace_seconds=30.0)
Raise it when subagents do real cleanup on the way out; lower it when a fast shutdown matters more than a graceful one. The guarantee either way is that every task is cancelled -- the setting only changes how long the parent waits to watch it happen.
Next Steps¶
- Dynamic Agents - Runtime agent creation
- Message Bus - Communication layer