How to Handle Abort Signals in Long-Running Agent Actions in BuilderIO Agent-Native

To handle abort signals in long-running agent actions, pass an optional AbortSignal through the action context and propagate it to every async operation—including fetch requests, retryDelay loops, and LLM calls—to enable immediate cancellation of long-running work.

BuilderIO/agent-native provides first-class support for cancellation through the standard Web API AbortSignal. When building agents that perform network requests, heavy computation, or extended retries, accepting and propagating this signal prevents resource leaks, enforces hard timeouts, and keeps user interfaces responsive.

Core Architecture of Abort Signal Handling

Action Definition Contract

In packages/core/src/action.ts, the action definition declares an optional signal?: AbortSignal field in its context (line 80). This allows callers to supply a cancellation token that the runtime automatically injects into executing actions. By declaring signal in the action schema, you signal that the operation respects cancellation.

Production Agent and Retry Logic

The retryDelay helper in packages/core/src/agent/production-agent.ts (lines 72-89) creates a cancellable Promise that resolves after a back-off period but rejects immediately if the signal is aborted. This ensures retry loops don't wait unnecessarily when the user cancels or a timeout triggers.

HTTP Fetch Integration

External calls consistently use fetch(..., { signal }). For example, in packages/core/src/cli/plan-local.ts (line 324), the implementation applies signal: init.signal ?? AbortSignal.timeout(PLAN_ACTION_HTTP_TIMEOUT_MS) to enforce hard timeouts on plan operations, preventing indefinite hangs on slow endpoints.

Completion Handling

For LLM text generation, packages/core/src/server/complete-text.ts (lines 102-108) uses createCompletionAbortSignal to combine a user-provided signal with an internal timeout. This ensures long-running completions can be interrupted cleanly without leaving orphaned connections.

Implementation Patterns for Abort Signal Handling

Defining Cancellable Actions

When defining actions in packages/core/src/action.ts, destructure the signal from the runtime context and pass it to all async operations. Use AbortSignal.timeout to provide a hard limit when the caller doesn't supply one.

import { defineAction }引以为傲 from "@agent-native/core";

export default defineAction({
  schema: z.object({ url: z.string().url() }),
  run: async ({ url }, { signal }) => {
    // Abort after 10 seconds if the caller didn't provide its own timeout.
    const timeoutSignal = signal ?? AbortSignal.timeout(10_000);

    // Pass the signal to fetch; if aborted, fetch rejects with AbortError.
    const resp = await fetch(url, { signal: timeoutSignal });
    if (!resp.ok) throw new Error(`HTTP ${resp.status}`);

    return await resp.json();
  },
});

The action receives { signal } from the runtime automatically. If the user cancels the UI action, the same signal aborts the fetch instantly, preventing memory leaks.

Using retryDelay with Abort Signals

The retryDelay utility respects abort signals to prevent unnecessary waits during back-off periods. Always pass the signal to avoid waiting through full retry delays after cancellation.

import { retryDelay } from "@agent-native/core";

async function resilientFetch(url: string, signal?: AbortSignal) {
  for (let attempt = 0; attempt < 5; ++attempt) {
    try {
      return await fetch(url, { signal });
    } catch (err) {
      // Network-related errors trigger a retry; abort signal stops the loop.
      if (signal?.aborted) throw new Error("aborted");
      await retryDelay(attempt, signal ?? new AbortController().signal);
    }
  }
  throw new Error("All retries failed");
}

As implemented in packages/core/src/agent/production-agent.ts, retryDelay checks signal.aborted and rejects immediately if the signal triggers, preventing the full back-off duration from elapsing.

Creating Scoped Abort Controllers in UI Code

For client-side cancellation, use createAbortController from packages/core/src/client/chat/runtime.ts (lines 696-720). This utility merges an existing signal with a new one, ensuring downstream code can always attach abort listeners safely.

import { createAbortController } from "@agent-native/core/client";

function useLongTask() {
  const abortCtrl = new AbortController();

  // Pass the controller's signal to the action.
  startAction({ url: "/slow-endpoint" }, { signal: abortCtrl.signal });

  // When the component unmounts or the user clicks "Cancel":
  return () => abortCtrl.abort();
}

This pattern ensures that even if the underlying action creates its own internal abort mechanisms, your component can still trigger cancellation.

Common Pitfalls and Best Practices

  • Pass the signal to every fetch: Always use fetch(url, { signal }). If ignored, requests continue after the UI closes, causing wasted bandwidth and memory leaks.

  • Wrap retry loops with signal checks: Use await retryDelay(attempt, signal) rather than raw setTimeout. Without this, the loop waits the full back-off even after the user cancels.

  • Use AbortSignal.timeout for hard limits: Apply signal ?? AbortSignal.timeout(ms) to prevent unbounded calls to external APIs that can hang indefinitely.

  • Check signal.aborted before expensive work: Insert if (signal?.aborted) throw new Error("aborted") before CPU-bound tasks to avoid running calculations whose results will be discarded.

Summary

  • Accept signal in action definitions via the runtime context in packages/core/src/action.ts
  • Propagate signals to fetch, retryDelay, and LLM calls throughout the call stack
  • Use AbortSignal.timeout for automatic hard limits on external requests
  • Leverage createAbortController from packages/core/src/client/chat/runtime.ts for UI scoping
  • Check signal.aborted before CPU-intensive work to prevent wasted computation

Following these patterns ensures that long-running agent actions can be stopped promptly, keeping applications responsive and preventing resource exhaustion.

Frequently Asked Questions

How do I cancel an agent action from a React component?

Create an AbortController in your component, pass its signal to the action via the options parameter, and call controller.abort() in a cleanup function or when the user clicks cancel. The createAbortController utility in packages/core/src/client/chat/runtime.ts can merge this with existing signals if needed.

What happens if I don't pass the signal to fetch?

The HTTP request continues in the background even after the UI component unmounts or the user cancels, causing wasted bandwidth, potential memory leaks, and unnecessary server load.

Can I combine multiple abort signals for complex timeouts?

Yes, use the createAbortController utility from packages/core/src/client/chat/runtime.ts (lines 696-720). This merges an existing signal with a new controller, ensuring downstream code can always call signal.addEventListener safely while respecting both user cancellation and internal timeouts.

Does retryDelay automatically respect the abort signal?

Yes, retryDelay in packages/core/src/agent/production-agent.ts (lines 72-89) checks signal.aborted and rejects immediately if the signal is triggered, preventing the function from waiting through the full back-off period when cancellation occurs.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →