Skip to content
5 changes: 5 additions & 0 deletions .changeset/eleven-apples-yell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

Add support for aborting / stopping an agent run & continuing an agent run using messages from prior runs
56 changes: 56 additions & 0 deletions packages/core/lib/v3/agent/utils/combineAbortSignals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* Combines multiple AbortSignals into a single signal that aborts when any of them abort.
*
* Uses AbortSignal.any() if available (Node 20+), otherwise falls back to a manual implementation.
*
* @param signals - Array of AbortSignals to combine (undefined signals are filtered out)
* @returns A combined AbortSignal, or undefined if no valid signals provided
*/
export function combineAbortSignals(
...signals: (AbortSignal | undefined)[]
): AbortSignal | undefined {
const validSignals = signals.filter((s): s is AbortSignal => s !== undefined);

if (validSignals.length === 0) {
return undefined;
}

if (validSignals.length === 1) {
return validSignals[0];
}

// Use AbortSignal.any() if available (Node 20+)
if (typeof AbortSignal.any === "function") {
return AbortSignal.any(validSignals);
}

// Fallback for older environments
const controller = new AbortController();

// Track abort handlers so we can clean them up when one signal aborts
const handlers: Array<{ signal: AbortSignal; handler: () => void }> = [];

const cleanup = () => {
for (const { signal, handler } of handlers) {
signal.removeEventListener("abort", handler);
}
};

for (const signal of validSignals) {
if (signal.aborted) {
cleanup(); // Remove handlers added to previous signals in this loop
controller.abort(signal.reason);
return controller.signal;
}

const handler = () => {
cleanup(); // Remove all listeners to prevent memory leak
controller.abort(signal.reason);
};

handlers.push({ signal, handler });
signal.addEventListener("abort", handler, { once: true });
}

return controller.signal;
}
48 changes: 48 additions & 0 deletions packages/core/lib/v3/agent/utils/errorHandling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { AgentAbortError } from "../../types/public/sdkErrors";

/**
* Extracts the abort signal from instruction or options.
*/
export function extractAbortSignal(
instructionOrOptions: string | { signal?: AbortSignal },
): AbortSignal | undefined {
return typeof instructionOrOptions === "object" &&
instructionOrOptions !== null
? instructionOrOptions.signal
: undefined;
}

/**
* Consistently extracts an error message from an unknown error.
*/
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
return String(error);
}

/**
* Checks if an error is abort-related (either an abort error type or the signal was aborted).
* Returns the appropriate reason string if it's an abort, or null if not.
*
* @param error - The caught error
* @param abortSignal - The abort signal to check
* @returns The abort reason string if abort-related, null otherwise
*/
export function getAbortErrorReason(
error: unknown,
abortSignal?: AbortSignal,
): string | null {
if (!AgentAbortError.isAbortError(error) && !abortSignal?.aborted) {
return null;
}

// Prefer the signal's reason if available
if (abortSignal?.reason) {
return String(abortSignal.reason);
}

// Fall back to the error message
return getErrorMessage(error);
}
91 changes: 91 additions & 0 deletions packages/core/lib/v3/agent/utils/validateExperimentalFeatures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
ExperimentalNotConfiguredError,
StagehandInvalidArgumentError,
} from "../../types/public/sdkErrors";
import type { AgentConfig, AgentExecuteOptionsBase } from "../../types/public";

export interface AgentValidationOptions {
/** Whether experimental mode is enabled */
isExperimental: boolean;
/** Agent config options (integrations, tools, stream, cua, etc.) */
agentConfig?: Partial<AgentConfig>;
/** Execute options (callbacks, signal, messages, etc.) */
executeOptions?:
| (Partial<AgentExecuteOptionsBase> & { callbacks?: unknown })
| null;
/** Whether this is streaming mode (can be derived from agentConfig.stream) */
isStreaming?: boolean;
}

/**
* Validates agent configuration and experimental feature usage.
*
* This utility consolidates all validation checks for both CUA and non-CUA agent paths:
* - Invalid argument errors for CUA (streaming, abort signal, message continuation are not supported)
* - Experimental feature checks for non-CUA (integrations, tools, callbacks, signal, messages, streaming)
*
* Throws StagehandInvalidArgumentError for invalid/unsupported configurations.
* Throws ExperimentalNotConfiguredError if experimental features are used without experimental mode.
*/
export function validateExperimentalFeatures(
options: AgentValidationOptions,
): void {
const { isExperimental, agentConfig, executeOptions, isStreaming } = options;

// CUA-specific validation: certain features are not available at all
if (agentConfig?.cua) {
const unsupportedFeatures: string[] = [];

if (agentConfig?.stream) {
unsupportedFeatures.push("streaming");
}
if (executeOptions?.signal) {
unsupportedFeatures.push("abort signal");
}
if (executeOptions?.messages) {
unsupportedFeatures.push("message continuation");
}

if (unsupportedFeatures.length > 0) {
throw new StagehandInvalidArgumentError(
`${unsupportedFeatures.join(", ")} ${unsupportedFeatures.length === 1 ? "is" : "are"} not supported with CUA (Computer Use Agent) mode.`,
);
}
}

// Skip experimental checks if already in experimental mode
if (isExperimental) return;

const features: string[] = [];

// Check agent config features (check array length to avoid false positives for empty arrays)
const hasIntegrations =
agentConfig?.integrations && agentConfig.integrations.length > 0;
const hasTools =
agentConfig?.tools && Object.keys(agentConfig.tools).length > 0;
if (hasIntegrations || hasTools) {
features.push("MCP integrations and custom tools");
}

// Check streaming mode (either explicit or derived from config) - only for non-CUA
if (!agentConfig?.cua && (isStreaming || agentConfig?.stream)) {
features.push("streaming");
}

// Check execute options features - only for non-CUA
if (executeOptions && !agentConfig?.cua) {
if (executeOptions.callbacks) {
features.push("callbacks");
}
if (executeOptions.signal) {
features.push("abort signal");
}
if (executeOptions.messages) {
features.push("message continuation");
}
}

if (features.length > 0) {
throw new ExperimentalNotConfiguredError(`Agent ${features.join(", ")}`);
}
}
Loading
Loading