Skip to content

[STG-1427] caching threshold configuration#1732

Open
sameelarif wants to merge 20 commits intomainfrom
sameelarif/stg-1427-caching-threshold-configuration-client-side
Open

[STG-1427] caching threshold configuration#1732
sameelarif wants to merge 20 commits intomainfrom
sameelarif/stg-1427-caching-threshold-configuration-client-side

Conversation

@sameelarif
Copy link
Member

@sameelarif sameelarif commented Feb 23, 2026

why

what changed

test plan


Summary by cubic

Adds client-side controls for server-side caching with per-method threshold overrides and cache bypass. Surfaces cache status on act/extract/observe results and updates docs to meet STG-1427.

  • New Features
    • Add serverCache config: global boolean or per-method object (act/extract/observe) with { threshold }. Per-call options override constructor; defaults to enabled.
    • Send cacheThreshold in act/extract/observe requests and set browserbase-cache-bypass when disabled; instance- and method-level settings are respected.
    • Expose cacheStatus ("HIT" | "MISS") on act/extract/observe (ObserveResult updated) and log cache status from response headers/SSE.
    • Prevent caching incomplete extract results and improve action cache variable handling (keys affect cache key; values substituted on replay), with tests and docs updates (includes blog link).

Written for commit e12d4b1. Summary will update on new commits. Review in cubic

@changeset-bot
Copy link

changeset-bot bot commented Feb 23, 2026

🦋 Changeset detected

Latest commit: e12d4b1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@browserbasehq/stagehand Patch
@browserbasehq/stagehand-evals Patch
@browserbasehq/stagehand-server Patch

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

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 23, 2026

Greptile Summary

This PR adds server-side caching configuration to the Stagehand SDK, allowing users to control caching behavior at both the instance level (V3Options.serverCache) and per-method level (ActOptions.serverCache, etc.). The implementation introduces a cacheThreshold field sent over the wire to control minimum hit counts, a browserbase-cache-bypass header for disabling cache, and cacheStatus on ActResult/ExtractResult to surface cache hit/miss info. Additionally, incomplete extractions are now flagged with a non-enumerable __extractionIncomplete property to prevent caching partial results.

  • Instance-level config: V3Options.serverCache supports boolean or per-method object ({ act, extract, observe }) with threshold overrides
  • Method-level override: serverCache on ActOptions, ExtractOptions, ObserveOptions supports boolean | { threshold: number }
  • Precedence: Method-level serverCache > constructor per-method config > constructor global boolean > default (true)
  • Type test bug: public-types.test.ts declares serverCache?: boolean but the actual type is boolean | { threshold: number } — these tests will fail with toEqualTypeOf
  • New test file: cache-variables.test.ts covers ActCache variable handling (cache key generation, variable substitution, key mismatch, missing variables)

Confidence Score: 3/5

  • The type test mismatch will cause test failures; the core caching logic itself is well-structured.
  • The caching feature implementation across v3.ts and api.ts is solid with proper precedence logic. However, the public type tests in public-types.test.ts declare serverCache?: boolean when the actual types use boolean | { threshold: number }, which will cause test failures. This is a straightforward fix but blocks merging if CI runs type tests.
  • packages/core/tests/public-api/public-types.test.ts — type assertions don't match actual types (3 occurrences)

Important Files Changed

Filename Overview
packages/core/lib/v3/api.ts Adds server-side caching plumbing: serverCache constructor param, shouldUseCache() helper, cache-bypass header, attachCacheStatus() for results, and cacheThreshold in request bodies. Well-structured with proper precedence logic.
packages/core/lib/v3/v3.ts Adds resolveServerCacheThreshold() and resolveServerCacheBoolean() for method-level vs constructor-level cache config resolution. Passes resolved values to API client methods for act/extract/observe. Logic is sound.
packages/core/lib/v3/types/public/methods.ts Adds `serverCache?: boolean
packages/core/lib/v3/types/public/options.ts Adds serverCache to V3Options with comprehensive union type supporting boolean or per-method object config. Well-documented with JSDoc.
packages/core/lib/v3/handlers/extractHandler.ts Adds non-enumerable __extractionIncomplete property to incomplete extraction results to signal cache layer to skip persisting them. Clean implementation using Object.defineProperty.
packages/core/tests/public-api/public-types.test.ts Type test declares serverCache?: boolean for ActOptions, ExtractOptions, and ObserveOptions, but actual type is `boolean

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["User calls act/extract/observe<br>with options.serverCache"] --> B{"Method-level<br>serverCache set?"}
    B -- Yes --> C{"typeof serverCache"}
    C -- "boolean" --> D["Use boolean directly"]
    C -- "{ threshold }" --> E["Cache enabled + custom threshold"]
    B -- No --> F{"Constructor-level<br>serverCache type?"}
    F -- "boolean" --> G["Use constructor boolean"]
    F -- "object (per-method)" --> H{"Method key exists<br>in object?"}
    H -- Yes --> I["Use per-method setting"]
    H -- No --> J["Default: cache enabled"]
    F -- "undefined" --> J

    D --> K{"Resolved to false?"}
    G --> K
    I --> K
    J --> K
    E --> L["Send cacheThreshold<br>in request body"]
    L --> K

    K -- Yes --> M["Add browserbase-cache-bypass<br>header = true"]
    K -- No --> N["No bypass header<br>(caching active)"]

    M --> O["API Request"]
    N --> O
    O --> P["Read browserbase-cache-status<br>response header"]
    P --> Q["Attach cacheStatus<br>to ActResult/ExtractResult"]
Loading

Last reviewed commit: 3552ff0

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

10 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

variables?: Stagehand.Variables;
timeout?: number;
page?: Stagehand.AnyPage;
serverCache?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Type mismatch will fail tests

The expected type here declares serverCache?: boolean, but the actual ActOptions type in methods.ts:25 defines serverCache?: boolean | { threshold: number }. Since toEqualTypeOf performs exact type matching, this test will fail. The same issue applies to ExtractOptions (line 155) and ObserveOptions (line 169).

Suggested change
serverCache?: boolean;
serverCache?: boolean | { threshold: number };

timeout?: number;
selector?: string;
page?: Stagehand.AnyPage;
serverCache?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same type mismatch as ActOptions

Should match the actual ExtractOptions.serverCache type.

Suggested change
serverCache?: boolean;
serverCache?: boolean | { threshold: number };

timeout?: number;
selector?: string;
page?: Stagehand.AnyPage;
serverCache?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same type mismatch as ActOptions

Should match the actual ObserveOptions.serverCache type.

Suggested change
serverCache?: boolean;
serverCache?: boolean | { threshold: number };

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 10 files

Confidence score: 3/5

  • Missing integration tests for new caching behavior in packages/core/lib/v3/api.ts increases regression risk for the Stagehand REST API client/server changes.
  • Hidden __extractionIncomplete hook in packages/core/lib/v3/handlers/extractHandler.ts exposes internals instead of using the event bus, which could lead to unintended API surface and maintenance issues.
  • Several API consistency/doc gaps (LaunchDarkly reference in packages/core/lib/v3/types/public/api.ts, observe return type missing cacheStatus in packages/core/lib/v3/types/public/methods.ts) suggest some user-facing confusion but are fixable.
  • Pay close attention to packages/core/lib/v3/api.ts, packages/core/lib/v3/handlers/extractHandler.ts, packages/core/lib/v3/types/public/api.ts, packages/core/lib/v3/types/public/methods.ts - ensure tests and public API surface stay consistent.
Prompt for AI agents (all 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/core/lib/v3/api.ts">

<violation number="1" location="packages/core/lib/v3/api.ts:876">
P1: Custom agent: **Any breaking changes to Stagehand REST API client / server implementation must be covered by an integration test under packages/server/test**

No integration tests cover the new caching behavior introduced in the API client. This PR adds `cacheThreshold` to request bodies, a `browserbase-cache-bypass` request header, and `browserbase-cache-status` response header handling — all of which modify the wire format between client and server. Per the rule, breaking changes to `packages/core/lib/v3/api.ts` must be covered by at least one integration test under `packages/server/test/**`.

At minimum, tests should verify:
- The `browserbase-cache-bypass: true` header is sent when `serverCache` is `false` (instance-level and method-level override)
- `cacheThreshold` is included in act/extract/observe request bodies when provided
- `cacheStatus` is correctly attached to act/extract results from the `browserbase-cache-status` response header

(Based on your team's feedback about preferring unit tests for new behavior.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/core/lib/v3/handlers/extractHandler.ts">

<violation number="1" location="packages/core/lib/v3/handlers/extractHandler.ts:239">
P1: Custom agent: **Don't allow exposing internals of stagehand library via hidden callback hooks, instead force event-based hooking via stagehand.bus EventEmitter**

This introduces a hidden `__extractionIncomplete` property on the return value as an undocumented hook for the cloud cache layer to detect incomplete extractions. This is the exact anti-pattern the team wants to avoid — exposing library internals via hidden `__`-prefixed mechanisms for external code to consume.

Instead, signal incomplete extractions through a well-defined event-based interface (e.g., `this.bus.emit("extraction_incomplete", { output })`) so the stagehand-api cache layer can subscribe cleanly via `stagehand.bus.on("extraction_incomplete", ...)`. This keeps the coupling explicit, discoverable, and consistent with the team's preferred architecture.</violation>

<violation number="2" location="packages/core/lib/v3/handlers/extractHandler.ts:239">
P2: When a non-object schema is used (e.g., `z.string()`, `z.number()`), `output` is unwrapped to a primitive value on line 224–225, and the `typeof output === "object"` guard here correctly avoids a runtime error—but it also means the `__extractionIncomplete` signal is silently lost for all primitive extraction results. The cache layer would have no way to distinguish a complete vs. incomplete primitive result and may persist incomplete data.

Consider an alternative signaling mechanism that works for all output types (e.g., a `WeakMap`, a wrapper return type, or returning a `{ data, metadata }` envelope).</violation>
</file>

<file name="packages/core/lib/v3/types/public/api.ts">

<violation number="1" location="packages/core/lib/v3/types/public/api.ts:438">
P2: The description references "LaunchDarkly flag" — an internal infrastructure detail that shouldn't be exposed in a public API schema. API consumers don't need to know about the feature-flag provider; a description like *"Custom cache threshold override. Minimum hit count before cached results are returned."* conveys the same intent without leaking internals.

(Based on your team's feedback about avoiding exposing internal types as public APIs.) [FEEDBACK_USED]</violation>
</file>

<file name="packages/core/lib/v3/types/public/methods.ts">

<violation number="1" location="packages/core/lib/v3/types/public/methods.ts:25">
P2: Inconsistency: `serverCache` option is added to `ObserveOptions` but there is no `cacheStatus` in the observe return type. The `cacheStatus` field was added to `ActResult` and `ExtractResult` but not to the observe result, so callers using `observe({ serverCache: true })` have no way to determine if the result was served from cache.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant App as Client Application
    participant V3 as Stagehand SDK (V3)
    participant Client as StagehandAPIClient
    participant BB_API as Browserbase Server API
    participant Handler as ExtractHandler (Internal)

    Note over App, BB_API: Configuration & Request Resolution

    App->>V3: act / extract / observe (options.serverCache)
    
    V3->>V3: NEW: resolveServerCacheBoolean()<br/>(Prioritize call-level over global config)
    V3->>V3: NEW: resolveServerCacheThreshold()<br/>(Extract custom threshold value)

    V3->>Client: Call method with resolved cache params
    
    Client->>Client: NEW: Prepare headers & body
    opt serverCache is false
        Note right of Client: Set 'browserbase-cache-bypass: true'
    end

    Client->>BB_API: POST request (JSON body includes cacheThreshold)
    
    Note over BB_API: Server processes request<br/>using cacheThreshold & bypass header

    BB_API-->>Client: Response Headers (NEW: browserbase-cache-status: HIT|MISS)
    
    loop SSE Stream
        BB_API-->>Client: data: { type: "system", status: "finished", result: {...} }
    end

    Note over Client, Handler: Result Post-Processing

    opt Method is "extract"
        Handler->>Handler: NEW: If result incomplete, set<br/>__extractionIncomplete: true
        Note right of Handler: Signals server-side cache layer<br/>to skip persistence
    end

    Client->>Client: NEW: attachCacheStatus()
    Note right of Client: Maps header/SSE data to result object
    
    Client-->>V3: Result (with cacheStatus field)
    V3-->>App: Result (e.g. { success: true, cacheStatus: "HIT" })

    Note over App, V3: Logging
    V3->>V3: Log: "act cache status: HIT (threshold: 2)" (NEW)
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

StagehandResponseBodyError,
StagehandResponseParseError,
StagehandServerError,
ExperimentalNotConfiguredError,
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Any breaking changes to Stagehand REST API client / server implementation must be covered by an integration test under packages/server/test

No integration tests cover the new caching behavior introduced in the API client. This PR adds cacheThreshold to request bodies, a browserbase-cache-bypass request header, and browserbase-cache-status response header handling — all of which modify the wire format between client and server. Per the rule, breaking changes to packages/core/lib/v3/api.ts must be covered by at least one integration test under packages/server/test/**.

At minimum, tests should verify:

  • The browserbase-cache-bypass: true header is sent when serverCache is false (instance-level and method-level override)
  • cacheThreshold is included in act/extract/observe request bodies when provided
  • cacheStatus is correctly attached to act/extract results from the browserbase-cache-status response header

(Based on your team's feedback about preferring unit tests for new behavior.)

View Feedback

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/api.ts, line 876:

<comment>No integration tests cover the new caching behavior introduced in the API client. This PR adds `cacheThreshold` to request bodies, a `browserbase-cache-bypass` request header, and `browserbase-cache-status` response header handling — all of which modify the wire format between client and server. Per the rule, breaking changes to `packages/core/lib/v3/api.ts` must be covered by at least one integration test under `packages/server/test/**`.

At minimum, tests should verify:
- The `browserbase-cache-bypass: true` header is sent when `serverCache` is `false` (instance-level and method-level override)
- `cacheThreshold` is included in act/extract/observe request bodies when provided
- `cacheStatus` is correctly attached to act/extract results from the `browserbase-cache-status` response header

(Based on your team's feedback about preferring unit tests for new behavior.) </comment>

<file context>
@@ -741,6 +871,12 @@ export class StagehandAPIClient {
     };
+
+    // Add cache bypass header if caching is disabled
+    if (!this.shouldUseCache(serverCache)) {
+      defaultHeaders["browserbase-cache-bypass"] = "true";
+    }
</file context>
Fix with Cubic

// (e.g., the stagehand-api cache layer) can skip persisting it.
// The property is non-enumerable so it is invisible to JSON.stringify
// and never exposed to clients.
if (!completed && output !== null && typeof output === "object") {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a non-object schema is used (e.g., z.string(), z.number()), output is unwrapped to a primitive value on line 224–225, and the typeof output === "object" guard here correctly avoids a runtime error—but it also means the __extractionIncomplete signal is silently lost for all primitive extraction results. The cache layer would have no way to distinguish a complete vs. incomplete primitive result and may persist incomplete data.

Consider an alternative signaling mechanism that works for all output types (e.g., a WeakMap, a wrapper return type, or returning a { data, metadata } envelope).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/handlers/extractHandler.ts, line 239:

<comment>When a non-object schema is used (e.g., `z.string()`, `z.number()`), `output` is unwrapped to a primitive value on line 224–225, and the `typeof output === "object"` guard here correctly avoids a runtime error—but it also means the `__extractionIncomplete` signal is silently lost for all primitive extraction results. The cache layer would have no way to distinguish a complete vs. incomplete primitive result and may persist incomplete data.

Consider an alternative signaling mechanism that works for all output types (e.g., a `WeakMap`, a wrapper return type, or returning a `{ data, metadata }` envelope).</comment>

<file context>
@@ -232,6 +232,18 @@ export class ExtractHandler {
+    // (e.g., the stagehand-api cache layer) can skip persisting it.
+    // The property is non-enumerable so it is invisible to JSON.stringify
+    // and never exposed to clients.
+    if (!completed && output !== null && typeof output === "object") {
+      Object.defineProperty(output, "__extractionIncomplete", {
+        value: true,
</file context>
Fix with Cubic

@miguelg719 miguelg719 changed the base branch from main to sameelarif/stg-1182-cache-bypass-from-sdk February 24, 2026 06:19
@miguelg719 miguelg719 changed the base branch from sameelarif/stg-1182-cache-bypass-from-sdk to main February 24, 2026 06:20
…rif/stg-1427-caching-threshold-configuration-client-side
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 4 files (changes from recent commits).

Prompt for AI agents (all 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/core/lib/v3/api.ts">

<violation number="1" location="packages/core/lib/v3/api.ts:376">
P1: Custom agent: **Any breaking changes to Stagehand REST API client / server implementation must be covered by an integration test under packages/server/test**

The `observe()` method now returns `ObserveResult` (with `cacheStatus`) instead of `Action[]`, and `attachCacheStatus` was extended to stamp `cacheStatus` on observe results. These are breaking behavioral changes to the API client, but no integration test under `packages/server/test` covers the new cache status surfacing or `cacheThreshold` parameter for observe. Per the rule, breaking changes to `packages/core/lib/v3/api.ts` must be covered by integration tests. Add tests in `packages/server/test/integration/v3/observe.test.ts` that verify (1) `cacheStatus` is present/correct on observe responses and (2) the `cacheThreshold` field is accepted and handled correctly.</violation>
</file>

<file name="packages/core/lib/v3/types/public/methods.ts">

<violation number="1" location="packages/core/lib/v3/types/public/methods.ts:93">
P2: `Action[] & { cacheStatus }` intersection is fragile: `cacheStatus` is silently dropped by `JSON.stringify()`, array spread, `.map()`, `.filter()`, and other common array operations, since they only preserve indexed elements. Consider wrapping the result in an object (e.g., `{ actions: Action[], cacheStatus?: ... }`) or using a subclass/helper to avoid this gotcha for API consumers.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

}: ClientObserveParameters): Promise<Action[]> {
// Strip non-serializable `page` from options before wire serialization
cacheThreshold,
}: ClientObserveParameters): Promise<ObserveResult> {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Custom agent: Any breaking changes to Stagehand REST API client / server implementation must be covered by an integration test under packages/server/test

The observe() method now returns ObserveResult (with cacheStatus) instead of Action[], and attachCacheStatus was extended to stamp cacheStatus on observe results. These are breaking behavioral changes to the API client, but no integration test under packages/server/test covers the new cache status surfacing or cacheThreshold parameter for observe. Per the rule, breaking changes to packages/core/lib/v3/api.ts must be covered by integration tests. Add tests in packages/server/test/integration/v3/observe.test.ts that verify (1) cacheStatus is present/correct on observe responses and (2) the cacheThreshold field is accepted and handled correctly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/api.ts, line 376:

<comment>The `observe()` method now returns `ObserveResult` (with `cacheStatus`) instead of `Action[]`, and `attachCacheStatus` was extended to stamp `cacheStatus` on observe results. These are breaking behavioral changes to the API client, but no integration test under `packages/server/test` covers the new cache status surfacing or `cacheThreshold` parameter for observe. Per the rule, breaking changes to `packages/core/lib/v3/api.ts` must be covered by integration tests. Add tests in `packages/server/test/integration/v3/observe.test.ts` that verify (1) `cacheStatus` is present/correct on observe responses and (2) the `cacheThreshold` field is accepted and handled correctly.</comment>

<file context>
@@ -372,7 +373,7 @@ export class StagehandAPIClient {
     frameId,
     cacheThreshold,
-  }: ClientObserveParameters): Promise<Action[]> {
+  }: ClientObserveParameters): Promise<ObserveResult> {
     // Strip non-serializable `page` and client-only `serverCache` from options before wire serialization
     let wireOptions: Api.ObserveRequest["options"];
</file context>
Fix with Cubic

serverCache?: boolean | { threshold: number };
}

export type ObserveResult = Action[] & { cacheStatus?: "HIT" | "MISS" };
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Action[] & { cacheStatus } intersection is fragile: cacheStatus is silently dropped by JSON.stringify(), array spread, .map(), .filter(), and other common array operations, since they only preserve indexed elements. Consider wrapping the result in an object (e.g., { actions: Action[], cacheStatus?: ... }) or using a subclass/helper to avoid this gotcha for API consumers.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/types/public/methods.ts, line 93:

<comment>`Action[] & { cacheStatus }` intersection is fragile: `cacheStatus` is silently dropped by `JSON.stringify()`, array spread, `.map()`, `.filter()`, and other common array operations, since they only preserve indexed elements. Consider wrapping the result in an object (e.g., `{ actions: Action[], cacheStatus?: ... }`) or using a subclass/helper to avoid this gotcha for API consumers.</comment>

<file context>
@@ -90,6 +90,8 @@ export interface ObserveOptions {
   serverCache?: boolean | { threshold: number };
 }
 
+export type ObserveResult = Action[] & { cacheStatus?: "HIT" | "MISS" };
+
 export enum V3FunctionName {
</file context>
Fix with Cubic


<V3Banner />

<Note>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs should now include the reference section per method and constructor

};

return this.execute<Action[]>({
return this.execute<ObserveResult>({
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this type changing

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants