feat(sdk-ts): add configurable RPC call controls (#2925) - #3000
seanmcguire12 wants to merge 1 commit into
Conversation
# why
Long-running Stagehand RPCs need two forms of caller control:
- A bounded response wait for environments that cannot wait
indefinitely.
- A way to stop waiting when the current caller or notebook cell is
cancelled.
Cancellation must not implicitly close Stagehand or the underlying
browser. A later operation should still be able to use the existing
session.
# what changed
- Add TypeScript-only `rpcTimeouts` to `Stagehand.create()`:
- `defaultMs` sets a response deadline for ordinary RPC calls.
- `methods` overrides the default for specific Stagehand RPC methods.
- Timeout values must be positive integers and are capped at
`2_147_473_647` ms.
- Configured values take precedence over Stagehand’s existing computed
response deadlines.
- Add TypeScript-only `getCallOptions()` to `Stagehand.create()`:
- Invoked once immediately before each ordinary outbound RPC call.
- May return `{ signal?: AbortSignal }`.
- Its signal is combined with an existing call signal and the
response-timeout signal.
- A pre-aborted signal fails before the RPC is sent.
- Keep `stagehand.init` and `stagehand.callback_batch` outside both
hooks:
- Initialization retains its existing lifecycle deadline.
- `experimentalBatch()` retains its existing whole-batch timeout and
cancellation behavior.
- Re-throw transport send errors after rejecting their pending RPC
request.
- Export schemas and types, document the options, and add a minor
Changeset.
# test plan
- [x] `pnpm --filter @browserbasehq/stagehand typecheck`
- [x] `pnpm --filter @browserbasehq/stagehand build`
- [x] Production build passed `publint`.
- [x] `pnpm --filter @browserbasehq/stagehand test:unit`
- 20 test files passed
- 290 tests passed
- [x] Published-package consumer coverage verifies the new schemas and
types.
Focused coverage verifies:
- configured per-method timeout overrides the configured default;
- existing fallback timeout behavior remains unchanged without
configuration;
- initialization and callback batches ignore the new hooks;
- `getCallOptions()` is called once per ordinary RPC;
- pre-aborted signals do not send an RPC;
- aborting one ordinary RPC leaves the client usable for a later RPC;
- transport send failures propagate to the caller.
<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds configurable response wait deadlines and per-call abort signals to
ordinary Stagehand RPC calls, so callers can bound how long they wait
and cancel a single call without closing the session.
- `Stagehand.create()` accepts two new TypeScript-only options:
`rpcTimeouts` (`defaultMs` with per-method overrides) and
`getCallOptions()` returning an optional `AbortSignal`.
- Configured timeouts take precedence over the SDK's existing computed
deadlines; values must be positive integers capped at `2_147_473_647`
ms.
- `getCallOptions()` is invoked once before each ordinary RPC call, and
a pre-aborted signal fails before the call is sent.
- `stagehand.init` and `experimentalBatch()` ignore both hooks and keep
their existing lifecycle timeout and cancellation behavior.
- Aborting a single call leaves the Stagehand instance and its browser
usable for later calls.
- Transport send failures now surface to the caller as errors after
their pending request is rejected.
<sup>Written for commit 807a930.
Summary will update on new commits.</sup>
<a
href="https://cubic.dev/pr/browserbase/stagehand/pull/2925?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
🦋 Changeset detectedLatest commit: 51918a1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
3 issues found across 8 files
Confidence score: 3/5
packages/sdk-ts/src/clientSchemas.tscan silently ignore timeout configuration when an RPC method name is misspelled, making callers believe a timeout is enforced when it is not; validatemethodskeys against registered method names.packages/sdk-ts/src/rpcClient.tsinvokesgetCallOptionseven when local parameter validation prevents an RPC from being sent, which can trigger unnecessary or misleading side effects; move the hook next to the actual request dispatch.packages/docs/v4/reference/stagehand.mdxcurrently instructs callers to return a signal instead of an options object, causing the documented usage to be rejected byStagehandCallOptionsSchema; update the example and wording to return{ signal: controller.signal }.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/docs/v4/reference/stagehand.mdx">
<violation number="1" location="packages/docs/v4/reference/stagehand.mdx:85">
P2: Tell callers to return an options object, such as `{ signal: controller.signal }`, rather than the signal itself. Following the current prose produces a value that `StagehandCallOptionsSchema` rejects.</violation>
</file>
<file name="packages/sdk-ts/src/clientSchemas.ts">
<violation number="1" location="packages/sdk-ts/src/clientSchemas.ts:284">
P2: When a caller misspells an RPC method name, this schema accepts the key and the requested timeout is silently ignored. Validate `methods` keys against the registered method names so timeout configuration errors fail during `Stagehand.create()`.</violation>
</file>
<file name="packages/sdk-ts/src/rpcClient.ts">
<violation number="1" location="packages/sdk-ts/src/rpcClient.ts:185">
P2: When local parameter validation fails, `getCallOptions` still runs even though `send` never issues an RPC. Move the hook invocation to immediately before `waitForResponse`/`cdp.send` so it runs only for an actual ordinary outbound call.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Client as Client Code
participant SH as Stagehand Class
participant RPC as RPCClient
participant CDP as CDP Transport
participant Browser as Browser Session
Note over Client,Browser: Stagehand RPC Call Flow with Configurable Controls
Client->>SH: Stagehand.create({ rpcTimeouts, getCallOptions })
SH->>SH: Parse options via StagehandCreateOptionsSchema
SH->>RPC: Constructor(cdp, { rpcTimeouts, getCallOptions })
Client->>RPC: send(method, params, options)
RPC->>RPC: Check if method is init or callback_batch
alt Ordinary RPC call
RPC->>RPC: callOptions = getCallOptions?.()
RPC->>RPC: Validate callOptions with StagehandCallOptionsSchema
RPC->>RPC: Combine signals: options.signal + callOptions.signal
RPC->>RPC: throwIfAborted(combinedSignal)
Note over RPC: Pre-aborted signal fails here without sending
RPC->>RPC: Determine timeout via rpcResponseTimeoutMs()
alt Configured timeout exists
RPC->>RPC: Use rpcTimeouts.methods[method] ?? rpcTimeouts.defaultMs
else No configured timeout
RPC->>RPC: Use computed deadline (legacy behavior)
end
RPC->>RPC: Create timeout AbortController if needed
RPC->>RPC: Combine callSignal + timeoutSignal
RPC->>CDP: Send JSON-RPC request with signal
alt Request succeeds
CDP-->>RPC: Response
RPC-->>Client: Result
else Signal aborted (timeout or user)
RPC->>RPC: Reject pending request with abort reason
Note over RPC: Client remains usable
RPC-->>Client: Error
else Transport send fails
CDP-->>RPC: Send error
RPC->>RPC: Reject pending request
RPC->>RPC: Re-throw transport error
RPC-->>Client: Transport error
end
else Init or callback_batch
Note over RPC: Skips getCallOptions and configured timeouts
RPC->>CDP: Send with existing lifecycle/deadline behavior
end
Note over RPC,Browser: Aborting an RPC does not close browser or Stagehand
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| </ParamField> | ||
|
|
||
| <ParamField path="getCallOptions" type="() => { signal?: AbortSignal } | undefined" optional> | ||
| Called immediately before each ordinary RPC call. Return an `AbortSignal` to stop waiting for that call when the signal aborts. Aborting a call does not close the Stagehand instance or its browser. |
There was a problem hiding this comment.
P2: Tell callers to return an options object, such as { signal: controller.signal }, rather than the signal itself. Following the current prose produces a value that StagehandCallOptionsSchema rejects.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/docs/v4/reference/stagehand.mdx, line 85:
<comment>Tell callers to return an options object, such as `{ signal: controller.signal }`, rather than the signal itself. Following the current prose produces a value that `StagehandCallOptionsSchema` rejects.</comment>
<file context>
@@ -77,6 +77,14 @@ const stagehand = await Stagehand.create({ browser });
+</ParamField>
+
+<ParamField path="getCallOptions" type="() => { signal?: AbortSignal } | undefined" optional>
+ Called immediately before each ordinary RPC call. Return an `AbortSignal` to stop waiting for that call when the signal aborts. Aborting a call does not close the Stagehand instance or its browser.
+</ParamField>
+
</file context>
| Called immediately before each ordinary RPC call. Return an `AbortSignal` to stop waiting for that call when the signal aborts. Aborting a call does not close the Stagehand instance or its browser. | |
| Called immediately before each ordinary RPC call. Return `{ signal: controller.signal }` (or `undefined`) to stop waiting for that call when the signal aborts. Aborting a call does not close the Stagehand instance or its browser. |
| export const StagehandRPCTimeoutsSchema = z | ||
| .strictObject({ | ||
| defaultMs: RPCTimeoutMsSchema.optional(), | ||
| methods: z.record(z.string(), RPCTimeoutMsSchema).optional(), |
There was a problem hiding this comment.
P2: When a caller misspells an RPC method name, this schema accepts the key and the requested timeout is silently ignored. Validate methods keys against the registered method names so timeout configuration errors fail during Stagehand.create().
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/src/clientSchemas.ts, line 284:
<comment>When a caller misspells an RPC method name, this schema accepts the key and the requested timeout is silently ignored. Validate `methods` keys against the registered method names so timeout configuration errors fail during `Stagehand.create()`.</comment>
<file context>
@@ -267,8 +268,40 @@ export const StagehandBrowserSchema = z
+export const StagehandRPCTimeoutsSchema = z
+ .strictObject({
+ defaultMs: RPCTimeoutMsSchema.optional(),
+ methods: z.record(z.string(), RPCTimeoutMsSchema).optional(),
+ })
+ .meta({ id: "StagehandRPCTimeouts" });
</file context>
| method.name === StagehandMethods.stagehandInit.name || | ||
| method.name === StagehandMethods.stagehandCallbackBatch.name | ||
| ? undefined | ||
| : this.getCallOptions?.(); |
There was a problem hiding this comment.
P2: When local parameter validation fails, getCallOptions still runs even though send never issues an RPC. Move the hook invocation to immediately before waitForResponse/cdp.send so it runs only for an actual ordinary outbound call.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/src/rpcClient.ts, line 185:
<comment>When local parameter validation fails, `getCallOptions` still runs even though `send` never issues an RPC. Move the hook invocation to immediately before `waitForResponse`/`cdp.send` so it runs only for an actual ordinary outbound call.</comment>
<file context>
@@ -163,6 +178,16 @@ export class RPCClient {
+ method.name === StagehandMethods.stagehandInit.name ||
+ method.name === StagehandMethods.stagehandCallbackBatch.name
+ ? undefined
+ : this.getCallOptions?.();
+ const callSignal = combineAbortSignals(
+ options.signal,
</file context>
this PR is a mirror of the original #2925
why
Long-running Stagehand RPCs need two forms of caller control:
Cancellation must not implicitly close Stagehand or the underlying browser. A later operation should still be able to use the existing session.
what changed
rpcTimeoutstoStagehand.create():defaultMssets a response deadline for ordinary RPC calls.methodsoverrides the default for specific Stagehand RPC methods.2_147_473_647ms.getCallOptions()toStagehand.create():{ signal?: AbortSignal }.stagehand.initandstagehand.callback_batchoutside both hooks:experimentalBatch()retains its existing whole-batch timeout and cancellation behavior.test plan
pnpm --filter @browserbasehq/stagehand typecheckpnpm --filter @browserbasehq/stagehand buildpublint.pnpm --filter @browserbasehq/stagehand test:unitFocused coverage verifies:
- configured per-method timeout overrides the configured default;
- existing fallback timeout behavior remains unchanged without configuration;
- initialization and callback batches ignore the new hooks;
- pre-aborted signals do not send an RPC;
- aborting one ordinary RPC leaves the client usable for a later RPC;
- transport send failures propagate to the caller.
---getCallOptions()is called once per ordinary RPC;Summary by cubic
Adds configurable response wait deadlines and per-call abort signals to ordinary Stagehand RPC calls, so callers can bound how long they wait and cancel a single call without closing the session.
Stagehand.create()accepts two new TypeScript-only options:rpcTimeouts(defaultMswith per-method overrides) andgetCallOptions()returning an optionalAbortSignal.2_147_473_647ms.getCallOptions()is invoked once before each ordinary RPC call, and a pre-aborted signal fails before the call is sent.stagehand.initandexperimentalBatch()ignore both hooks and keep their existing lifecycle timeout and cancellation behavior.Written for commit 807a930. Summary will update on new commits.