# How to Implement Abort Signals for Cancelling Long-Running Agent Operations in Flue

> Learn to implement abort signals in Flue to cancel long-running agent operations. Master timeouts, manual triggers, and composite controllers for efficient async call management.

- Repository: [Astro/flue](https://github.com/withastro/flue)
- Tags: how-to-guide
- Published: 2026-05-11

---

**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`](https://github.com/withastro/flue/blob/main/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 merged `AbortSignal` that reflects the current abort state
- **`abort(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`](https://github.com/withastro/flue/blob/main/packages/sdk/src/abort.ts) implements the actual wrapping logic. When you invoke a session method, the SDK:

1. Creates a fresh `AbortController` for the internal operation
2. Checks if the caller provided an external signal via `options.signal`
3. If the external signal is already aborted, immediately aborts the internal controller with the same reason
4. 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`](https://github.com/withastro/flue/blob/main/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:

1. **Session method** (`session.prompt()` or `session.shell()`) receives the options
2. **`createCallHandle`** merges external and internal signals
3. **Sandbox executor** ([`packages/sdk/src/sandbox.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/sandbox.ts)) receives the signal and respects it during `exec()` calls, killing subprocesses if aborted
4. **Error surfacing** rejects the handle's promise with the `AbortError` containing 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.

```typescript
// 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:

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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

- **`CallHandle`** wraps every async operation in Flue, exposing `signal` and `abort()` for cancellation control
- **`createCallHandle`** in [`packages/sdk/src/abort.ts`](https://github.com/withastro/flue/blob/main/packages/sdk/src/abort.ts) merges external and internal signals, supporting both pre-emptive and mid-flight cancellation
- **Session methods** (`prompt`, `skill`, `task`, `shell`) uniformly support `options.signal` for timeout and composite signal patterns
- **Sandbox operations** respect abort signals, terminating subprocesses when `shell()` calls are cancelled
- **Error handling** produces `AbortError` DOMExceptions with the original reason preserved in the `cause` property

## 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`](https://github.com/withastro/flue/blob/main/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.