Skip to content
Changelog

Run Claude and Codex agents in the background and cancel them mid-turn

October 12, 20262 min read

When a ClaudeAgent or CodexAgent run arrives with background=True, Agno's AgentOS writes a PENDING run, answers 202 at once and hands the SDK turn to a worker. From there the run moves to RUNNING and ends as COMPLETED, ERROR or CANCELLED on the same background task, worker slot and event-stream code that native agents use.

Picture a coding agent asked to refactor a large module. The request should return right away, the UI should show progress, and a user who changes their mind should be able to stop it halfway through. On the server, the agent needs a database, and the durable queue makes each accepted run a committed job:

from agno.agents.claude import ClaudeAgent
from agno.db.postgres import PostgresDb
from agno.os import AgentOS, QueueConfig
 
db = PostgresDb(db_url="postgresql+psycopg://ai:ai@localhost:5532/ai")
agent = ClaudeAgent(id="claude", name="Claude", db=db)
 
agent_os = AgentOS(agents=[agent], db=db, queue=QueueConfig(durable=True))
app = agent_os.get_app()

A client then sends background=true and stream=false with the run, receives its run_id and session_id, polls GET /agents/claude/runs/{run_id}?session_id={session_id}, and stops it with POST /agents/claude/runs/{run_id}/cancel. In Python, await agent.arun(..., background=True) returns the PENDING run and await agent.acancel_run(run_id) stops it.

Diagram of a background run on ClaudeAgent or CodexAgent in three rows across Client, AgentOS and the Claude or Codex SDK. First, a POST to the agent runs endpoint with background=true makes AgentOS write the run and answer 202 with status PENDING, and with the durable queue the job is a committed row; nothing runs in the SDK yet. Second, the client polls the run endpoint or streams and reconnects through /resume with last_event_index, while a worker takes a slot and starts the turn, every streamed event carries an event_index, the status is RUNNING, and the SDK side keeps a live handle per run: its own ClaudeSDKClient for Claude and the turn handle for Codex. Third, a POST to the cancel endpoint or acancel_run in Python stores the cancel intent, the process that owns the run calls interrupt() on that handle to stop the turn, and the run is saved as CANCELLED.

Cancelling a turn that is already running

Cancellation reaches the SDK itself. Each Claude run gets its own ClaudeSDKClient, and CodexAgent keeps the handle of the Codex turn, so the process that owns the run can call interrupt() on it. A cancel request stores the intent first, and the owning process acts on it, whether the call came through that process or through the shared cancellation backend.

We served a ClaudeAgent from AgentOS with the durable queue on an embedded Postgres. No Claude login was available, so a stand-in claude_agent_sdk client streamed one text block and then waited until it was interrupted. Polling, cancelling and polling again printed this (real output):

POST /agents/claude/runs background=true -> 202 {"run_id":"16eef695-8c75-48a0-9687-be689c355c1d","session_id":"s1","status":"PENDING"}
GET run -> PENDING
GET run -> 200 RUNNING
POST cancel -> 200 {}
GET run -> 200 CANCELLED | 'Run cancelled'
SDK calls: ['connect', 'query', 'interrupt', 'disconnect']

The last line is the order of calls the adapter made on the SDK client: it connected, sent the prompt, received the interrupt and disconnected. Codex works the same way through its turn handle, and an interrupted Codex turn is stored as CANCELLED.

Streaming and reconnecting

Send stream=true together with background=true and the response is an SSE stream in which every event carries an event_index. If the client drops, POST /agents/{agent_id}/runs/{run_id}/resume with the last event_index it saw sends the missed events and then continues live. Closing the stream does not cancel the run, which keeps going until it finishes or someone calls /cancel.

A background run needs a database on the agent, and arun raises a ValueError without one. Running more than one replica needs a shared event stream and cancellation backend, which QueueConfig(redis=...) sets up.

See the cookbook, and learn more about background execution in the documentation.

Frequently asked questions

Give the agent a database and pass background=True to arun, or send background=true to POST /agents/{agent_id}/runs on AgentOS. The call returns a PENDING run with a run_id right away, and you poll that run until it reaches COMPLETED, ERROR or CANCELLED.

Call POST /agents/{agent_id}/runs/{run_id}/cancel, or agent.acancel_run(run_id) in Python. Agno records the cancel intent, the process that owns the run calls interrupt() on the live Claude client or Codex turn handle, and the run is stored as CANCELLED.

Yes. Submit with stream=true and background=true, keep the event_index of the last event you received, and call POST /agents/{agent_id}/runs/{run_id}/resume with last_event_index and session_id to pick up the events you missed.

Yes. With QueueConfig(durable=True) and a Postgres or Redis store, the submission becomes a committed job row before the 202, and a queue worker runs the turn with the same background, slot and event-stream code that native agents use.

Shipped around the same time