Skip to content

add serverCacheThreshold option to act/observe/extract#2358

Open
sameelarif wants to merge 7 commits into
mainfrom
sameelarif/stg-2564-cache-configurability
Open

add serverCacheThreshold option to act/observe/extract#2358
sameelarif wants to merge 7 commits into
mainfrom
sameelarif/stg-2564-cache-configurability

Conversation

@sameelarif

@sameelarif sameelarif commented Jul 14, 2026

Copy link
Copy Markdown
Member

Summary

Adds the SDK side of caching configurability: a per-action override of the server cache validation threshold.

await stagehand.act("click submit", {
  serverCacheThreshold: 5,
});
  • serverCacheThreshold?: number (non-negative integer) added to ActOptions, ObserveOptions, and ExtractOptions
  • The API client maps it to options.cacheThreshold on the wire and strips the SDK-named field, following the existing serverCache pattern
  • Wire schemas (ActOptionsSchema / ObserveOptionsSchema / ExtractOptionsSchema) gain the cacheThreshold field
  • Ignored in local (non-API) mode, same as serverCache

Tests

  • Serialization tests: serverCacheThreshold maps to options.cacheThreshold for act/observe/extract and is never sent under its SDK name
  • Public type-shape tests updated for the new option

Summary by cubic

Adds a per-action server cache threshold and an opt-in flag for detailed cache metadata on act, observe, and extract. cacheStatus is still attached by default when caching is used, and all cache fields remain suppressed when caching is disabled.

  • New Features

    • Added serverCacheThreshold?: number to ActOptions, ObserveOptions, and ExtractOptions; sent as serverCacheThreshold and included in the wire schemas.
    • Added includeCacheMetadata?: boolean; when true, results include cacheHitCount, tokensSaved (input/output/total), and cacheMissReason on misses. cacheStatus is always present when caching is used.
    • Introduced TokenSavings type. Ignored in local (non-API) mode. Meets STG-2564, STG-2602, STG-2614.
  • Bug Fixes

    • Suppress all cache metadata (cacheStatus, cacheMissReason, cacheHitCount, tokensSaved) when serverCache: false, including for final buffered SSE events (STG-2602).
    • Expose server cacheCount as cacheHitCount on results when metadata is included (STG-2602).

Written for commit d91acab. Summary will update on new commits.

Review in cubic

Adds a per-action override of the server cache validation threshold.
The SDK option serverCacheThreshold maps to options.cacheThreshold on
the wire; the server persists it on the cache entry so later requests
that omit it keep honoring it.
@changeset-bot

changeset-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: d91acab

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 5 files

Confidence score: 5/5

  • In packages/core/lib/v3/types/public/api.ts, the cacheThreshold option is documented without its cloud-only limitation, so local-mode users may assume it works and misconfigure caching expectations; this is mainly a clarity risk rather than a runtime regression—mark it cloud-only (and ideally explicitly ignored in local mode) in all three option schema docs before or right after merge.
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/core/lib/v3/types/public/api.ts">

<violation number="1" location="packages/core/lib/v3/types/public/api.ts:627">
P3: Local-mode users cannot tell this option has no effect because its public schema docs omit the cloud-only limitation. Label `cacheThreshold` cloud-only (and ideally note it is ignored locally) in all three option schemas.

(Based on your team's feedback about cloud-only schema fields.) [FEEDBACK_USED]</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Client as User Code
    participant SDK as Stagehand SDK (api.ts)
    participant Execute as execute()
    participant API as Stagehand API Server
    participant Cache as Redis Cache Entry
    participant LD as LaunchDarkly Flag

    Note over Client,LD: NEW: serverCacheThreshold flow

    Client->>SDK: stagehand.act("click", { serverCacheThreshold: 5 })
    Note over SDK: destructure options,<br/>strip page, serverCache,<br/>extract serverCacheThreshold
    SDK->>SDK: map serverCacheThreshold → cacheThreshold
    SDK->>Execute: execute({<br/>method: "act",<br/>args: {<br/>  input: "click",<br/>  options: { timeout,<br/>            cacheThreshold: 5 }<br/>},<br/>serverCache: undefined<br/>})

    alt API mode
        Execute->>API: HTTP POST /act with wire options
        Note over API: Threshold resolution priority:<br/>1) request cacheThreshold<br/>2) eval env override<br/>3) sticky per-entry value<br/>4) project LD flag
        API->>Cache: GET cache entry (key excludes threshold)
        Cache-->>API: entry found (count=3, threshold=2)
        API->>Cache: CHANGED: update threshold to 5<br/>(persists on cache entry)
        API->>API: check: count (3) >= threshold (5)? → no
        API->>API: execute action (cache miss, compute)
        API-->>Execute: response with result
    else Local mode (SDK)
        Note over SDK: serverCacheThreshold ignored<br/>(same as serverCache)
        SDK->>Client: execute action locally
    end

    Execute-->>SDK: result
    SDK-->>Client: ActResult
Loading

Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

@@ -624,6 +624,11 @@ export const ActOptionsSchema = z
description: "Timeout in ms for the action",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: Local-mode users cannot tell this option has no effect because its public schema docs omit the cloud-only limitation. Label cacheThreshold cloud-only (and ideally note it is ignored locally) in all three option schemas.

(Based on your team's feedback about cloud-only schema fields.)

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/types/public/api.ts, line 627:

<comment>Local-mode users cannot tell this option has no effect because its public schema docs omit the cloud-only limitation. Label `cacheThreshold` cloud-only (and ideally note it is ignored locally) in all three option schemas.

(Based on your team's feedback about cloud-only schema fields.) </comment>

<file context>
@@ -624,6 +624,11 @@ export const ActOptionsSchema = z
       description: "Timeout in ms for the action",
       example: 30000,
     }),
+    cacheThreshold: z.number().int().nonnegative().optional().meta({
+      description:
+        "Per-action override of the server cache validation threshold — how many times this action must be seen before the cached result is served. Persists on the cache entry for later requests that omit it.",
</file context>

@sameelarif sameelarif changed the title [STG-2564] feat: add serverCacheThreshold option to act/observe/extract [STG-2564] add serverCacheThreshold option to act/observe/extract Jul 16, 2026
@sameelarif sameelarif changed the title [STG-2564] add serverCacheThreshold option to act/observe/extract add serverCacheThreshold option to act/observe/extract Jul 16, 2026

@seanmcguire12 seanmcguire12 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

add minor changeset

Comment thread packages/core/lib/v3/api.ts Outdated
const mappedOptions = {
...restOptions,
...(serverCacheThreshold !== undefined && {
cacheThreshold: serverCacheThreshold,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

assuming it would require a server side change but wouldn't it make more sense to have the both named serverCacheThreshold ?

When the server cache misses and reports why, attach the reason
(e.g. threshold, empty_array, timeout, error) to the result next to
cacheStatus. The server sends it in the finished SSE message; the
field is absent on hits and on first-time misses with no prior entry,
and against older servers that don't send it.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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

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/core/lib/v3/api.ts">

<violation number="1" location="packages/core/lib/v3/api.ts:949">
P2: The new `cacheMissReason` population logic in `attachCacheStatus` has no accompanying unit tests. The PR only adds type-shape tests, but the runtime behavior — that the reason is only attached on MISS, only when provided, and is suppressed when caching is disabled — is untested. Without coverage, regressions or silent breaks in this response-handling path (which can affect callers inspecting why a result missed the cache) won't be caught.</violation>
</file>

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

<violation number="1" location="packages/core/lib/v3/types/public/methods.ts:44">
P2: Extract schemas that legitimately return a `cacheMissReason` field now get an unusable intersection type and have their extracted value overwritten on cache misses. Keep cache metadata separate from arbitrary extraction keys (or reserve/strip this key before augmenting the result).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

* "empty_array", "timeout", "error"). Absent on hits and on first-time
* misses with no prior cache entry.
*/
cacheMissReason?: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Extract schemas that legitimately return a cacheMissReason field now get an unusable intersection type and have their extracted value overwritten on cache misses. Keep cache metadata separate from arbitrary extraction keys (or reserve/strip this key before augmenting the result).

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 44:

<comment>Extract schemas that legitimately return a `cacheMissReason` field now get an unusable intersection type and have their extracted value overwritten on cache misses. Keep cache metadata separate from arbitrary extraction keys (or reserve/strip this key before augmenting the result).</comment>

<file context>
@@ -36,11 +36,18 @@ export interface ActResult {
+   * "empty_array", "timeout", "error"). Absent on hits and on first-time
+   * misses with no prior cache entry.
+   */
+  cacheMissReason?: string;
 }
 
</file context>

Comment thread packages/core/lib/v3/api.ts Outdated
Comment thread packages/core/lib/v3/api.ts Outdated
finalCacheStatus === "MISS" &&
typeof eventData.data.cacheMissReason === "string"
) {
cacheAwareResult.cacheMissReason = eventData.data.cacheMissReason;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: The new cacheMissReason population logic in attachCacheStatus has no accompanying unit tests. The PR only adds type-shape tests, but the runtime behavior — that the reason is only attached on MISS, only when provided, and is suppressed when caching is disabled — is untested. Without coverage, regressions or silent breaks in this response-handling path (which can affect callers inspecting why a result missed the cache) won't be caught.

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 949:

<comment>The new `cacheMissReason` population logic in `attachCacheStatus` has no accompanying unit tests. The PR only adds type-shape tests, but the runtime behavior — that the reason is only attached on MISS, only when provided, and is suppressed when caching is disabled — is untested. Without coverage, regressions or silent breaks in this response-handling path (which can affect callers inspecting why a result missed the cache) won't be caught.</comment>

<file context>
@@ -936,9 +936,18 @@ export class StagehandAPIClient {
+        finalCacheStatus === "MISS" &&
+        typeof eventData.data.cacheMissReason === "string"
+      ) {
+        cacheAwareResult.cacheMissReason = eventData.data.cacheMissReason;
+      }
     }
</file context>

The API now accepts serverCacheThreshold directly, so drop the
SDK-to-wire rename and send the option under its own name. Server must
deploy before this releases: older servers strip the unknown key and
fall back to the default threshold.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 3 files (changes from recent commits).

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/core/tests/unit/api-client-serialization.test.ts">

<violation number="1" location="packages/core/tests/unit/api-client-serialization.test.ts:168">
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 PR description states that `serverCacheThreshold` should be mapped to `options.cacheThreshold` on the wire, following the existing `serverCache` pattern. However, looking at the API client in `packages/core/lib/v3/api.ts`, the `act()`, `observe()`, and `extract()` methods all destructure `serverCacheThreshold` from options and then spread it back into the wire payload under the same SDK name (`serverCacheThreshold`) — without renaming it to `cacheThreshold`. This means the wire schemas (which expect `cacheThreshold`) will never receive the cache threshold value. The unit tests were also updated to match the incorrect behavior, expecting `serverCacheThreshold` in the wire payload instead of `cacheThreshold`. Please update the API client to map `serverCacheThreshold` → `cacheThreshold` when building wire options, and fix the test expectations accordingly.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

input: "click the submit button",
options: {
timeout: 30000,
serverCacheThreshold: 5,

Copy link
Copy Markdown
Contributor

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 PR description states that serverCacheThreshold should be mapped to options.cacheThreshold on the wire, following the existing serverCache pattern. However, looking at the API client in packages/core/lib/v3/api.ts, the act(), observe(), and extract() methods all destructure serverCacheThreshold from options and then spread it back into the wire payload under the same SDK name (serverCacheThreshold) — without renaming it to cacheThreshold. This means the wire schemas (which expect cacheThreshold) will never receive the cache threshold value. The unit tests were also updated to match the incorrect behavior, expecting serverCacheThreshold in the wire payload instead of cacheThreshold. Please update the API client to map serverCacheThresholdcacheThreshold when building wire options, and fix the test expectations accordingly.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/tests/unit/api-client-serialization.test.ts, line 168:

<comment>The PR description states that `serverCacheThreshold` should be mapped to `options.cacheThreshold` on the wire, following the existing `serverCache` pattern. However, looking at the API client in `packages/core/lib/v3/api.ts`, the `act()`, `observe()`, and `extract()` methods all destructure `serverCacheThreshold` from options and then spread it back into the wire payload under the same SDK name (`serverCacheThreshold`) — without renaming it to `cacheThreshold`. This means the wire schemas (which expect `cacheThreshold`) will never receive the cache threshold value. The unit tests were also updated to match the incorrect behavior, expecting `serverCacheThreshold` in the wire payload instead of `cacheThreshold`. Please update the API client to map `serverCacheThreshold` → `cacheThreshold` when building wire options, and fix the test expectations accordingly.</comment>

<file context>
@@ -165,15 +165,15 @@ describe("StagehandAPIClient variable serialization", () => {
         options: {
           timeout: 30000,
-          cacheThreshold: 5,
+          serverCacheThreshold: 5,
         },
         frameId: undefined,
</file context>

sameelarif and others added 4 commits July 17, 2026 11:25
Apply cache gating to final buffered SSE events and cover cache miss reason attachment behavior so bypassed requests cannot expose server cache metadata.

Co-authored-by: Cursor <cursoragent@cursor.com>
Expose cache hit counts and input/output token savings on action results while preserving cache-bypass metadata suppression.

Co-authored-by: Cursor <cursoragent@cursor.com>
Use the clearer client-facing cacheHitCount name while retaining the server wire field for compatibility.

Co-authored-by: Cursor <cursoragent@cursor.com>
…data

Keep cacheStatus on results by default, and only attach cacheHitCount, tokensSaved, and cacheMissReason when callers opt in via includeCacheMetadata.

Co-authored-by: Cursor <cursoragent@cursor.com>

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

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/core/lib/v3/api.ts">

<violation number="1" location="packages/core/lib/v3/api.ts:310">
P3: `extract()` and `observe()` now have independent metadata-forwarding paths, but coverage exercises only `act()`, so a regression in either duplicated path will go unnoticed. Add parity serialization/result tests for both methods.

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

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// Strip non-serializable `page` and SDK-only fields from options before wire serialization
let wireOptions: Api.ActRequest["options"];
let serverCache: boolean | undefined;
let includeCacheMetadata: boolean | undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P3: extract() and observe() now have independent metadata-forwarding paths, but coverage exercises only act(), so a regression in either duplicated path will go unnoticed. Add parity serialization/result tests for both methods.

(Based on your team's feedback about adding 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 310:

<comment>`extract()` and `observe()` now have independent metadata-forwarding paths, but coverage exercises only `act()`, so a regression in either duplicated path will go unnoticed. Add parity serialization/result tests for both methods.

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

<file context>
@@ -302,15 +307,18 @@ export class StagehandAPIClient {
     // Strip non-serializable `page` and SDK-only fields from options before wire serialization
     let wireOptions: Api.ActRequest["options"];
     let serverCache: boolean | undefined;
+    let includeCacheMetadata: boolean | undefined;
     if (options) {
       const {
</file context>

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