How to Implement Abort Signals for Cancelling Long-Running Agent Operations in Flue
Every async operation in Flue returns a CallHandle that exposes an AbortSignal and an abort() method, allowing you to cancel prompt(), skill(), task(), or shell() calls via timeout signals, manual triggers, or composite controllers.
The Flue SDK (from withastro/flue) provides first-class support for aborting long-running AI agent operations through a unified cancellation API. Whether you need to enforce timeouts on expensive LLM calls or let users manually cancel an in-flight task, the SDK wraps every async operation in a cancellable handle that propagates abort signals down to underlying tools like the sandboxed shell executor.
Core Abort Signal Architecture
The cancellation system centers on three components that work together to provide abort semantics across all session methods.
The CallHandle Interface
In packages/sdk/src/types.ts, the SDK defines a CallHandle<T> interface that extends PromiseLike<T>. This design allows handles to be awaited directly while carrying additional control properties:
signal: The mergedAbortSignalthat reflects the current abort stateabort(reason?): A method to trigger cancellation programmatically
Every session method—prompt(), skill(), task(), and shell()—returns this handle type, ensuring consistent cancellation patterns regardless of the operation type.
Signal Composition in createCallHandle
The createCallHandle function in packages/sdk/src/abort.ts implements the actual wrapping logic. When you invoke a session method, the SDK:
- Creates a fresh
AbortControllerfor the internal operation - Checks if the caller provided an external signal via
options.signal - If the external signal is already aborted, immediately aborts the internal controller with the same reason
- Otherwise, registers a listener that forwards abort events from the external signal to the internal controller
This composition allows external signals (like timeouts) and manual handle.abort() calls to trigger the same cancellation pathway.
How Abort Propagation Works
When an abort triggers, the SDK transforms the raw signal into a standardized error format. The abortErrorFor utility in abort.ts generates a DOMException of type AbortError, using Object.defineProperty to attach the original abort reason to the error's cause property.
The signal propagates through the following chain:
- Session method (
session.prompt()orsession.shell()) receives the options createCallHandlemerges external and internal signals- Sandbox executor (
packages/sdk/src/sandbox.ts) receives the signal and respects it duringexec()calls, killing subprocesses if aborted - Error surfacing rejects the handle's promise with the
AbortErrorcontaining the original reason
Practical Implementation Examples
Timeout-Based Cancellation
Pass AbortSignal.timeout() to any session method to enforce automatic cancellation after a specified duration. This works with prompt(), skill(), task(), and shell() operations.
// Cancel a prompt after 2 seconds
await session.prompt(
"Run `sleep 30` via the bash tool, then describe what happened.",
{ signal: AbortSignal.timeout(2_000) },
);
When the timeout fires, the SDK rejects the promise with an AbortError. The error's cause property contains the timeout's reason (typically undefined for timeouts).
Manual Abort with CallHandle
For user-driven cancellation, retain the returned handle and call its abort() method:
const handle = session.prompt(
"Run `sleep 30` via the bash tool, then describe what happened."
);
// Cancel after 1 second from another async branch
setTimeout(() => handle.abort("user-cancel"), 1_000);
try {
await handle;
} catch (e) {
console.log(e.name, e.message, (e as any).cause);
// Output: AbortError ... "user-cancel"
}
This approach gives you programmatic control over the lifecycle of long-running agent operations without pre-defining timeouts.
Pre-Aborted Signals
If you already know an operation should cancel before it starts, pass an already-aborted signal:
await session.prompt("Say hi.", {
signal: AbortSignal.abort("already done"),
});
createCallHandle detects that the signal is already aborted and immediately triggers the internal controller, rejecting the promise without starting any work. This prevents unnecessary compute for operations invalidated by prior context changes.
Composite Signals for Complex Scenarios
Combine multiple abort conditions using AbortSignal.any() to support both timeouts and external cancellation triggers:
const external = new AbortController();
const combined = AbortSignal.any([
external.signal,
AbortSignal.timeout(3_000),
]);
await session.shell("sleep 30", { signal: combined });
// Elsewhere, manual cancellation still works
external.abort("user-requested-stop");
Handling Abort Errors
When an operation aborts, the SDK rejects the handle's promise with an AbortError. Detect these errors by checking the error name or using the cause property for custom logic:
try {
await session.task("Long computation");
} catch (err) {
if (err.name === "AbortError") {
console.log("Task aborted:", (err as any).cause);
// Handle cleanup or UI updates
}
}
The error structure ensures that aborts are distinguishable from operational failures like tool errors or LLM API exceptions, allowing you to handle cancellation gracefully without confusing it with actual execution failures.
Summary
CallHandlewraps every async operation in Flue, exposingsignalandabort()for cancellation controlcreateCallHandleinpackages/sdk/src/abort.tsmerges external and internal signals, supporting both pre-emptive and mid-flight cancellation- Session methods (
prompt,skill,task,shell) uniformly supportoptions.signalfor timeout and composite signal patterns - Sandbox operations respect abort signals, terminating subprocesses when
shell()calls are cancelled - Error handling produces
AbortErrorDOMExceptions with the original reason preserved in thecauseproperty
Frequently Asked Questions
How do I cancel a Flue agent operation after it has already started?
Retain the CallHandle returned by any session method and call handle.abort(reason). For example: const handle = session.prompt("Analyze this code"); handle.abort("User clicked cancel");. The promise will reject with an AbortError containing your reason in the cause property.
Can I set a timeout on shell commands executed through Flue?
Yes. Pass AbortSignal.timeout(milliseconds) in the options object: await session.shell("sleep 30", { signal: AbortSignal.timeout(1_000) });. When the timeout fires, the SDK propagates the abort signal to the sandbox, which terminates the underlying subprocess before rejecting the promise.
What is the difference between passing an external signal and using handle.abort()?
Passing an external signal (like AbortSignal.timeout()) allows the caller to control cancellation timing before the call starts. Using handle.abort() allows post-start cancellation from the returned handle. Both methods trigger the same internal abort logic and result in an AbortError, but handle.abort() provides imperative control while external signals provide declarative control.
Where can I find a complete working example of abort patterns in Flue?
The repository includes a reference implementation at examples/hello-world/.flue/agents/with-abort.ts that demonstrates timeout-based aborts on prompt and shell, manual aborts via handle.abort(), and handling of pre-aborted signals. This example exercises all abort scenarios supported by the SDK.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →