diff --git a/packages/core/lib/v3/types/public/agent.ts b/packages/core/lib/v3/types/public/agent.ts index 9ce9bcb21..e6c0b1b77 100644 --- a/packages/core/lib/v3/types/public/agent.ts +++ b/packages/core/lib/v3/types/public/agent.ts @@ -1,10 +1,7 @@ import type { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { ToolSet } from "ai"; import { LogLine } from "./logs"; -import { Page as PlaywrightPage } from "playwright-core"; -import { Page as PuppeteerPage } from "puppeteer-core"; -import { Page as PatchrightPage } from "patchright-core"; -import { Page } from "../../understudy/page"; +import { AnyPage } from "./page"; export interface AgentAction { type: string; @@ -37,7 +34,7 @@ export interface AgentResult { export interface AgentExecuteOptions { instruction: string; maxSteps?: number; - page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page; + page?: AnyPage; highlightCursor?: boolean; } export type AgentType = "openai" | "anthropic" | "google"; diff --git a/packages/core/lib/v3/types/public/methods.ts b/packages/core/lib/v3/types/public/methods.ts index d0e17fe81..45b3faa55 100644 --- a/packages/core/lib/v3/types/public/methods.ts +++ b/packages/core/lib/v3/types/public/methods.ts @@ -1,16 +1,13 @@ -import { Page as PatchrightPage } from "patchright-core"; -import { Page as PlaywrightPage } from "playwright-core"; -import { Page as PuppeteerPage } from "puppeteer-core"; import { z } from "zod"; import type { InferStagehandSchema, StagehandZodSchema } from "../../zodCompat"; -import { Page } from "../../understudy/page"; import { ModelConfiguration } from "../public/model"; +import { AnyPage } from "./page"; export interface ActOptions { model?: ModelConfiguration; variables?: Record; timeout?: number; - page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page; + page?: AnyPage; } export interface ActResult { @@ -41,7 +38,7 @@ export interface ExtractOptions { model?: ModelConfiguration; timeout?: number; selector?: string; - page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page; + page?: AnyPage; } export const defaultExtractSchema = z.object({ @@ -56,7 +53,7 @@ export interface ObserveOptions { model?: ModelConfiguration; timeout?: number; selector?: string; - page?: PlaywrightPage | PuppeteerPage | PatchrightPage | Page; + page?: AnyPage; } export enum V3FunctionName { diff --git a/packages/core/lib/v3/types/public/page.ts b/packages/core/lib/v3/types/public/page.ts index f141f3d67..11ec3de40 100644 --- a/packages/core/lib/v3/types/public/page.ts +++ b/packages/core/lib/v3/types/public/page.ts @@ -1,9 +1,10 @@ -import { Page } from "../../understudy/page"; -import { Page as PlaywrightPage } from "playwright-core"; import { Page as PatchrightPage } from "patchright-core"; +import { Page as PlaywrightPage } from "playwright-core"; import { Page as PuppeteerPage } from "puppeteer-core"; +import { Page } from "../../understudy/page"; export type { PlaywrightPage, PatchrightPage, PuppeteerPage, Page }; + export type AnyPage = PlaywrightPage | PuppeteerPage | PatchrightPage | Page; export { ConsoleMessage } from "../../understudy/consoleMessage"; diff --git a/packages/core/tests/page-boundary.test.ts b/packages/core/tests/page-boundary.test.ts new file mode 100644 index 000000000..5bb5363e3 --- /dev/null +++ b/packages/core/tests/page-boundary.test.ts @@ -0,0 +1,326 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { z } from "zod/v3"; +import { + Stagehand, + StagehandInvalidArgumentError, + type ActResult, + type AgentResult, + type AnyPage, + type Page, + type V3Options, +} from "../dist/index.js"; + +const baseOptions: Pick = { + env: "LOCAL", + disableAPI: true, + disablePino: true, +}; + +const defaultActResult: ActResult = { + success: true, + message: "ok", + actionDescription: "", + actions: [], +}; +const defaultAgentResult: AgentResult = { + success: true, + message: "", + actions: [], + completed: true, +}; + +function createStagehand(overrides: Partial = {}): Stagehand { + return new Stagehand({ ...baseOptions, ...overrides }); +} + +function createInternalPage(frameId: string): Page { + return { + mainFrameId: () => frameId, + } as unknown as Page; +} + +function stubActCache(instance: Stagehand): void { + Reflect.set(instance, "actCache", { + enabled: false, + prepareContext: vi.fn(), + tryReplay: vi.fn(), + store: vi.fn(), + }); +} + +function stubActHandler(instance: Stagehand): void { + Reflect.set(instance, "actHandler", { + act: vi.fn(), + actFromObserveResult: vi.fn(), + }); +} + +function stubExtractHandler(instance: Stagehand): void { + Reflect.set(instance, "extractHandler", { + extract: vi.fn(), + }); +} + +function stubObserveHandler(instance: Stagehand): void { + Reflect.set(instance, "observeHandler", { + observe: vi.fn(), + }); +} + +function stubAgentCache(instance: Stagehand): void { + Reflect.set(instance, "agentCache", { + isRecording: vi.fn().mockReturnValue(false), + isReplayActive: vi.fn().mockReturnValue(false), + beginRecording: vi.fn(), + endRecording: vi.fn().mockReturnValue([]), + discardRecording: vi.fn(), + buildConfigSignature: vi.fn().mockReturnValue("signature"), + sanitizeExecuteOptions: vi.fn().mockImplementation((opts) => opts), + shouldAttemptCache: vi.fn().mockReturnValue(false), + prepareContext: vi.fn(), + tryReplay: vi.fn(), + store: vi.fn(), + }); +} + +function createApiClient() { + return { + act: vi.fn().mockResolvedValue(defaultActResult), + extract: vi.fn().mockResolvedValue({ pageText: "" }), + observe: vi.fn().mockResolvedValue([]), + agentExecute: vi.fn().mockResolvedValue(defaultAgentResult), + end: vi.fn(), + }; +} + +describe("Page boundary contracts", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("resolvePage", () => { + it("uses ctx.awaitActivePage when options.page is undefined", async () => { + const stagehand = createStagehand(); + const fakePage = createInternalPage("frame-default"); + const awaitActivePage = vi.fn().mockResolvedValue(fakePage); + Reflect.set(stagehand, "ctx", { awaitActivePage }); + + const resolvePage = Reflect.get(stagehand, "resolvePage") as ( + page?: AnyPage, + ) => Promise; + + const resolved = await resolvePage.call(stagehand); + + expect(awaitActivePage).toHaveBeenCalledTimes(1); + expect(resolved).toBe(fakePage); + }); + + it("rejects inputs outside the AnyPage union", async () => { + const stagehand = createStagehand(); + + const resolvePage = Reflect.get(stagehand, "resolvePage") as ( + page?: AnyPage, + ) => Promise; + + await expect( + resolvePage.call(stagehand, {} as AnyPage), + ).rejects.toBeInstanceOf(StagehandInvalidArgumentError); + }); + }); + + describe("normalizeToV3Page", () => { + it("bridges Playwright pages via frameId lookup", async () => { + const stagehand = createStagehand(); + const internalPage = createInternalPage("internal-frame"); + const resolvePageByMainFrameId = vi.fn().mockReturnValue(internalPage); + const frameId = "frame-playwright"; + + Reflect.set(stagehand, "ctx", { resolvePageByMainFrameId }); + Reflect.set( + stagehand, + "resolveTopFrameId", + vi.fn().mockResolvedValue(frameId), + ); + + const normalize = Reflect.get(stagehand, "normalizeToV3Page") as ( + page: AnyPage, + ) => Promise; + + const playwrightPage = { + context: () => ({}), + } as unknown as AnyPage; + + const result = await normalize.call(stagehand, playwrightPage); + + expect(resolvePageByMainFrameId).toHaveBeenCalledWith(frameId); + expect(result).toBe(internalPage); + }); + + it("bridges Patchright pages when the guard matches", async () => { + const stagehand = createStagehand(); + const internalPage = createInternalPage("internal-frame"); + const resolvePageByMainFrameId = vi.fn().mockReturnValue(internalPage); + const frameId = "frame-patchright"; + + Reflect.set(stagehand, "ctx", { resolvePageByMainFrameId }); + Reflect.set( + stagehand, + "resolveTopFrameId", + vi.fn().mockResolvedValue(frameId), + ); + Reflect.set( + stagehand, + "isPlaywrightPage", + vi.fn().mockReturnValue(false), + ); + Reflect.set(stagehand, "isPatchrightPage", vi.fn().mockReturnValue(true)); + + const normalize = Reflect.get(stagehand, "normalizeToV3Page") as ( + page: AnyPage, + ) => Promise; + + const patchrightPage = {} as AnyPage; + + const result = await normalize.call(stagehand, patchrightPage); + + expect(resolvePageByMainFrameId).toHaveBeenCalledWith(frameId); + expect(result).toBe(internalPage); + }); + + it("bridges Puppeteer pages via frameId lookup", async () => { + const stagehand = createStagehand(); + const internalPage = createInternalPage("internal-frame"); + const resolvePageByMainFrameId = vi.fn().mockReturnValue(internalPage); + const frameId = "frame-puppeteer"; + + Reflect.set(stagehand, "ctx", { resolvePageByMainFrameId }); + Reflect.set( + stagehand, + "resolveTopFrameId", + vi.fn().mockResolvedValue(frameId), + ); + + const normalize = Reflect.get(stagehand, "normalizeToV3Page") as ( + page: AnyPage, + ) => Promise; + + const puppeteerPage = { + target: () => ({}), + } as unknown as AnyPage; + + const result = await normalize.call(stagehand, puppeteerPage); + + expect(resolvePageByMainFrameId).toHaveBeenCalledWith(frameId); + expect(result).toBe(internalPage); + }); + }); + + describe("API payload serialization", () => { + it("act forwards only the frameId to StagehandAPI", async () => { + const stagehand = createStagehand(); + stubActHandler(stagehand); + stubActCache(stagehand); + stubAgentCache(stagehand); + + const frameId = "frame-act"; + const fakePage = createInternalPage(frameId); + Reflect.set( + stagehand, + "resolvePage", + vi.fn().mockResolvedValue(fakePage), + ); + + const apiClient = createApiClient(); + Reflect.set(stagehand, "apiClient", apiClient); + + const options = {}; + + await stagehand.act("Click button", options); + + expect(apiClient.act).toHaveBeenCalledWith({ + input: "Click button", + options, + frameId: "frame-act", + }); + }); + + it("extract forwards frameId and JSON-safe payload", async () => { + const stagehand = createStagehand(); + stubExtractHandler(stagehand); + stubAgentCache(stagehand); + + const frameId = "frame-extract"; + const fakePage = createInternalPage(frameId); + Reflect.set( + stagehand, + "resolvePage", + vi.fn().mockResolvedValue(fakePage), + ); + + const apiClient = createApiClient(); + Reflect.set(stagehand, "apiClient", apiClient); + + const schema = z.object({ value: z.string() }); + const options = {}; + + await stagehand.extract("Summarize", schema, options); + + expect(apiClient.extract).toHaveBeenCalledWith({ + instruction: "Summarize", + schema, + options, + frameId: "frame-extract", + }); + }); + + it("observe forwards frameId without leaking the page object", async () => { + const stagehand = createStagehand(); + stubObserveHandler(stagehand); + stubAgentCache(stagehand); + + const frameId = "frame-observe"; + const fakePage = createInternalPage(frameId); + Reflect.set( + stagehand, + "resolvePage", + vi.fn().mockResolvedValue(fakePage), + ); + + const apiClient = createApiClient(); + Reflect.set(stagehand, "apiClient", apiClient); + + const options = {}; + + await stagehand.observe("Check", options); + + expect(apiClient.observe).toHaveBeenCalledWith({ + instruction: "Check", + options, + frameId: "frame-observe", + }); + }); + + it("agent.execute obtains frameId from ctx.awaitActivePage", async () => { + const stagehand = createStagehand(); + stubAgentCache(stagehand); + + const fakePage = createInternalPage("frame-agent"); + const awaitActivePage = vi.fn().mockResolvedValue(fakePage); + const setActivePage = vi.fn(); + Reflect.set(stagehand, "ctx", { awaitActivePage, setActivePage }); + + const apiClient = createApiClient(); + Reflect.set(stagehand, "apiClient", apiClient); + + const agent = stagehand.agent(); + const result = await agent.execute("Do something"); + + expect(apiClient.agentExecute).toHaveBeenCalledWith( + undefined, + { instruction: "Do something" }, + "frame-agent", + ); + expect(result).toEqual(defaultAgentResult); + }); + }); +}); diff --git a/packages/core/tests/public-api/export-surface.test.ts b/packages/core/tests/public-api/export-surface.test.ts index 5602dc865..5a9637e08 100644 --- a/packages/core/tests/public-api/export-surface.test.ts +++ b/packages/core/tests/public-api/export-surface.test.ts @@ -1,69 +1,8 @@ -import { describe, expect, it } from "vitest"; import StagehandDefaultExport, * as Stagehand from "../../dist/index.js"; -import { publicErrorTypes } from "./public-error-types.test"; +import { runExportSurfaceSuite } from "../../../../tests/shared/exportSurfaceSuite"; -// Type matcher guidelines: -// -// toEqualTypeOf – Default. Assert full, deep type equality; any type change should fail. -// e.g. expectTypeOf>().toEqualTypeOf() -// -// toMatchObjectType – Assert (part of) an object's shape while allowing extra fields. -// e.g. expectTypeOf(user).toMatchObjectType<{ id: string; email: string }>() -// -// toExtend – Assert that a type is compatible with a broader contract (assignable/extends). -// e.g. expectTypeOf().toExtend() - -const publicApiShape = { - AISdkClient: Stagehand.AISdkClient, - AVAILABLE_CUA_MODELS: Stagehand.AVAILABLE_CUA_MODELS, - AgentProvider: Stagehand.AgentProvider, - AnnotatedScreenshotText: Stagehand.AnnotatedScreenshotText, - ConsoleMessage: Stagehand.ConsoleMessage, - LLMClient: Stagehand.LLMClient, - LOG_LEVEL_NAMES: Stagehand.LOG_LEVEL_NAMES, - Response: Stagehand.Response, - Stagehand: Stagehand.Stagehand, - V3: Stagehand.V3, - V3Evaluator: Stagehand.V3Evaluator, - V3FunctionName: Stagehand.V3FunctionName, - connectToMCPServer: Stagehand.connectToMCPServer, - default: StagehandDefaultExport, - defaultExtractSchema: Stagehand.defaultExtractSchema, - getZodType: Stagehand.getZodType, - injectUrls: Stagehand.injectUrls, - isRunningInBun: Stagehand.isRunningInBun, - isZod3Schema: Stagehand.isZod3Schema, - isZod4Schema: Stagehand.isZod4Schema, - jsonSchemaToZod: Stagehand.jsonSchemaToZod, - loadApiKeyFromEnv: Stagehand.loadApiKeyFromEnv, - modelToAgentProviderMap: Stagehand.modelToAgentProviderMap, - pageTextSchema: Stagehand.pageTextSchema, - providerEnvVarMap: Stagehand.providerEnvVarMap, - toGeminiSchema: Stagehand.toGeminiSchema, - toJsonSchema: Stagehand.toJsonSchema, - transformSchema: Stagehand.transformSchema, - trimTrailingTextNode: Stagehand.trimTrailingTextNode, - validateZodSchema: Stagehand.validateZodSchema, - ...publicErrorTypes, -} as const; - -type StagehandExports = typeof Stagehand & { - default: typeof StagehandDefaultExport; -}; - -type PublicAPI = { - [K in keyof typeof publicApiShape]: StagehandExports[K]; -}; - -describe("Stagehand public API export surface", () => { - it("public API shape matches module exports", () => { - const _check: PublicAPI = publicApiShape; - void _check; - }); - - it("does not expose unexpected top-level exports", () => { - const expected = Object.keys(publicApiShape).sort(); - const actual = Object.keys(Stagehand).sort(); - expect(actual).toStrictEqual(expected); - }); -}); +runExportSurfaceSuite( + "Stagehand public API export surface", + Stagehand, + StagehandDefaultExport, +); diff --git a/packages/core/tests/public-api/llm-and-agents.test.ts b/packages/core/tests/public-api/llm-and-agents.test.ts index b2094c1ef..755c79a59 100644 --- a/packages/core/tests/public-api/llm-and-agents.test.ts +++ b/packages/core/tests/public-api/llm-and-agents.test.ts @@ -200,7 +200,7 @@ describe("LLM and Agents public API types", () => { type ExpectedSignature = ( options: Stagehand.CreateChatCompletionOptions, ) => Promise; - + expectTypeOf< LLMClientInstance["createChatCompletion"] >().toExtend(); diff --git a/packages/core/tests/public-api/public-error-types.test.ts b/packages/core/tests/public-api/public-error-types.test.ts index 6534dce39..1322c9093 100644 --- a/packages/core/tests/public-api/public-error-types.test.ts +++ b/packages/core/tests/public-api/public-error-types.test.ts @@ -1,65 +1,10 @@ import { describe, expectTypeOf, it } from "vitest"; import * as Stagehand from "../../dist"; - -export const publicErrorTypes = { - AgentScreenshotProviderError: Stagehand.AgentScreenshotProviderError, - BrowserbaseSessionNotFoundError: Stagehand.BrowserbaseSessionNotFoundError, - CaptchaTimeoutError: Stagehand.CaptchaTimeoutError, - ConnectionTimeoutError: Stagehand.ConnectionTimeoutError, - ContentFrameNotFoundError: Stagehand.ContentFrameNotFoundError, - CreateChatCompletionResponseError: - Stagehand.CreateChatCompletionResponseError, - CuaModelRequiredError: Stagehand.CuaModelRequiredError, - ElementNotVisibleError: Stagehand.ElementNotVisibleError, - ExperimentalApiConflictError: Stagehand.ExperimentalApiConflictError, - ExperimentalNotConfiguredError: Stagehand.ExperimentalNotConfiguredError, - HandlerNotInitializedError: Stagehand.HandlerNotInitializedError, - InvalidAISDKModelFormatError: Stagehand.InvalidAISDKModelFormatError, - LLMResponseError: Stagehand.LLMResponseError, - MCPConnectionError: Stagehand.MCPConnectionError, - MissingEnvironmentVariableError: Stagehand.MissingEnvironmentVariableError, - MissingLLMConfigurationError: Stagehand.MissingLLMConfigurationError, - PageNotFoundError: Stagehand.PageNotFoundError, - ResponseBodyError: Stagehand.ResponseBodyError, - ResponseParseError: Stagehand.ResponseParseError, - StagehandAPIError: Stagehand.StagehandAPIError, - StagehandAPIUnauthorizedError: Stagehand.StagehandAPIUnauthorizedError, - StagehandClickError: Stagehand.StagehandClickError, - StagehandDefaultError: Stagehand.StagehandDefaultError, - StagehandDomProcessError: Stagehand.StagehandDomProcessError, - StagehandElementNotFoundError: Stagehand.StagehandElementNotFoundError, - StagehandEnvironmentError: Stagehand.StagehandEnvironmentError, - StagehandError: Stagehand.StagehandError, - StagehandEvalError: Stagehand.StagehandEvalError, - StagehandHttpError: Stagehand.StagehandHttpError, - StagehandIframeError: Stagehand.StagehandIframeError, - StagehandInitError: Stagehand.StagehandInitError, - StagehandInvalidArgumentError: Stagehand.StagehandInvalidArgumentError, - StagehandMissingArgumentError: Stagehand.StagehandMissingArgumentError, - StagehandNotInitializedError: Stagehand.StagehandNotInitializedError, - StagehandResponseBodyError: Stagehand.StagehandResponseBodyError, - StagehandResponseParseError: Stagehand.StagehandResponseParseError, - StagehandServerError: Stagehand.StagehandServerError, - StagehandShadowRootMissingError: Stagehand.StagehandShadowRootMissingError, - StagehandShadowSegmentEmptyError: Stagehand.StagehandShadowSegmentEmptyError, - StagehandShadowSegmentNotFoundError: - Stagehand.StagehandShadowSegmentNotFoundError, - TimeoutError: Stagehand.TimeoutError, - UnsupportedAISDKModelProviderError: - Stagehand.UnsupportedAISDKModelProviderError, - UnsupportedModelError: Stagehand.UnsupportedModelError, - UnsupportedModelProviderError: Stagehand.UnsupportedModelProviderError, - XPathResolutionError: Stagehand.XPathResolutionError, - ZodSchemaValidationError: Stagehand.ZodSchemaValidationError, -} as const; - -const errorTypes = Object.keys(publicErrorTypes) as Array< - keyof typeof publicErrorTypes ->; +import { PUBLIC_ERROR_TYPE_KEYS } from "../../../../tests/shared/publicErrorTypeKeys"; describe("Stagehand public error types", () => { describe("errors", () => { - it.each(errorTypes)("%s extends Error", (errorTypeName) => { + it.each(PUBLIC_ERROR_TYPE_KEYS)("%s extends Error", (errorTypeName) => { const ErrorClass = Stagehand[errorTypeName]; type ErrorClassType = typeof ErrorClass; expectTypeOf>().toExtend(); diff --git a/packages/core/tests/public-api/public-types.test.ts b/packages/core/tests/public-api/public-types.test.ts index 80a91a8d2..d145c2311 100644 --- a/packages/core/tests/public-api/public-types.test.ts +++ b/packages/core/tests/public-api/public-types.test.ts @@ -133,7 +133,9 @@ describe("Stagehand public API types", () => { }; it("matches expected type shape", () => { - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + ExpectedExtractOptions + >(); }); }); @@ -146,7 +148,9 @@ describe("Stagehand public API types", () => { }; it("matches expected type shape", () => { - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + ExpectedObserveOptions + >(); }); }); @@ -181,7 +185,9 @@ describe("Stagehand public API types", () => { }; it("matches expected type shape", () => { - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + ExpectedAgentExecuteOptions + >(); }); }); @@ -246,7 +252,9 @@ describe("Stagehand public API types", () => { }; it("matches expected type shape", () => { - expectTypeOf().toEqualTypeOf(); + expectTypeOf().toEqualTypeOf< + ExpectedHistoryEntry + >(); }); }); }); diff --git a/packages/core/tests/public-api/sdk-export-surface.test.ts b/packages/core/tests/public-api/sdk-export-surface.test.ts new file mode 100644 index 000000000..1aa8f41be --- /dev/null +++ b/packages/core/tests/public-api/sdk-export-surface.test.ts @@ -0,0 +1,8 @@ +import StagehandSDKDefaultExport, * as StagehandSDK from "../../../ts-sdk/dist/index.js"; +import { runExportSurfaceSuite } from "../../../../tests/shared/exportSurfaceSuite"; + +runExportSurfaceSuite( + "Stagehand SDK public API export surface", + StagehandSDK, + StagehandSDKDefaultExport, +); diff --git a/packages/core/tests/public-api/sdk-parity.test.ts b/packages/core/tests/public-api/sdk-parity.test.ts new file mode 100644 index 000000000..16a9de083 --- /dev/null +++ b/packages/core/tests/public-api/sdk-parity.test.ts @@ -0,0 +1,34 @@ +import { describe, it } from "vitest"; +import * as Stagehand from "../../dist/index.js"; +import * as StagehandSDK from "../../../ts-sdk/dist/index.js"; + +type AssertNever = T; + +describe("Stagehand SDK API parity", () => { + it("exposes at least the same top-level exports as core", () => { + type CoreKeys = keyof typeof Stagehand; + type SdkKeys = keyof typeof StagehandSDK; + type MissingOnSdk = Exclude; + type IgnoredKeys = + | "Page" + | "PlaywrightPage" + | "PatchrightPage" + | "PuppeteerPage"; + type FilteredMissing = Exclude; + + type _MissingOnSdk = AssertNever; + void (null as unknown as _MissingOnSdk); + }); + + it("mirrors the Stagehand.V3 method surface", () => { + type CoreInstance = InstanceType; + type SdkInstance = InstanceType; + type MissingInstanceMethods = Exclude< + keyof CoreInstance, + keyof SdkInstance + >; + + type _MissingInstanceMethods = AssertNever; + void (null as unknown as _MissingInstanceMethods); + }); +}); diff --git a/packages/core/tests/public-api/v3-core.test.ts b/packages/core/tests/public-api/v3-core.test.ts index 9da299801..dcb9c3a96 100644 --- a/packages/core/tests/public-api/v3-core.test.ts +++ b/packages/core/tests/public-api/v3-core.test.ts @@ -10,8 +10,12 @@ describe("V3 Core public API types", () => { input: string | Stagehand.Action, options?: Stagehand.ActOptions, ) => Promise; - extract: (...args: unknown[]) => Promise; - observe: (...args: unknown[]) => Promise; + extract: ( + ...args: unknown[] + ) => Promise; + observe: ( + ...args: unknown[] + ) => Promise; agent: (config?: Stagehand.AgentConfig) => { execute: ( instructionOrOptions: string | Stagehand.AgentExecuteOptions, diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index ccdabcb0e..f981853c1 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -12,7 +12,8 @@ "baseUrl": "../../", "paths": { "*": ["node_modules/*", "packages/core/lib/types/*"], - "@/*": ["./"] + "@/*": ["./"], + "vitest": ["packages/core/node_modules/vitest"] }, "skipLibCheck": true, "declaration": true, diff --git a/packages/ts-sdk/package.json b/packages/ts-sdk/package.json new file mode 100644 index 000000000..4ea5a14f8 --- /dev/null +++ b/packages/ts-sdk/package.json @@ -0,0 +1,24 @@ +{ + "name": "@browserbasehq/stagehand-sdk", + "version": "0.0.0", + "description": "Stagehand thin-client SDK stubs", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "sideEffects": false, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "clean": "rm -rf dist" + }, + "files": [ + "dist" + ], + "keywords": [ + "stagehand", + "sdk", + "browserbase" + ], + "author": "Browserbase", + "license": "MIT" +} diff --git a/packages/ts-sdk/src/constants.ts b/packages/ts-sdk/src/constants.ts new file mode 100644 index 000000000..5b7b4e097 --- /dev/null +++ b/packages/ts-sdk/src/constants.ts @@ -0,0 +1,110 @@ +import type { + AgentModelConfig, + AgentProviderType, + JsonSchema, + JsonSchemaDocument, + LogLevel, + Logger, + ModelProvider, + StagehandZodObject, + StagehandZodSchema, +} from "./types"; +import { ZodSchemaValidationError } from "./errors"; + +class SchemaStub implements StagehandZodObject { + public readonly kind = "stagehand-sdk-schema"; + + constructor(public readonly shape: Record) {} +} + +const createSchemaStub = (shape: Record) => + new SchemaStub(shape); + +export const AnnotatedScreenshotText = + "This is a screenshot of the current page state with the elements annotated on it. Each element id is annotated with a number to the top left of it. Duplicate annotations at the same location are under each other vertically."; + +export const LOG_LEVEL_NAMES: Record = Object.freeze({ + 0: "error", + 1: "info", + 2: "debug", +}); + +export const AVAILABLE_CUA_MODELS: Record = + Object.freeze({}); + +export const modelToAgentProviderMap: Record = + Object.freeze({}); + +export const providerEnvVarMap: Partial< + Record> +> = Object.freeze({}); + +export const defaultExtractSchema = createSchemaStub({ + extraction: "string", +}); + +export const pageTextSchema = createSchemaStub({ + pageText: "string", +}); + +export function getZodType(schema: StagehandZodSchema): string { + return ( + (schema as unknown as { _def?: { typeName?: string } })?._def?.typeName ?? + "unknown" + ); +} + +export function transformSchema( + schema: StagehandZodSchema, + _currentPath: Array = [], +): [StagehandZodSchema, Array>] { + return [schema, []]; +} + +export function injectUrls( + _obj: unknown, + _path: Array, + _idToUrlMapping: Record, +): void {} + +export function trimTrailingTextNode(selector?: string): string | undefined { + if (!selector) return selector; + return selector.replace(/\/text\(\)\[\d+\]$/u, ""); +} + +export function validateZodSchema(schema: T): T { + if (!schema) { + throw new ZodSchemaValidationError("A schema instance is required"); + } + return schema; +} + +export function toGeminiSchema(schema: StagehandZodSchema): JsonSchemaDocument { + return { schema }; +} +export function toJsonSchema(schema: StagehandZodObject): JsonSchemaDocument { + return { schema }; +} + +export function jsonSchemaToZod(_schema: JsonSchema): StagehandZodSchema { + return createSchemaStub({}); +} + +export function loadApiKeyFromEnv( + _provider: string | undefined, + _logger: Logger, +): string | undefined { + return undefined; +} + +export function isRunningInBun(): boolean { + return typeof (globalThis as { Bun?: unknown }).Bun !== "undefined"; +} + +export function isZod3Schema(_schema: unknown): _schema is StagehandZodSchema { + return false; +} + +export function isZod4Schema(_schema: unknown): _schema is StagehandZodSchema { + return false; +} diff --git a/packages/ts-sdk/src/errors.ts b/packages/ts-sdk/src/errors.ts new file mode 100644 index 000000000..b9321fb0d --- /dev/null +++ b/packages/ts-sdk/src/errors.ts @@ -0,0 +1,52 @@ +export class StagehandError extends Error { + constructor(message?: string) { + super(message); + this.name = "StagehandError"; + } +} + +export class AgentScreenshotProviderError extends StagehandError {} +export class BrowserbaseSessionNotFoundError extends StagehandError {} +export class CaptchaTimeoutError extends StagehandError {} +export class ConnectionTimeoutError extends StagehandError {} +export class ContentFrameNotFoundError extends StagehandError {} +export class CreateChatCompletionResponseError extends StagehandError {} +export class CuaModelRequiredError extends StagehandError {} +export class ElementNotVisibleError extends StagehandError {} +export class ExperimentalApiConflictError extends StagehandError {} +export class ExperimentalNotConfiguredError extends StagehandError {} +export class HandlerNotInitializedError extends StagehandError {} +export class InvalidAISDKModelFormatError extends StagehandError {} +export class LLMResponseError extends StagehandError {} +export class MCPConnectionError extends StagehandError {} +export class MissingEnvironmentVariableError extends StagehandError {} +export class MissingLLMConfigurationError extends StagehandError {} +export class PageNotFoundError extends StagehandError {} +export class ResponseBodyError extends StagehandError {} +export class ResponseParseError extends StagehandError {} +export class StagehandAPIError extends StagehandError {} +export class StagehandAPIUnauthorizedError extends StagehandError {} +export class StagehandClickError extends StagehandError {} +export class StagehandDefaultError extends StagehandError {} +export class StagehandDomProcessError extends StagehandError {} +export class StagehandElementNotFoundError extends StagehandError {} +export class StagehandEnvironmentError extends StagehandError {} +export class StagehandEvalError extends StagehandError {} +export class StagehandHttpError extends StagehandError {} +export class StagehandIframeError extends StagehandError {} +export class StagehandInitError extends StagehandError {} +export class StagehandInvalidArgumentError extends StagehandError {} +export class StagehandMissingArgumentError extends StagehandError {} +export class StagehandNotInitializedError extends StagehandError {} +export class StagehandResponseBodyError extends StagehandError {} +export class StagehandResponseParseError extends StagehandError {} +export class StagehandServerError extends StagehandError {} +export class StagehandShadowRootMissingError extends StagehandError {} +export class StagehandShadowSegmentEmptyError extends StagehandError {} +export class StagehandShadowSegmentNotFoundError extends StagehandError {} +export class TimeoutError extends StagehandError {} +export class UnsupportedAISDKModelProviderError extends StagehandError {} +export class UnsupportedModelError extends StagehandError {} +export class UnsupportedModelProviderError extends StagehandError {} +export class XPathResolutionError extends StagehandError {} +export class ZodSchemaValidationError extends StagehandError {} diff --git a/packages/ts-sdk/src/index.ts b/packages/ts-sdk/src/index.ts new file mode 100644 index 000000000..09ead4b5a --- /dev/null +++ b/packages/ts-sdk/src/index.ts @@ -0,0 +1,5 @@ +export * from "./errors"; +export * from "./constants"; +export * from "./runtime"; +export * from "./types"; +export { default as Stagehand, V3 } from "./runtime"; diff --git a/packages/ts-sdk/src/runtime.ts b/packages/ts-sdk/src/runtime.ts new file mode 100644 index 000000000..a4459361e --- /dev/null +++ b/packages/ts-sdk/src/runtime.ts @@ -0,0 +1,429 @@ +import type { + ActOptions, + ActResult, + Action, + AgentAction, + AgentConfig, + AgentExecuteOptions, + AgentExecutionOptions, + AgentProviderType, + AgentReplayStep, + AgentResult, + AgentType, + AnyPage, + ClientOptions, + ConnectToMCPServerOptions, + ConsoleListener, + ExtractOptions, + HistoryEntry, + JsonSchema, + JsonSchemaDocument, + JsonSchemaProperty, + LoadState, + LogLevel, + LogLine, + Logger, + MCPClient, + ModelConfiguration, + ObserveOptions, + StagehandMetrics, + StdioServerConfig, + ToolUseItem, + V3Env, + V3FunctionName, + V3Options, + AISDKCustomProvider, + AISDKProvider, + ChatCompletionOptions, + CreateChatCompletionOptions, + LLMResponse, + LLMUsage, + AgentClient, +} from "./types"; +import { + AnnotatedScreenshotText, + AVAILABLE_CUA_MODELS, + LOG_LEVEL_NAMES, + defaultExtractSchema, + getZodType, + injectUrls, + isRunningInBun, + isZod3Schema, + isZod4Schema, + jsonSchemaToZod, + loadApiKeyFromEnv, + modelToAgentProviderMap, + pageTextSchema, + providerEnvVarMap, + toGeminiSchema, + toJsonSchema, + transformSchema, + trimTrailingTextNode, + validateZodSchema, +} from "./constants"; +import { + AgentScreenshotProviderError, + BrowserbaseSessionNotFoundError, + CaptchaTimeoutError, + ConnectionTimeoutError, + ContentFrameNotFoundError, + CreateChatCompletionResponseError, + CuaModelRequiredError, + ElementNotVisibleError, + ExperimentalApiConflictError, + ExperimentalNotConfiguredError, + HandlerNotInitializedError, + InvalidAISDKModelFormatError, + LLMResponseError, + MCPConnectionError, + MissingEnvironmentVariableError, + MissingLLMConfigurationError, + PageNotFoundError, + ResponseBodyError, + ResponseParseError, + StagehandAPIError, + StagehandAPIUnauthorizedError, + StagehandClickError, + StagehandDefaultError, + StagehandDomProcessError, + StagehandElementNotFoundError, + StagehandEnvironmentError, + StagehandError, + StagehandEvalError, + StagehandHttpError, + StagehandIframeError, + StagehandInitError, + StagehandInvalidArgumentError, + StagehandMissingArgumentError, + StagehandNotInitializedError, + StagehandResponseBodyError, + StagehandResponseParseError, + StagehandServerError, + StagehandShadowRootMissingError, + StagehandShadowSegmentEmptyError, + StagehandShadowSegmentNotFoundError, + TimeoutError, + UnsupportedAISDKModelProviderError, + UnsupportedModelError, + UnsupportedModelProviderError, + XPathResolutionError, + ZodSchemaValidationError, +} from "./errors"; +import { V3FunctionName as V3Fn } from "./types"; + +class StagehandSDKNotImplementedError extends Error { + constructor(method: string) { + super(`Stagehand SDK stub: ${method} is not implemented yet.`); + this.name = "StagehandSDKNotImplementedError"; + } +} + +const rejectNotImplemented = (method: string): Promise => + Promise.reject(new StagehandSDKNotImplementedError(method)); + +const notImplemented = (method: string): never => { + throw new StagehandSDKNotImplementedError(method); +}; + +export class ConsoleMessage { + constructor( + public readonly type: string, + public readonly message: string, + public readonly args: unknown[] = [], + public readonly location: Record = {}, + ) {} + + text(): string { + return this.message; + } + + argumentValues(): unknown[] { + return this.args; + } +} + +export class Response { + url(): string { + return notImplemented("Response.url"); + } + status(): number { + return notImplemented("Response.status"); + } + statusText(): string { + return notImplemented("Response.statusText"); + } + headers(): Record { + return notImplemented("Response.headers"); + } + headersArray(): Array<{ name: string; value: string }> { + return notImplemented("Response.headersArray"); + } + allHeaders(): Promise> { + return rejectNotImplemented("Response.allHeaders"); + } + headerValue(_name: string): string | null { + return notImplemented("Response.headerValue"); + } + headerValues(_name: string): string[] { + return notImplemented("Response.headerValues"); + } + finished(): boolean { + return false; + } + markFinished(): void {} + applyExtraInfo(_info: unknown): void {} + fromServiceWorker(): boolean { + return false; + } + frame(): unknown { + return null; + } + ok(): boolean { + return false; + } + securityDetails(): unknown { + return null; + } + serverAddr(): unknown { + return null; + } + body(): Promise { + return rejectNotImplemented("Response.body"); + } + text(): Promise { + return rejectNotImplemented("Response.text"); + } + json(): Promise { + return rejectNotImplemented("Response.json"); + } +} + +export abstract class LLMClient { + public type: string = "sdk"; + public modelName: string; + public hasVision = false; + public clientOptions: ClientOptions = {}; + public userProvidedInstructions?: string; + + constructor(modelName: string, userProvidedInstructions?: string) { + this.modelName = modelName; + this.userProvidedInstructions = userProvidedInstructions; + } + + generateText = (..._args: unknown[]): Promise => + rejectNotImplemented("LLMClient.generateText"); + generateObject = (..._args: unknown[]): Promise => + rejectNotImplemented("LLMClient.generateObject"); + streamText = (..._args: unknown[]): AsyncIterable => + (async function* () { + throw new StagehandSDKNotImplementedError("LLMClient.streamText"); + })(); + streamObject = (..._args: unknown[]): AsyncIterable => + (async function* () { + throw new StagehandSDKNotImplementedError("LLMClient.streamObject"); + })(); + generateImage = (..._args: unknown[]): Promise => + rejectNotImplemented("LLMClient.generateImage"); + embed = (..._args: unknown[]): Promise => + rejectNotImplemented("LLMClient.embed"); + embedMany = (..._args: unknown[]): Promise => + rejectNotImplemented("LLMClient.embedMany"); + transcribe = (..._args: unknown[]): Promise => + rejectNotImplemented("LLMClient.transcribe"); + generateSpeech = (..._args: unknown[]): Promise => + rejectNotImplemented("LLMClient.generateSpeech"); +} + +export class AISdkClient extends LLMClient { + constructor({ model }: { model: unknown }) { + super(String((model as { name?: string })?.name ?? "unknown")); + } + + createChatCompletion( + options: CreateChatCompletionOptions, + ): Promise { + void options; + return rejectNotImplemented("AISdkClient.createChatCompletion"); + } +} + +export class AgentProvider { + constructor(private readonly logger?: Logger) {} + + getClient( + _modelName: string, + _clientOptions?: Record, + _userProvidedInstructions?: string, + _tools?: Record, + ): AgentClient { + return notImplemented("AgentProvider.getClient"); + } +} + +export class V3Evaluator { + constructor(private readonly v3: V3) { + void this.v3; + } + + getClient(): unknown { + return notImplemented("V3Evaluator.getClient"); + } + + ask( + _instruction: string, + _options?: Record, + ): Promise { + return rejectNotImplemented("V3Evaluator.ask"); + } + + batchAsk( + _instructions: Array<{ + instruction: string; + options?: Record; + }>, + ): Promise { + return rejectNotImplemented("V3Evaluator.batchAsk"); + } + + protected _evaluateWithMultipleScreenshots(): Promise { + return rejectNotImplemented("V3Evaluator._evaluateWithMultipleScreenshots"); + } +} + +export async function connectToMCPServer( + _serverConfig: string | URL | StdioServerConfig | ConnectToMCPServerOptions, +): Promise { + return rejectNotImplemented("connectToMCPServer"); +} + +export class V3 { + public llmClient!: LLMClient; + public readonly experimental = false; + public readonly logInferenceToFile = false; + public readonly disableAPI = true; + public verbose: 0 | 1 | 2 = 1; + public browserbaseSessionId?: string; + public stagehandMetrics: StagehandMetrics = {}; + protected historyEntries: HistoryEntry[] = []; + protected currentMetrics: StagehandMetrics = {}; + + constructor(public readonly opts: V3Options = {}) {} + + get browserbaseSessionID(): string | undefined { + return undefined; + } + + get browserbaseSessionURL(): string | undefined { + return undefined; + } + + get browserbaseDebugURL(): string | undefined { + return undefined; + } + + get metrics(): Promise { + return rejectNotImplemented("V3.metrics"); + } + + async init(): Promise { + return notImplemented("V3.init"); + } + + async close(_opts?: { force?: boolean }): Promise { + return notImplemented("V3.close"); + } + + async act( + _input: string | Action, + _options?: ActOptions, + ): Promise { + return notImplemented("V3.act"); + } + + async extract( + _instructionOrSchema?: string | JsonSchema, + _schemaOrOptions?: JsonSchema | ExtractOptions, + _maybeOptions?: ExtractOptions, + ): Promise { + return notImplemented("V3.extract"); + } + + async observe( + _instructionOrOptions?: string | ObserveOptions, + _maybeOptions?: ObserveOptions, + ): Promise { + return notImplemented("V3.observe"); + } + + agent(_options?: AgentConfig) { + return { + execute: ( + instructionOrOptions: string | AgentExecuteOptions, + _page?: AnyPage, + ): Promise => { + void instructionOrOptions; + return rejectNotImplemented("V3.agent.execute"); + }, + }; + } + + isAgentReplayActive(): boolean { + return false; + } + + isAgentReplayRecording(): boolean { + return false; + } + + beginAgentReplayRecording(): void {} + + endAgentReplayRecording(): AgentReplayStep[] { + return notImplemented("V3.endAgentReplayRecording"); + } + + discardAgentReplayRecording(): void {} + + recordAgentReplayStep(_step: AgentReplayStep): void {} + + get history(): Promise> { + return Promise.resolve(this.historyEntries); + } + + addToHistory( + functionName: V3Fn | string, + parameters: Record, + ): void { + this.historyEntries.push({ + functionName, + parameters, + timestamp: new Date().toISOString(), + }); + } + + updateMetrics(partial: Partial): void { + this.currentMetrics = { ...this.currentMetrics, ...partial }; + this.stagehandMetrics = { ...this.stagehandMetrics, ...partial }; + } + + updateTotalMetrics(partial: Partial): void { + this.updateMetrics(partial); + } + + connectURL(): string { + return notImplemented("V3.connectURL"); + } + + get context(): unknown { + return notImplemented("V3.context"); + } + + get logger(): Logger { + return (_log) => { + throw new StagehandSDKNotImplementedError("V3.logger"); + }; + } +} + +export class Stagehand extends V3 {} + +export default Stagehand; diff --git a/packages/ts-sdk/src/types.ts b/packages/ts-sdk/src/types.ts new file mode 100644 index 000000000..cf1885cc1 --- /dev/null +++ b/packages/ts-sdk/src/types.ts @@ -0,0 +1,389 @@ +export type AvailableModel = string; +export type AvailableCuaModel = string; +export type ModelProvider = string; + +export interface ClientOptions { + [key: string]: unknown; +} + +export interface ModelConfiguration { + modelName?: AvailableModel; + clientOptions?: ClientOptions; +} + +export interface AISDKProvider { + name: string; + description?: string; + apiKeyEnv?: string; + baseUrl?: string; +} + +export interface AISDKCustomProvider extends AISDKProvider { + headers?: Record; +} + +export type LLMTool = { + type: string; + name: string; + description?: string; + parameters?: unknown; +}; + +export interface Action { + selector?: string; + method?: string; + arguments?: string[]; + description?: string; +} + +export interface ActResult { + success: boolean; + message: string; + actionDescription?: string; + actions?: Action[]; +} + +export interface HistoryEntry { + functionName: string; + parameters: Record; + timestamp: string; +} + +export interface ActOptions { + model?: ModelConfiguration | string; + variables?: Record; + timeout?: number; + page?: AnyPage; +} + +export interface ExtractOptions { + model?: ModelConfiguration | string; + timeout?: number; + selector?: string; + page?: AnyPage; +} + +export interface ObserveOptions { + model?: ModelConfiguration | string; + timeout?: number; + selector?: string; + page?: AnyPage; +} + +export enum V3FunctionName { + ACT = "ACT", + EXTRACT = "EXTRACT", + OBSERVE = "OBSERVE", + AGENT = "AGENT", +} + +export interface AgentAction extends Action {} + +export interface AgentResult { + success: boolean; + steps?: AgentAction[]; + output?: string; +} + +export interface AgentExecuteOptions { + instruction: string; + page?: AnyPage; + maxSteps?: number; + tools?: Record; +} + +export type AgentType = string; + +export interface AgentExecutionOptions { + instruction: string; + options?: T; +} + +export interface AgentHandlerOptions { + type?: AgentType; + model?: ModelConfiguration | string; + systemPrompt?: string; +} + +export interface ActionExecutionResult { + action: AgentAction; + success: boolean; + error?: string; +} + +export interface ToolUseItem { + [key: string]: unknown; +} + +export interface ZodPathSegments { + segments: Array; +} + +export interface AnthropicMessage { + id?: string; + role?: string; + content?: Array; +} + +export interface AnthropicContentBlock { + type: string; + text?: string; + id?: string; +} + +export interface AnthropicTextBlock { + type: "text"; + text: string; +} + +export interface AnthropicToolResult { + type: "tool_result"; + result?: unknown; +} + +export type ResponseItem = Record; +export type ComputerCallItem = Record; +export type FunctionCallItem = Record; +export type ResponseInputItem = Record; + +export interface AgentInstance { + execute(options: AgentExecutionOptions): Promise; +} + +export type AgentProviderType = string; + +export interface AgentModelConfig { + modelName: AvailableModel; + provider?: AgentProviderType; +} + +export interface AgentConfig extends AgentHandlerOptions { + model?: ModelConfiguration | string; + executionModel?: ModelConfiguration | string; + cua?: boolean; + tools?: Record; + integrations?: unknown[]; +} + +export interface AgentClient { + execute(options: AgentExecutionOptions): Promise; + captureScreenshot?(options?: Record): Promise; + setViewport?(width: number, height: number): void; + setCurrentUrl?(url: string): void; + setScreenshotProvider?(provider: () => Promise): void; + setActionHandler?(handler: (action: AgentAction) => Promise): void; +} + +export type LogLevel = 0 | 1 | 2; + +export interface LogLine { + id?: string; + category?: string; + message: string; + level?: LogLevel; + timestamp?: string; + auxiliary?: Record< + string, + { + value: string; + type: "object" | "string" | "html" | "integer" | "float" | "boolean"; + } + >; +} + +export type Logger = (logLine: LogLine) => void; + +export interface StagehandMetrics { + totalPromptTokens?: number; + totalCompletionTokens?: number; + totalReasoningTokens?: number; + totalCachedInputTokens?: number; + totalInferenceTimeMs?: number; +} + +export type V3Env = "LOCAL" | "BROWSERBASE"; + +export interface LocalBrowserLaunchOptions { + args?: string[]; + executablePath?: string; + userDataDir?: string; + headless?: boolean; + devtools?: boolean; + locale?: string; + viewport?: { + width: number; + height: number; + }; + deviceScaleFactor?: number; + hasTouch?: boolean; + ignoreHTTPSErrors?: boolean; + proxy?: { + server?: string; + bypass?: string; + }; + preserveUserDataDir?: boolean; + connectTimeoutMs?: number; + downloadsPath?: string; + acceptDownloads?: boolean; +} + +export interface V3Options { + env?: V3Env; + apiKey?: string; + projectId?: string; + cacheDir?: string; + logger?: Logger; + systemPrompt?: string; + verbose?: 0 | 1 | 2; + model?: ModelConfiguration | string; + llmClient?: unknown; + selfHeal?: boolean; + disableAPI?: boolean; + browserbaseSessionID?: string; + browserbaseSessionCreateParams?: Record; +} + +export type AnyPage = + | { + kind?: string; + reference?: unknown; + } + | unknown; + +// Core only exposes these as TypeScript aliases (no runtime objects), so the SDK +// mirrors that shape and relies on AnyPage underneath. +export type Page = AnyPage; +export type PlaywrightPage = AnyPage; +export type PatchrightPage = AnyPage; +export type PuppeteerPage = AnyPage; + +export type ConsoleListener = (message: unknown) => void; + +export type LoadState = "load" | "domcontentloaded" | "networkidle"; + +export interface ChatMessageImageContent { + type: string; + image_url?: { url: string }; + text?: string; + source?: { + type: string; + media_type: string; + data: string; + }; +} + +export interface ChatMessageTextContent { + type: string; + text: string; +} + +export type ChatMessageContent = + | string + | Array; + +export interface ChatMessage { + role: "system" | "user" | "assistant"; + content: ChatMessageContent; +} + +export interface ChatCompletionOptions { + messages: ChatMessage[]; + temperature?: number; + top_p?: number; + frequency_penalty?: number; + presence_penalty?: number; + image?: { + buffer: Buffer; + description?: string; + }; + response_model?: { + name: string; + schema: StagehandZodSchema; + }; + tools?: LLMTool[]; + tool_choice?: "auto" | "none" | "required"; + maxOutputTokens?: number; + requestId?: string; +} + +export type LLMResponse = { + id: string; + object: string; + created: number; + model: string; + choices: { + index: number; + message: { + role: string; + content: string | null; + tool_calls: { + id: string; + type: string; + function: { + name: string; + arguments: string; + }; + }[]; + }; + finish_reason: string; + }[]; + usage: LLMUsage; +}; + +export interface CreateChatCompletionOptions { + options: ChatCompletionOptions; + logger: Logger; + retries?: number; +} + +export interface LLMUsage { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + reasoning_tokens?: number; + cached_input_tokens?: number; +} + +export interface LLMParsedResponse { + data: T; + usage?: LLMUsage; +} + +export interface StagehandZodSchema { + kind?: string; + shape?: Record; +} + +export interface StagehandZodObject extends StagehandZodSchema {} + +export type InferStagehandSchema = unknown; + +export type JsonSchemaDocument = Record; +export type JsonSchema = Record; +export type JsonSchemaProperty = Record; + +export type ExtractResult = unknown; + +export interface ObserveResult { + actions: Action[]; +} + +export interface AgentReplayStep { + type: string; + payload?: unknown; +} + +export interface MCPClient { + [key: string]: unknown; +} + +export interface ConnectToMCPServerOptions { + serverUrl: string | URL; + clientOptions?: ClientOptions; +} + +export interface StdioServerConfig { + command: string; + args?: string[]; + env?: Record; +} diff --git a/packages/ts-sdk/tsconfig.json b/packages/ts-sdk/tsconfig.json new file mode 100644 index 000000000..0ceb0de7f --- /dev/null +++ b/packages/ts-sdk/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "emitDeclarationOnly": false, + "types": ["node"], + "composite": false, + "module": "commonjs" + }, + "include": ["src/**/*"], + "exclude": ["dist", "node_modules"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f1be5a15d..a9a7e66f2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,4 @@ packages: - "packages/core" - "packages/evals" - "packages/docs" + - "packages/ts-sdk" diff --git a/tests/shared/exportSurfaceSuite.ts b/tests/shared/exportSurfaceSuite.ts new file mode 100644 index 000000000..db732ab2a --- /dev/null +++ b/tests/shared/exportSurfaceSuite.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from "vitest"; +import { buildPublicApiShape } from "./publicApiManifest"; + +export function runExportSurfaceSuite, D>( + label: string, + moduleExports: M, + defaultExport: D, +): void { + describe(label, () => { + const publicApiShape = buildPublicApiShape(moduleExports, defaultExport); + + type PublicAPI = { + [K in keyof typeof publicApiShape]: K extends "default" ? D : M[K]; + }; + + it("public API shape matches module exports", () => { + const _check: PublicAPI = publicApiShape; + void _check; + }); + + it("does not expose unexpected top-level exports", () => { + const expected = Object.keys(publicApiShape).sort(); + const actual = Object.keys(moduleExports) + // Node injects __esModule / module.exports on ESM/CJS interop; ignore them. + .filter((key) => key !== "__esModule" && key !== "module.exports") + .sort(); + expect(actual).toStrictEqual(expected); + }); + }); +} diff --git a/tests/shared/publicApiManifest.ts b/tests/shared/publicApiManifest.ts new file mode 100644 index 000000000..79ca58ab8 --- /dev/null +++ b/tests/shared/publicApiManifest.ts @@ -0,0 +1,61 @@ +import type { PublicErrorTypeKey } from "./publicErrorTypeKeys"; +import { PUBLIC_ERROR_TYPE_KEYS } from "./publicErrorTypeKeys"; + +export const PUBLIC_API_EXPORT_KEYS = [ + "AISdkClient", + "AVAILABLE_CUA_MODELS", + "AgentProvider", + "AnnotatedScreenshotText", + "ConsoleMessage", + "LLMClient", + "LOG_LEVEL_NAMES", + "Response", + "Stagehand", + "V3", + "V3Evaluator", + "V3FunctionName", + "connectToMCPServer", + "defaultExtractSchema", + "getZodType", + "injectUrls", + "isRunningInBun", + "isZod3Schema", + "isZod4Schema", + "jsonSchemaToZod", + "loadApiKeyFromEnv", + "modelToAgentProviderMap", + "pageTextSchema", + "providerEnvVarMap", + "toGeminiSchema", + "toJsonSchema", + "transformSchema", + "trimTrailingTextNode", + "validateZodSchema", +] as const; + +export type PublicApiExportKey = (typeof PUBLIC_API_EXPORT_KEYS)[number]; + +export type PublicApiShape, D> = { + [K in + | PublicApiExportKey + | PublicErrorTypeKey + | "default"]: K extends "default" ? D : M[K]; +}; + +export function buildPublicApiShape, D>( + moduleExports: M, + defaultExport: D, +): PublicApiShape { + const shape: Record = {}; + + for (const key of PUBLIC_API_EXPORT_KEYS) { + shape[key] = moduleExports[key]; + } + for (const key of PUBLIC_ERROR_TYPE_KEYS) { + shape[key] = moduleExports[key]; + } + + shape.default = defaultExport; + + return shape as PublicApiShape; +} diff --git a/tests/shared/publicErrorTypeKeys.ts b/tests/shared/publicErrorTypeKeys.ts new file mode 100644 index 000000000..125a121c4 --- /dev/null +++ b/tests/shared/publicErrorTypeKeys.ts @@ -0,0 +1,50 @@ +export const PUBLIC_ERROR_TYPE_KEYS = [ + "AgentScreenshotProviderError", + "BrowserbaseSessionNotFoundError", + "CaptchaTimeoutError", + "ConnectionTimeoutError", + "ContentFrameNotFoundError", + "CreateChatCompletionResponseError", + "CuaModelRequiredError", + "ElementNotVisibleError", + "ExperimentalApiConflictError", + "ExperimentalNotConfiguredError", + "HandlerNotInitializedError", + "InvalidAISDKModelFormatError", + "LLMResponseError", + "MCPConnectionError", + "MissingEnvironmentVariableError", + "MissingLLMConfigurationError", + "PageNotFoundError", + "ResponseBodyError", + "ResponseParseError", + "StagehandAPIError", + "StagehandAPIUnauthorizedError", + "StagehandClickError", + "StagehandDefaultError", + "StagehandDomProcessError", + "StagehandElementNotFoundError", + "StagehandEnvironmentError", + "StagehandError", + "StagehandEvalError", + "StagehandHttpError", + "StagehandIframeError", + "StagehandInitError", + "StagehandInvalidArgumentError", + "StagehandMissingArgumentError", + "StagehandNotInitializedError", + "StagehandResponseBodyError", + "StagehandResponseParseError", + "StagehandServerError", + "StagehandShadowRootMissingError", + "StagehandShadowSegmentEmptyError", + "StagehandShadowSegmentNotFoundError", + "TimeoutError", + "UnsupportedAISDKModelProviderError", + "UnsupportedModelError", + "UnsupportedModelProviderError", + "XPathResolutionError", + "ZodSchemaValidationError", +] as const; + +export type PublicErrorTypeKey = (typeof PUBLIC_ERROR_TYPE_KEYS)[number];