@browserbasehq/eve 0.2.2 → 0.2.3-alpha-99e855dc16394f85652b080eac74f1053cea05b4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ import type { ExperimentalBatchCallback } from "@browserbasehq/stagehand";
2
+ import type { RefAction } from "./contract.js";
3
+ import { type PlaywrightCompatTelemetry } from "./runtime.js";
4
+ /**
5
+ * The JavaScript the facade sends to `experimentalBatch`. The SDK sends a
6
+ * callback's source text, so the exported *_SOURCE / RUN_PRELUDE / RUN_EPILOGUE
7
+ * strings are exactly what reaches the executor and go into the facade bundle
8
+ * unchanged for other hosts.
9
+ */
10
+ export type ActionInput = {
11
+ actions: RefAction[];
12
+ xpathById: Record<string, string>;
13
+ };
14
+ /** `staleId` is the first action ID that resolved to no XPath; no action ran. */
15
+ export type ActionResult = {
16
+ completed: number;
17
+ staleId?: never;
18
+ } | {
19
+ staleId: string;
20
+ completed?: never;
21
+ };
22
+ export type ScreenshotArtifact = {
23
+ path: string;
24
+ base64: string;
25
+ };
26
+ export type RunInput = {
27
+ hiddenPageIds?: string[];
28
+ quietWait?: boolean;
29
+ };
30
+ export type RunEnvelope = {
31
+ __stagehandPlaywrightCompat: true;
32
+ value: unknown;
33
+ executionError?: {
34
+ name: string;
35
+ message: string;
36
+ stack?: string;
37
+ };
38
+ telemetry: PlaywrightCompatTelemetry;
39
+ artifacts: ScreenshotArtifact[];
40
+ closeRequested: boolean;
41
+ batchRuntimeMs: number;
42
+ };
43
+ /** createPlaywrightCompatRuntime as one self-contained function source. */
44
+ export declare const RUNTIME_SOURCE: string;
45
+ /** Resolves snapshot IDs against the page's stored XPath map, then runs the actions. */
46
+ export declare const actionRunner: ExperimentalBatchCallback<ActionInput, ActionResult>;
47
+ export declare const ACTION_RUNNER_SOURCE: string;
48
+ /** The source of `runCallback(code)` is `RUN_PRELUDE + code + RUN_EPILOGUE`. */
49
+ export declare const RUN_PRELUDE: string;
50
+ export declare const RUN_EPILOGUE: string;
51
+ /**
52
+ * Agent code runs with Playwright-shaped page, context and browser in scope and
53
+ * resolves to a RunEnvelope. The AsyncFunction constructor parses `code` as a
54
+ * whole function body, so it cannot close the wrapper early.
55
+ */
56
+ export declare function runCallback(code: string): ExperimentalBatchCallback<RunInput, RunEnvelope>;
@@ -0,0 +1,84 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { createPlaywrightCompatRuntime } from "./runtime.mjs";
8
+ const AsyncFunction = Object.getPrototypeOf(async function() {}).constructor;
9
+ const dynamicFunctionHead = (params) => `async function anonymous(${params.join(",")}\n) {\n`;
10
+ const DYNAMIC_FUNCTION_TAIL = "\n}";
11
+ const RUNTIME_SOURCE = createPlaywrightCompatRuntime.toString();
12
+ const ACTION_RUNNER_BODY = `"use strict";
13
+ const resolveSnapshotXPath = (id) => {
14
+ const exact = input.xpathById[id];
15
+ if (exact !== undefined) return exact;
16
+ if (id.includes("-")) return undefined;
17
+ const suffix = "-" + id;
18
+ const matches = Object.keys(input.xpathById).filter((key) => key.endsWith(suffix));
19
+ return matches.length === 1 ? input.xpathById[matches[0]] : undefined;
20
+ };
21
+ const selectors = [];
22
+ for (const action of input.actions) {
23
+ const xpath = resolveSnapshotXPath(action.id)?.replace(/\\/text\\(\\)(\\[\\d+\\])?$/iu, "");
24
+ if (!xpath) return { staleId: action.id };
25
+ selectors.push("xpath=" + xpath);
26
+ }
27
+ let completed = 0;
28
+ for (const [index, action] of input.actions.entries()) {
29
+ const locator = stagehand.page.locator(selectors[index]);
30
+ switch (action.op) {
31
+ case "click": await locator.click(); break;
32
+ case "hover": await locator.hover(); break;
33
+ case "fill": await locator.fill(action.value); break;
34
+ case "type": await locator.type(action.text, action.delay === undefined ? undefined : { delay: action.delay }); break;
35
+ case "press": await locator.click(); await stagehand.page.keyPress(action.key); break;
36
+ case "select": await locator.selectOption(action.values); break;
37
+ default: throw new Error("Unsupported ref action: " + String(action.op));
38
+ }
39
+ completed += 1;
40
+ }
41
+ return { completed };`;
42
+ const RUN_PARAMS = ["batchStagehand", "input"];
43
+ const RUN_PRELUDE_BODY = `"use strict";
44
+ const __stagehandCompatIdentity = (target) => target;
45
+ for (let index = 0; index <= 32; index += 1) {
46
+ globalThis[index === 0 ? "__name" : "__name" + index] = __stagehandCompatIdentity;
47
+ }
48
+ const createRuntime = ${RUNTIME_SOURCE};
49
+ const runtime = await createRuntime(batchStagehand, { hiddenPageIds: input.hiddenPageIds ?? [], quietWait: input.quietWait === true });
50
+ const page = runtime.page;
51
+ const context = runtime.context;
52
+ const browser = runtime.browser;
53
+ const console = globalThis.console;
54
+ const __stagehandBatchStartedAt = performance.now();
55
+ let value;
56
+ let executionError;
57
+ try {
58
+ value = await (async () => { `;
59
+ const RUN_EPILOGUE_BODY = `
60
+ })();
61
+ } catch (error) {
62
+ executionError = {
63
+ name: typeof error?.name === "string" ? error.name : "Error",
64
+ message: typeof error?.message === "string" ? error.message : String(error),
65
+ ...(typeof error?.stack === "string" ? { stack: error.stack } : {}),
66
+ };
67
+ }
68
+ return {
69
+ __stagehandPlaywrightCompat: true,
70
+ value,
71
+ executionError,
72
+ telemetry: runtime.telemetry(),
73
+ artifacts: runtime.artifacts(),
74
+ closeRequested: runtime.closeRequested(),
75
+ batchRuntimeMs: performance.now() - __stagehandBatchStartedAt,
76
+ };`;
77
+ const actionRunner = new AsyncFunction("stagehand", "input", ACTION_RUNNER_BODY);
78
+ const ACTION_RUNNER_SOURCE = Function.prototype.toString.call(actionRunner);
79
+ const RUN_PRELUDE = dynamicFunctionHead(RUN_PARAMS) + RUN_PRELUDE_BODY;
80
+ const RUN_EPILOGUE = RUN_EPILOGUE_BODY + DYNAMIC_FUNCTION_TAIL;
81
+ function runCallback(code) {
82
+ return new AsyncFunction(...RUN_PARAMS, RUN_PRELUDE_BODY + code + RUN_EPILOGUE_BODY);
83
+ }
84
+ export { ACTION_RUNNER_SOURCE, RUNTIME_SOURCE, RUN_EPILOGUE, RUN_PRELUDE, actionRunner, runCallback };
@@ -716,9 +716,11 @@ export declare const SNAPSHOT_ACTIONS_WORKFLOW = "Workflow: after navigating or
716
716
  export declare const SNAPSHOT_TOOL_DESCRIPTION_EXPLICIT = "Capture the active page's Stagehand accessibility tree and hydrate its displayed IDs for subsequent run actions. Every call replaces the active page's ID map. Call it after every navigation or page change and before acting by ID.";
717
717
  export declare const SCREENSHOT_TOOL_DESCRIPTION_EXPLICIT = "Capture a screenshot of the active page. For size-constrained MCP clients, prefer a viewport JPEG: {\"type\":\"jpeg\",\"quality\":40,\"fullPage\":false}. Use it to verify the visible state before reporting done, on visual or canvas content, and when text evidence is ambiguous.";
718
718
  export declare const RUN_TOOL_DESCRIPTION_EXPLICIT = "Browse and automate websites in the persistent browser by executing JavaScript against a Playwright-shaped API. The code runs inside an async function with page, context, and browser in scope (Playwright Page, BrowserContext, and Browser); use await directly and return a JSON-serializable value when useful. Navigate with await page.goto(\"https://example.com\"); there is no separate navigate or start tool. Alternatively, provide a batch of actions using IDs from the latest snapshot. Provide exactly one of code or actions. Each action must use \"op\" (never \"kind\") and \"id\" (never \"ref\"). Copy the bracketed snapshot ID as a string. Examples: {\"actions\":[{\"op\":\"click\",\"id\":\"1-42\"}]}, {\"actions\":[{\"op\":\"fill\",\"id\":\"2-14\",\"value\":\"Miami\"}]}, {\"actions\":[{\"op\":\"select\",\"id\":\"3-9\",\"values\":\"Lowest price\"}]}. Prefer \"actions\" with snapshot IDs for simple clicks/fills/selects on visible elements; use \"code\" for multi-step logic and extraction.";
719
- export declare function explicitSnapshotActionsEnabled(env?: NodeJS.ProcessEnv): boolean;
719
+ /** Environment variables as read by the facade; `process.env` satisfies it. */
720
+ export type FacadeEnv = Readonly<Record<string, string | undefined>>;
721
+ export declare function explicitSnapshotActionsEnabled(env?: FacadeEnv): boolean;
720
722
  export declare const FACADE_TOOLS_EXPLICIT_SNAPSHOT: typeof FACADE_TOOLS;
