AI agent cancellation becomes important when a customer changes their mind while your bot is still working. They say “cancel”, but the agent has already started checking stock or preparing an order. Stopping the reply text doesn’t tell you whether those other actions stopped too.
The customer wants a simple outcome. Your backend needs to know which work can stop, which work has committed and which result is still unknown. Let’s introduce those distinctions one at a time.
Start with a task you can identify
Give each customer request a task ID and an intent version. Keep its current stage in durable storage. A cancellation updates that task rather than merely appending the word “cancel” to the chat history.
When new intent replaces old intent, increment the version. Tool requests carry the version they were prepared under. Before a new action starts, the action service compares that version with current intent.
This doesn’t require a complex agent framework. It requires a place where the application, rather than the model’s next sentence, decides whether the task may continue.
Separate preparation from commitment
Searching a catalogue is usually preparation. Creating a paid order is a commitment. Put an explicit checkpoint between them.
Before commitment, verify that the task is still active, the proposal is current and any required customer confirmation applies to this exact proposal. A confirmation for two items isn’t permission to buy three after a later edit.
Store the confirmation’s proposal version, amount and relevant limits. Don’t let a model-generated boolean such as userApproved: true replace that evidence.
Build the checkpoint
This local example cancels one task before commitment and another after commitment. The expected outcomes are deliberately different.
import assert from 'node:assert/strict';
function task() { return { version: 1, state: 'preparing', order: null }; }
function cancel(t) {
t.version++;
t.state = t.order ? 'reversal_requested' : 'cancelled';
}
function commit(t, expectedVersion) {
if (t.version !== expectedVersion || t.state !== 'preparing') return false;
t.order = 'order-1'; t.state = 'committed'; return true;
}
const before = task(), version = before.version;
cancel(before);
assert.equal(commit(before, version), false);
assert.equal(before.order, null);
const after = task();
assert.equal(commit(after, after.version), true);
cancel(after);
assert.equal(after.state, 'reversal_requested');
assert.equal(after.order, 'order-1');
console.log('PASS: pre-commit cancellation and post-commit reversal are distinct');
The first task never creates the order. The second already has an order, so the application records a request to reverse or change it. Telling both customers “cancelled” would hide an important difference.
The external side-effect boundary still needs careful handling. The in-memory example makes a local commit in one turn. A real payment or order service can have an uncertain network outcome.
Abort work that supports cancellation
Pass an abort signal through local reads and cooperating HTTP calls. That can save time and resources after intent changes. Node documents AbortController and AbortSignal for this purpose. Node cancellation reference.
Treat an abort as a request to stop, not proof that every external service has stopped. A remote job may continue. If the provider gives you an operation ID, store it and use the provider’s supported cancellation or status endpoint.
Generation cancellation and action cancellation should have separate metrics. You may successfully stop the model from producing text while an earlier order submission is still being reconciled.
Record uncertainty instead of guessing
Suppose an order request times out at the same moment the customer cancels. Don’t create a new order to discover whether the first one worked. Record the original operation ID, keep the state unresolved and query the authoritative service.
Once the result is known, apply the relevant policy. If the order was created, request cancellation against that order. If it wasn’t created, finish the task as cancelled before commitment.
Use idempotent operation keys where the business service supports them, but follow that service’s exact contract. A local task version doesn’t automatically make an external API retry safe.
Structure storage around the transition
A task row can hold task_id, conversation_id, intent_version, stage, cancel_requested_at and proposal_version. An action row holds the stable operation ID, provider reference and outcome.
Update a task using the version you read. If another request changed it, reload rather than overwriting the cancellation. Keep action history separate so the current task status doesn’t erase evidence that something actually happened.
For long tasks, workers need leases and checkpoints. After a restart, resume from recorded state. Don’t blindly rerun the last tool call because the model transcript ends before its result.
Tell the customer what is true now
Before commitment, a clear “I’ve stopped that request” is appropriate once the system has prevented further actions. After commitment, explain that you’re checking whether the order can still be cancelled. If the outcome is unknown, say that you’re confirming it.
You don’t need to expose internal state names to the customer. You do need response text that corresponds to the actual state rather than a promise the application hasn’t fulfilled.
Test cancellation at the boundaries
Cancel before a tool starts, during a read, before commitment, during submission and after success. Restart the worker in the uncertain state and check that it resumes reconciliation rather than creating another action.
Also test an old worker waking after a newer request has replaced the task. Its version should fail the checkpoint even if the current task is active again.
Start with one consequential tool in your agent. Put a versioned checkpoint immediately before it, and make cancellation outcomes explicit. That gives you a reliable foundation before adding more tools.
Run the example and adapt it
Save the code as 07-interruptible-ai-agent.mjs and run it with Node.js 24 or later:
node 07-interruptible-ai-agent.mjs
The code above includes assertions you can run locally. It tests the local decision logic with controlled inputs; it doesn’t connect to a live provider or establish production performance. Keep that boundary when adapting it to your application.