721
- export declare function facadeToolsFor(surface: FacadeSurface, env?: NodeJS.ProcessEnv): readonly [{
723
+ export declare function facadeToolsFor(surface: FacadeSurface, env?: FacadeEnv): readonly [{
722
724
  readonly name: "run";
723
725
  readonly description: "Browse and automate websites in the persistent browser by executing JavaScript against a Playwright-shaped API. The code runs inside an async function with page, context, and browser in scope (Playwright Page, BrowserContext, and Browser); use await directly and return a JSON-serializable value when useful. Navigate with await page.goto(\"https://example.com\"); there is no separate navigate or start tool. Alternatively, provide a batch of actions using IDs from the latest snapshot. Provide exactly one of code or actions. Each action must use \"op\" (never \"kind\") and \"id\" (never \"ref\"). Copy the bracketed snapshot ID as a string. Examples: {\"actions\":[{\"op\":\"click\",\"id\":\"1-42\"}]}, {\"actions\":[{\"op\":\"fill\",\"id\":\"2-14\",\"value\":\"Miami\"}]}, {\"actions\":[{\"op\":\"select\",\"id\":\"3-9\",\"values\":\"Lowest price\"}]}.";
724
726
  readonly inputSchema: {
@@ -997,7 +999,7 @@ export declare function facadeToolsFor(surface: FacadeSurface, env?: NodeJS.Proc
997
999
  readonly additionalProperties: false;
998
1000
  };
999
1001
  }];
1000
- export declare function facadeAgentInstructions(env?: NodeJS.ProcessEnv): string;
1002
+ export declare function facadeAgentInstructions(env?: FacadeEnv): string;
1001
1003
  /**
1002
1004
  * Canonical agent system prompt for the facade tool surface. Host examples
1003
1005
  * (Eve, Vercel AI SDK, deepagents) should use this text rather than authoring
@@ -1013,6 +1015,50 @@ export declare const NO_HYDRATED_SNAPSHOT_ERROR = "No hydrated snapshot exists f
1013
1015
  export declare const NAVIGATED_SNAPSHOT_ERROR = "The active page navigated after its snapshot; call snapshot again.";
1014
1016
  export declare const STALE_SNAPSHOT_ID_ERROR = "Snapshot ID \"${id}\" is stale or not actionable; call snapshot again.";
1015
1017
  export declare function staleSnapshotIdError(id: string): string;
1018
+ export declare const SCREENSHOT_ARTIFACT_PATH_ERROR = "Screenshot artifact path must stay within artifactRoot.";
1019
+ export declare const SCREENSHOT_ARTIFACT_SYMLINK_ERROR = "Screenshot artifact directory must not be a symlink.";
1020
+ export declare const SCREENSHOT_ARTIFACT_FILE_SYMLINK_ERROR = "Screenshot artifact file must not be a symlink.";
1021
+ export declare const FACADE_BROWSER_CLOSED_ERROR = "Stagehand facade browser is closed.";
1022
+ /**
1023
+ * Part of the error a Stagehand SDK raises from `context.activePage()` when the
1024
+ * focused window's active tab is not one Stagehand registered (another window
1025
+ * has focus). Hosts then act on the newest registered tab instead of failing.
1026
+ */
1027
+ export declare const UNMANAGED_ACTIVE_PAGE_ERROR_MARKER = "active target not registered";
1028
+ /**
1029
+ * Caps on what one `run` batch may hand back to its host. They bound memory and
1030
+ * disk use against a runaway or hostile batch, and sit far above any real agent
1031
+ * run: a full-page PNG is a few MB, and a 10 MB value is already far more than a
1032
+ * model can read. Value size is the UTF-8 length of the value's compact JSON.
1033
+ */
1034
+ export declare const RUN_RESULT_LIMITS: Readonly<{
1035
+ readonly maxArtifacts: 32;
1036
+ readonly maxArtifactBytes: number;
1037
+ readonly maxTotalArtifactBytes: number;
1038
+ readonly maxValueBytes: number;
1039
+ }>;
1040
+ /**
1041
+ * Run options a host passes in the `run` batch input; all are off unless the
1042
+ * host turns them on. `quietWait`: page.waitForTimeout(ms) returns once the
1043
+ * page is quiet (readyState past "loading", no mutation in the main document
1044
+ * for `windowMs`; shadow roots and iframes are not observed), checked in the
1045
+ * page every `pollMs` through page.waitForFunction, never later than ms; at or
1046
+ * below `windowMs` it just sleeps, as it does on pages without waitForFunction. The runtime (src/facade/runtime.ts) holds the same numbers.
1047
+ */
1048
+ export declare const RUN_OPTIONS: {
1049
+ readonly quietWait: {
1050
+ readonly default: false;
1051
+ readonly windowMs: 400;
1052
+ readonly pollMs: 100;
1053
+ };
1054
+ };
1055
+ export declare const RUN_RESULT_MALFORMED_ERROR = "The run batch returned a malformed result.";
1056
+ export declare const SCREENSHOT_ARTIFACT_COUNT_ERROR: string;
1057
+ export declare const SCREENSHOT_ARTIFACT_SIZE_ERROR: string;
1058
+ export declare const SCREENSHOT_ARTIFACT_TOTAL_SIZE_ERROR: string;
1059
+ export declare const RUN_VALUE_SIZE_ERROR: string;
1060
+ export declare const SCREENSHOT_CAPTURED_MESSAGE = "Screenshot captured.";
1061
+ export declare const SCREENSHOT_COMPRESSED_MESSAGE = "Screenshot captured with transport-safe compression.";
1016
1062
  /**
1017
1063
  * Prefix of the terminal error every facade tool returns once the browser
1018
1064
  * session is gone. Harnesses match on it to tell consequences from agent errors.
@@ -162,6 +162,30 @@ const STALE_SNAPSHOT_ID_ERROR = "Snapshot ID \"${id}\" is stale or not actionabl
162
162
  function staleSnapshotIdError(id) {
163
163
  return STALE_SNAPSHOT_ID_ERROR.replace("${id}", id);
164
164
  }
165
+ const SCREENSHOT_ARTIFACT_PATH_ERROR = "Screenshot artifact path must stay within artifactRoot.";
166
+ const SCREENSHOT_ARTIFACT_SYMLINK_ERROR = "Screenshot artifact directory must not be a symlink.";
167
+ const SCREENSHOT_ARTIFACT_FILE_SYMLINK_ERROR = "Screenshot artifact file must not be a symlink.";
168
+ const FACADE_BROWSER_CLOSED_ERROR = "Stagehand facade browser is closed.";
169
+ const UNMANAGED_ACTIVE_PAGE_ERROR_MARKER = "active target not registered";
170
+ const MB = 1048576;
171
+ const RUN_RESULT_LIMITS = Object.freeze({
172
+ maxArtifacts: 32,
173
+ maxArtifactBytes: 25 * MB,
174
+ maxTotalArtifactBytes: 100 * MB,
175
+ maxValueBytes: 10 * MB
176
+ });
177
+ const RUN_OPTIONS = { quietWait: {
178
+ default: false,
179
+ windowMs: 400,
180
+ pollMs: 100
181
+ } };
182
+ const RUN_RESULT_MALFORMED_ERROR = "The run batch returned a malformed result.";
183
+ const SCREENSHOT_ARTIFACT_COUNT_ERROR = `A run can save at most ${RUN_RESULT_LIMITS.maxArtifacts} screenshot artifacts.`;
184
+ const SCREENSHOT_ARTIFACT_SIZE_ERROR = `A screenshot artifact can be at most ${RUN_RESULT_LIMITS.maxArtifactBytes / MB} MB.`;
185
+ const SCREENSHOT_ARTIFACT_TOTAL_SIZE_ERROR = `A run's screenshot artifacts can total at most ${RUN_RESULT_LIMITS.maxTotalArtifactBytes / MB} MB.`;
186
+ const RUN_VALUE_SIZE_ERROR = `A run can return at most ${RUN_RESULT_LIMITS.maxValueBytes / MB} MB of JSON; return less data.`;
187
+ const SCREENSHOT_CAPTURED_MESSAGE = "Screenshot captured.";
188
+ const SCREENSHOT_COMPRESSED_MESSAGE = "Screenshot captured with transport-safe compression.";
165
189
  const BROWSER_SESSION_LOST_ERROR_PREFIX = "Browser session lost (";
166
190
  const BROWSER_SESSION_LOST_ERROR = "Browser session lost (${cause}). The task cannot continue; report your final result now.";
167
191
  const SESSION_LOST_TELEMETRY_PREFIX = "stagehand_facade_session_lost ";
@@ -212,4 +236,4 @@ const ScreenshotInputSchema = z.strictObject({
212
236
  type: z.enum(["png", "jpeg"]).optional(),
213
237
  quality: z.number().min(0).max(100).optional()
214
238
  });
215
- export { BROWSER_SESSION_LOST_ERROR, BROWSER_SESSION_LOST_ERROR_PREFIX, CodeModeRunInputSchema, FACADE_AGENT_INSTRUCTIONS, FACADE_LEGACY_TOOLS, FACADE_TOOLS, FACADE_TOOLS_EXPLICIT_SNAPSHOT, LEGACY_FACADE_AGENT_INSTRUCTIONS, LEGACY_RUN_TOOL_DESCRIPTION, NAVIGATED_SNAPSHOT_ERROR, NO_HYDRATED_SNAPSHOT_ERROR, RUN_INPUT_SCHEMA, RUN_TOOL_DESCRIPTION, RUN_TOOL_DESCRIPTION_EXPLICIT, RefActionSchema, SCREENSHOT_INPUT_SCHEMA, SCREENSHOT_TOOL_DESCRIPTION, SCREENSHOT_TOOL_DESCRIPTION_EXPLICIT, SESSION_INFO_TOOL_NAME, SESSION_LOST_TELEMETRY_PREFIX, SNAPSHOT_ACTIONS_WORKFLOW, SNAPSHOT_INPUT_SCHEMA, SNAPSHOT_TOOL_DESCRIPTION, SNAPSHOT_TOOL_DESCRIPTION_EXPLICIT, STALE_SNAPSHOT_ID_ERROR, ScreenshotInputSchema, SnapshotInputSchema, browserSessionLostError, explicitSnapshotActionsEnabled, facadeAgentInstructions, facadeSurfaceFromArgs, facadeToolsFor, facadeToolsForSurface, isBrowserSessionLostError, staleSnapshotIdError };
239
+ export { BROWSER_SESSION_LOST_ERROR, BROWSER_SESSION_LOST_ERROR_PREFIX, CodeModeRunInputSchema, FACADE_AGENT_INSTRUCTIONS, FACADE_BROWSER_CLOSED_ERROR, FACADE_LEGACY_TOOLS, FACADE_TOOLS, FACADE_TOOLS_EXPLICIT_SNAPSHOT, LEGACY_FACADE_AGENT_INSTRUCTIONS, LEGACY_RUN_TOOL_DESCRIPTION, NAVIGATED_SNAPSHOT_ERROR, NO_HYDRATED_SNAPSHOT_ERROR, RUN_INPUT_SCHEMA, RUN_OPTIONS, RUN_RESULT_LIMITS, RUN_RESULT_MALFORMED_ERROR, RUN_TOOL_DESCRIPTION, RUN_TOOL_DESCRIPTION_EXPLICIT, RUN_VALUE_SIZE_ERROR, RefActionSchema, SCREENSHOT_ARTIFACT_COUNT_ERROR, SCREENSHOT_ARTIFACT_FILE_SYMLINK_ERROR, SCREENSHOT_ARTIFACT_PATH_ERROR, SCREENSHOT_ARTIFACT_SIZE_ERROR, SCREENSHOT_ARTIFACT_SYMLINK_ERROR, SCREENSHOT_ARTIFACT_TOTAL_SIZE_ERROR, SCREENSHOT_CAPTURED_MESSAGE, SCREENSHOT_COMPRESSED_MESSAGE, SCREENSHOT_INPUT_SCHEMA, SCREENSHOT_TOOL_DESCRIPTION, SCREENSHOT_TOOL_DESCRIPTION_EXPLICIT, SESSION_INFO_TOOL_NAME, SESSION_LOST_TELEMETRY_PREFIX, SNAPSHOT_ACTIONS_WORKFLOW, SNAPSHOT_INPUT_SCHEMA, SNAPSHOT_TOOL_DESCRIPTION, SNAPSHOT_TOOL_DESCRIPTION_EXPLICIT, STALE_SNAPSHOT_ID_ERROR, ScreenshotInputSchema, SnapshotInputSchema, UNMANAGED_ACTIVE_PAGE_ERROR_MARKER, browserSessionLostError, explicitSnapshotActionsEnabled, facadeAgentInstructions, facadeSurfaceFromArgs, facadeToolsFor, facadeToolsForSurface, isBrowserSessionLostError, staleSnapshotIdError };
@@ -41,6 +41,10 @@ type RawPage = {
41
41
  waitForLoadState(state: string, timeout?: number): Promise<void>;
42
42
  waitForTimeout(ms: number): Promise<void>;
43
43
  waitForSelector(selector: string, options?: Record<string, unknown>): Promise<boolean>;
44
+ waitForFunction?(expression: string, arg?: undefined, options?: {
45
+ polling?: "raf" | number;
46
+ timeout?: number;
47
+ }): Promise<unknown>;
44
48
  url(): Promise<string>;
45
49
  title(): Promise<string>;
46
50
  close(): Promise<void>;
@@ -88,6 +92,17 @@ type BatchStagehandRuntime = {
88
92
  export type PlaywrightCompatTelemetry = {
89
93
  calls: Record<string, number>;
90
94
  misses: Record<string, number>;
95
+ /**
96
+ * page.waitForTimeout calls: the time agent code asked for and the time it
97
+ * waited (capped at the request). They differ only with `quietWait`, where
98
+ * earlyReturns counts the waits that ended because the page was quiet.
99
+ */
100
+ waits: {
101
+ count: number;
102
+ requestedMs: number;
103
+ waitedMs: number;
104
+ earlyReturns: number;
105
+ };
91
106
  };
92
107
  export type PlaywrightCompatRuntime = {
93
108
  page: unknown;
@@ -108,6 +123,13 @@ export type PlaywrightCompatRuntime = {
108
123
  export type PlaywrightCompatRuntimeOptions = {
109
124
  /** Pages the host keeps for itself (the facade's keeper tab); never surfaced to agent code. */
110
125
  hiddenPageIds?: string[];
126
+ /**
127
+ * page.waitForTimeout(ms) returns once the page is quiet (DOM content loaded
128
+ * and no mutation in the main document for 400 ms; shadow roots and iframes
129
+ * are not observed), never later than ms. Waits of 400 ms or less always
130
+ * sleep in full. Off by default: the wait then sleeps exactly ms.
131
+ */
132
+ quietWait?: boolean;
111
133
  };
112
134
  export declare function createPlaywrightCompatRuntime(stagehand: BatchStagehandRuntime, options?: PlaywrightCompatRuntimeOptions): Promise<PlaywrightCompatRuntime>;
113
135
  export {};
@@ -13,7 +13,13 @@ async function createPlaywrightCompatRuntime(stagehand, options = {}) {
13
13
  };
14
14
  const stats = {
15
15
  calls: {},
16
- misses: {}
16
+ misses: {},
17
+ waits: {
18
+ count: 0,
19
+ requestedMs: 0,
20
+ waitedMs: 0,
21
+ earlyReturns: 0
22
+ }
17
23
  };
18
24
  const record = (bucket, method) => {
19
25
  stats[bucket][method] = (stats[bucket][method] ?? 0) + 1;
@@ -634,6 +640,55 @@ async function createPlaywrightCompatRuntime(stagehand, options = {}) {
634
640
  const pageKey = (page) => page.pageId ?? page;
635
641
  const hiddenPageIds = new Set(options.hiddenPageIds ?? []);
636
642
  const visibleRawPages = async () => (await rawContext.pages()).filter((page) => page.pageId === void 0 || !hiddenPageIds.has(page.pageId));
643
+ const QUIET_WAIT_WINDOW_MS = 400;
644
+ const QUIET_WAIT_POLL_MS = 100;
645
+ const quietWaitPredicate = (key) => `(() => {
646
+ const key = ${JSON.stringify(key)};
647
+ let state = document[key];
648
+ if (!state) {
649
+ state = { lastMutation: performance.now() };
650
+ state.observer = new MutationObserver(() => {
651
+ state.lastMutation = performance.now();
652
+ });
653
+ Object.defineProperty(document, key, { value: state, configurable: true });
654
+ state.observer.observe(document, { subtree: true, childList: true, attributes: true, characterData: true });
655
+ }
656
+ return document.readyState !== "loading" && performance.now() - state.lastMutation >= ${QUIET_WAIT_WINDOW_MS};
657
+ })()`;
658
+ const quietWaitCleanup = (key) => `(() => {
659
+ const key = ${JSON.stringify(key)};
660
+ const state = document[key];
661
+ if (state) {
662
+ state.observer.disconnect();
663
+ delete document[key];
664
+ }
665
+ })()`;
666
+ const waitForTimeoutOrQuiet = async (page, ms) => {
667
+ const requestedMs = Number.isFinite(ms) && ms > 0 ? ms : 0;
668
+ stats.waits.count += 1;
669
+ stats.waits.requestedMs += requestedMs;
670
+ const waitForFunction = page.waitForFunction;
671
+ if (!options.quietWait || !(requestedMs > QUIET_WAIT_WINDOW_MS) || typeof waitForFunction !== "function") {
672
+ await page.waitForTimeout(ms);
673
+ stats.waits.waitedMs += requestedMs;
674
+ return;
675
+ }
676
+ const key = `_${crypto.randomUUID()}`;
677
+ const startedAt = Date.now();
678
+ let timer;
679
+ const early = await Promise.race([waitForFunction.call(page, quietWaitPredicate(key), void 0, {
680
+ polling: QUIET_WAIT_POLL_MS,
681
+ timeout: requestedMs
682
+ }).then(() => true, () => false), new Promise((resolve) => {
683
+ timer = setTimeout(() => resolve(false), requestedMs);
684
+ })]);
685
+ clearTimeout(timer);
686
+ const left = startedAt + requestedMs - Date.now();
687
+ if (!early && left > 0) await page.waitForTimeout(left);
688
+ stats.waits.waitedMs += Math.min(Date.now() - startedAt, requestedMs);
689
+ if (early) stats.waits.earlyReturns += 1;
690
+ Promise.resolve().then(() => page.evaluate(quietWaitCleanup(key))).catch(() => void 0);
691
+ };
637
692
  const compatPages = new Map();
638
693
  const closedPages = new Set();
639
694
  let closeRequested = false;
@@ -2068,7 +2123,7 @@ async function createPlaywrightCompatRuntime(stagehand, options = {}) {
2068
2123
  },
2069
2124
  waitForTimeout: async (ms) => {
2070
2125
  record("calls", "page.waitForTimeout");
2071
- await page.waitForTimeout(ms);
2126
+ await waitForTimeoutOrQuiet(page, ms);
2072
2127
  for (const candidate of await visibleRawPages()) await createPage(candidate);
2073
2128
  },
2074
2129
  waitForLoadState: (state = "load", options = {}) => {
@@ -2428,7 +2483,8 @@ async function createPlaywrightCompatRuntime(stagehand, options = {}) {
2428
2483
  }),
2429
2484
  telemetry: () => ({
2430
2485
  calls: { ...stats.calls },
2431
- misses: { ...stats.misses }
2486
+ misses: { ...stats.misses },
2487
+ waits: { ...stats.waits }
2432
2488
  }),
2433
2489
  artifacts: () => screenshotArtifacts.map((artifact) => ({ ...artifact })),
2434
2490
  closeRequested: () => closeRequested
@@ -1,8 +1,10 @@
1
1
  import type { Stagehand } from "@browserbasehq/stagehand";
2
2
  import { type FacadeSessionLoss, type RefAction } from "./contract.js";
3
- import { type PlaywrightCompatTelemetry } from "./runtime.js";
3
+ import type { PlaywrightCompatTelemetry } from "./runtime.js";
4
4
  export type StagehandFacadeRunReport = {
5
5
  telemetry: PlaywrightCompatTelemetry;
6
+ /** Whether the run used StagehandFacadeToolsOptions.quietWait. */
7
+ quietWait: boolean;
6
8
  /** Wall-clock time of the whole experimentalBatch round trip. */
7
9
  batchRoundTripMs: number;
8
10
  /** Time the agent's code spent executing inside the batch. */
@@ -43,6 +45,13 @@ export type StagehandFacadeToolsOptions = {
43
45
  * (`context.pages()` / `waitForEvent("page")` never list it).
44
46
  */
45
47
  keeperPage?: boolean;
48
+ /**
49
+ * Experimental, off by default: page.waitForTimeout(ms) longer than 400 ms
50
+ * in run code returns once the main document is quiet, capped at ms (see
51
+ * RUN_OPTIONS.quietWait). The run
52
+ * report's telemetry.waits shows requested against waited time.
53
+ */
54
+ quietWait?: boolean;
46
55
  };
47
56
  /** Every facade tool returns this once the browser session is gone. */
48
57
  export declare class StagehandFacadeSessionLostError extends Error {
@@ -101,6 +110,9 @@ export declare class StagehandFacadeTools {
101
110
  * The page the agent is working on. A closed or crashed tab leaves the
102
111
  * context without an active page (or with only the keeper); the agent then
103
112
  * gets a fresh blank page rather than a dead end, and the keeper stays hidden.
113
+ * When another window has focus on a tab Stagehand does not manage, the agent
114
+ * keeps working on the newest tab Stagehand registered; nothing is reordered,
115
+ * since activating a tab does not move window focus.
104
116
  */
105
117
  private activePage;
106
118
  /**
@@ -4,8 +4,8 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- import { NAVIGATED_SNAPSHOT_ERROR, NO_HYDRATED_SNAPSHOT_ERROR, RefActionSchema, browserSessionLostError, staleSnapshotIdError } from "./contract.mjs";
8
- import { createPlaywrightCompatRuntime } from "./runtime.mjs";
7
+ import { actionRunner, runCallback } from "./batch-sources.mjs";
8
+ import { FACADE_BROWSER_CLOSED_ERROR, NAVIGATED_SNAPSHOT_ERROR, NO_HYDRATED_SNAPSHOT_ERROR, RUN_OPTIONS, RUN_RESULT_LIMITS, RUN_RESULT_MALFORMED_ERROR, RUN_VALUE_SIZE_ERROR, RefActionSchema, SCREENSHOT_ARTIFACT_COUNT_ERROR, SCREENSHOT_ARTIFACT_FILE_SYMLINK_ERROR, SCREENSHOT_ARTIFACT_PATH_ERROR, SCREENSHOT_ARTIFACT_SIZE_ERROR, SCREENSHOT_ARTIFACT_SYMLINK_ERROR, SCREENSHOT_ARTIFACT_TOTAL_SIZE_ERROR, browserSessionLostError, staleSnapshotIdError } from "./contract.mjs";
9
9
  import { sanitizeErrorMessage } from "../harness/redact.mjs";
10
10
  import fsp from "node:fs/promises";
11
11
  import { constants } from "node:fs";
@@ -41,58 +41,7 @@ var StagehandFacadeSessionLostError = class extends Error {
41
41
  };
42
42
  const RUN_BATCH_TIMEOUT_MS = 6e4;
43
43
  const PAGE_CAPTURE_DEADLINE_MS = 12e4;
44
- const AsyncFunction = Object.getPrototypeOf(async function() {}).constructor;
45
- const actionRunner = new AsyncFunction("stagehand", "input", `"use strict";
46
- let completed = 0;
47
- for (const action of input.actions) {
48
- const locator = stagehand.page.locator(action.selector);
49
- switch (action.op) {
50
- case "click": await locator.click(); break;
51
- case "hover": await locator.hover(); break;
52
- case "fill": await locator.fill(action.value); break;
53
- case "type": await locator.type(action.text, action.delay === undefined ? undefined : { delay: action.delay }); break;
54
- case "press": await locator.click(); await stagehand.page.keyPress(action.key); break;
55
- case "select": await locator.selectOption(action.values); break;
56
- default: throw new Error("Unsupported ref action: " + String(action.op));
57
- }
58
- completed += 1;
59
- }
60
- return { completed };`);
61
44
  const FACADE_KEEPER_PAGE_URL = "about:blank";
62
- const FACADE_PRELUDE = `"use strict";
63
- const __stagehandCompatIdentity = (target) => target;
64
- for (let index = 0; index <= 32; index += 1) {
65
- globalThis[index === 0 ? "__name" : "__name" + index] = __stagehandCompatIdentity;
66
- }
67
- const createRuntime = ${createPlaywrightCompatRuntime.toString()};
68
- const runtime = await createRuntime(batchStagehand, { hiddenPageIds: input.hiddenPageIds ?? [] });
69
- const page = runtime.page;
70
- const context = runtime.context;
71
- const browser = runtime.browser;
72
- const console = globalThis.console;
73
- const __stagehandBatchStartedAt = performance.now();
74
- let value;
75
- let executionError;
76
- try {
77
- value = await (async () => { `;
78
- const FACADE_EPILOGUE = `
79
- })();
80
- } catch (error) {
81
- executionError = {
82
- name: typeof error?.name === "string" ? error.name : "Error",
83
- message: typeof error?.message === "string" ? error.message : String(error),
84
- ...(typeof error?.stack === "string" ? { stack: error.stack } : {}),
85
- };
86
- }
87
- return {
88
- __stagehandPlaywrightCompat: true,
89
- value,
90
- executionError,
91
- telemetry: runtime.telemetry(),
92
- artifacts: runtime.artifacts(),
93
- closeRequested: runtime.closeRequested(),
94
- batchRuntimeMs: performance.now() - __stagehandBatchStartedAt,
95
- };`;
96
45
  var StagehandFacadeTools = class StagehandFacadeTools {
97
46
  stagehand;
98
47
  options;
@@ -184,40 +133,44 @@ var StagehandFacadeTools = class StagehandFacadeTools {
184
133
  this.snapshotsByPage.delete(page.pageId);
185
134
  throw new StagehandFacadeInputError(NAVIGATED_SNAPSHOT_ERROR);
186
135
  }
187
- const hydrated = parsed.map((action) => {
188
- const xpath = trimTrailingTextNode(resolveSnapshotXPath(snapshot.xpathById, action.id));
189
- if (!xpath) throw new StagehandFacadeInputError(staleSnapshotIdError(action.id));
190
- return {
191
- ...action,
192
- selector: `xpath=${xpath}`
193
- };
136
+ const result = await this.stagehand.experimentalBatch(actionRunner, {
137
+ actions: parsed,
138
+ xpathById: snapshot.xpathById
139
+ }, {
140
+ page,
141
+ timeout: RUN_BATCH_TIMEOUT_MS
194
142
  });
143
+ if (result?.staleId !== void 0) throw new StagehandFacadeInputError(staleSnapshotIdError(result.staleId));
195
144
  return {
196
- completed: (await this.stagehand.experimentalBatch(actionRunner, { actions: hydrated }, {
197
- page,
198
- timeout: RUN_BATCH_TIMEOUT_MS
199
- }))?.completed ?? hydrated.length,
145
+ completed: result?.completed ?? parsed.length,
200
146
  url: await page.url()
201
147
  };
202
148
  }
203
149
  async runNow(code) {
204
150
  const page = await this.activePage();
205
- const callback = new AsyncFunction("batchStagehand", "input", FACADE_PRELUDE + code + FACADE_EPILOGUE);
151
+ const callback = runCallback(code);
206
152
  const startedAt = performance.now();
207
153
  const keeperPageId = await this.keeper;
208
- const input = keeperPageId ? { hiddenPageIds: [keeperPageId] } : {};
154
+ const quietWait = this.options.quietWait ?? RUN_OPTIONS.quietWait.default;
155
+ const input = {
156
+ ...keeperPageId && { hiddenPageIds: [keeperPageId] },
157
+ ...quietWait && { quietWait: true }
158
+ };
209
159
  const envelope = await this.runBatchWithActivePageFallback(callback, input, page);
160
+ if (!isRunEnvelope(envelope)) throw new StagehandFacadeInputError(RUN_RESULT_MALFORMED_ERROR);
210
161
  let runError;
211
162
  let runFailed = false;
212
163
  try {
213
164
  this.options.onRunReport?.({
214
165
  telemetry: envelope.telemetry,
166
+ quietWait,
215
167
  batchRoundTripMs: performance.now() - startedAt,
216
168
  batchRuntimeMs: envelope.batchRuntimeMs,
217
169
  closeRequested: envelope.closeRequested
218
170
  });
219
171
  await this.writeScreenshotArtifacts(envelope.artifacts);
220
172
  if (envelope.executionError) throw new StagehandFacadeExecutionError(envelope.executionError);
173
+ if (Buffer.byteLength(JSON.stringify(envelope.value) ?? "") > RUN_RESULT_LIMITS.maxValueBytes) throw new StagehandFacadeInputError(RUN_VALUE_SIZE_ERROR);
221
174
  } catch (error) {
222
175
  runError = error;
223
176
  runFailed = true;
@@ -248,13 +201,25 @@ var StagehandFacadeTools = class StagehandFacadeTools {
248
201
  }
249
202
  async writeScreenshotArtifacts(artifacts) {
250
203
  if (artifacts.length === 0) return;
204
+ if (artifacts.length > RUN_RESULT_LIMITS.maxArtifacts) throw new StagehandFacadeInputError(SCREENSHOT_ARTIFACT_COUNT_ERROR);
205
+ let totalBytes = 0;
206
+ for (const artifact of artifacts) {
207
+ const size = decodedBase64Length(artifact.base64);
208
+ if (size > RUN_RESULT_LIMITS.maxArtifactBytes) throw new StagehandFacadeInputError(SCREENSHOT_ARTIFACT_SIZE_ERROR);
209
+ totalBytes += size;
210
+ if (totalBytes > RUN_RESULT_LIMITS.maxTotalArtifactBytes) throw new StagehandFacadeInputError(SCREENSHOT_ARTIFACT_TOTAL_SIZE_ERROR);
211
+ }
212
+ const decoded = artifacts.map((artifact) => ({
213
+ path: artifact.path,
214
+ bytes: Buffer.from(artifact.base64, "base64")
215
+ }));
251
216
  const configuredRoot = path.resolve(this.options.artifactRoot ?? process.cwd());
252
217
  await fsp.mkdir(configuredRoot, { recursive: true });
253
218
  const root = await fsp.realpath(configuredRoot);
254
- for (const artifact of artifacts) {
219
+ for (const artifact of decoded) {
255
220
  const target = path.resolve(configuredRoot, artifact.path);
256
221
  const relative = path.relative(configuredRoot, target);
257
- if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new StagehandFacadeInputError("Screenshot artifact path must stay within artifactRoot.");
222
+ if (!relative || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new StagehandFacadeInputError(SCREENSHOT_ARTIFACT_PATH_ERROR);
258
223
  let directory = root;
259
224
  const components = relative.split(path.sep);
260
225
  for (const component of components.slice(0, -1)) {
@@ -262,11 +227,14 @@ var StagehandFacadeTools = class StagehandFacadeTools {
262
227
  await fsp.mkdir(directory).catch((error) => {
263
228
  if (error.code !== "EEXIST") throw error;
264
229
  });
265
- if (!(await fsp.lstat(directory)).isDirectory()) throw new StagehandFacadeInputError("Screenshot artifact directory must not be a symlink.");
230
+ if (!(await fsp.lstat(directory)).isDirectory()) throw new StagehandFacadeInputError(SCREENSHOT_ARTIFACT_SYMLINK_ERROR);
266
231
  }
267
- const file = await fsp.open(path.join(directory, components.at(-1)), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 384);
232
+ const file = await fsp.open(path.join(directory, components.at(-1)), constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW, 384).catch((error) => {
233
+ if (error.code === "ELOOP") throw new StagehandFacadeInputError(SCREENSHOT_ARTIFACT_FILE_SYMLINK_ERROR);
234
+ throw error;
235
+ });
268
236
  try {
269
- await file.writeFile(Buffer.from(artifact.base64, "base64"));
237
+ await file.writeFile(artifact.bytes);
270
238
  } finally {
271
239
  await file.close();
272
240
  }
@@ -275,7 +243,13 @@ var StagehandFacadeTools = class StagehandFacadeTools {
275
243
  async activePage() {
276
244
  const context = this.stagehand.browser.context;
277
245
  const keeperPageId = await this.keeper;
278
- const active = await context.activePage();
246
+ let active;
247
+ try {
248
+ active = await context.activePage();
249
+ } catch (error) {
250
+ if (!(error instanceof Error) || !error.message.includes("active target not registered")) throw error;
251
+ return (await context.pages()).filter((page) => page.pageId !== keeperPageId).at(-1) ?? await context.newPage();
252
+ }
279
253
  if (active && active.pageId !== keeperPageId) return active;
280
254
  const page = (await context.pages()).filter((page) => page.pageId !== keeperPageId)[0] ?? await context.newPage();
281
255
  await context.setActivePage(page);
@@ -303,7 +277,7 @@ var StagehandFacadeTools = class StagehandFacadeTools {
303
277
  enqueue(tool, operation) {
304
278
  const guarded = async () => {
305
279
  if (this.loss) throw new StagehandFacadeSessionLostError(this.loss);
306
- if (this.closed) throw new StagehandFacadeInputError("Stagehand facade browser is closed.");
280
+ if (this.closed) throw new StagehandFacadeInputError(FACADE_BROWSER_CLOSED_ERROR);
307
281
  try {
308
282
  await this.ensureKeeperPage();
309
283
  const value = await operation();
@@ -343,6 +317,21 @@ var StagehandFacadeTools = class StagehandFacadeTools {
343
317
  } catch {}
344
318
  }
345
319
  };
320
+ function isRecord(value) {
321
+ return typeof value === "object" && value !== null && !Array.isArray(value);
322
+ }
323
+ function isRunEnvelope(value) {
324
+ if (!isRecord(value) || value.__stagehandPlaywrightCompat !== true) return false;
325
+ const { executionError, artifacts } = value;
326
+ return typeof value.closeRequested === "boolean" && typeof value.batchRuntimeMs === "number" && isRecord(value.telemetry) && Array.isArray(artifacts) && artifacts.every((artifact) => isRecord(artifact) && typeof artifact.path === "string" && isCanonicalBase64(artifact.base64)) && (executionError === void 0 || executionError === null || isRecord(executionError) && typeof executionError.name === "string" && typeof executionError.message === "string" && (executionError.stack === void 0 || typeof executionError.stack === "string"));
327
+ }
328
+ function isCanonicalBase64(value) {
329
+ return typeof value === "string" && value.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(value);
330
+ }
331
+ function decodedBase64Length(value) {
332
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
333
+ return value.length / 4 * 3 - padding;
334
+ }
346
335
  var FacadeDeadlineError = class extends Error {
347
336
  name = "FacadeDeadlineError";
348
337
  constructor(operation, timeoutMs) {
@@ -387,15 +376,4 @@ function cdpSessionLossCause(error) {
387
376
  }
388
377
  return sanitizeErrorMessage(messages.join("; caused by "));
389
378
  }
390
- function trimTrailingTextNode(path) {
391
- return path?.replace(/\/text\(\)(\[\d+\])?$/iu, "");
392
- }
393
- function resolveSnapshotXPath(xpathById, id) {
394
- const exact = xpathById[id];
395
- if (exact !== void 0) return exact;
396
- if (id.includes("-")) return void 0;
397
- const suffix = `-${id}`;
398
- const matches = Object.keys(xpathById).filter((key) => key.endsWith(suffix));
399
- return matches.length === 1 ? xpathById[matches[0]] : void 0;
400
- }
401
379
  export { FACADE_KEEPER_PAGE_URL, StagehandFacadeCleanupError, StagehandFacadeExecutionError, StagehandFacadeInputError, StagehandFacadeSessionLostError, StagehandFacadeTools };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbasehq/eve",
3
- "version": "0.2.2",
3
+ "version": "0.2.3-alpha-99e855dc16394f85652b080eac74f1053cea05b4",
4
4
  "description": "Browserbase for Eve, powered by Stagehand V4 Code Mode.",
5
5
  "keywords": [
6
6
  "ai-agents",
@@ -40,8 +40,8 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@browserbasehq/sdk": "^2.16.0",
43
- "zod": "4.4.3",
44
- "@browserbasehq/stagehand": "4.3.0"
43
+ "zod": "4.6.6",
44
+ "@browserbasehq/stagehand": "4.3.1-alpha-99e855dc16394f85652b080eac74f1053cea05b4"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^24",