@browserbasehq/stagehand 2.1.0 → 2.2.0-alpha-8814af9ece99fddc3dd9fb32671d0513a3a00c67

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,2 @@
1
+ import { EvalFunction } from "@/types/evals";
2
+ export declare const extract_jfk_links: EvalFunction;
@@ -0,0 +1,2 @@
1
+ import { EvalFunction } from "@/types/evals";
2
+ export declare const extract_single_link: EvalFunction;
package/dist/index.d.ts CHANGED
@@ -524,6 +524,23 @@ interface HistoryEntry {
524
524
  result: unknown;
525
525
  timestamp: string;
526
526
  }
527
+ /**
528
+ * Represents a path through a Zod schema from the root object down to a
529
+ * particular field. The `segments` array describes the chain of keys/indices.
530
+ *
531
+ * - **String** segments indicate object property names.
532
+ * - **Number** segments indicate array indices.
533
+ *
534
+ * For example, `["users", 0, "homepage"]` might describe reaching
535
+ * the `homepage` field in `schema.users[0].homepage`.
536
+ */
537
+ interface ZodPathSegments {
538
+ /**
539
+ * The ordered list of keys/indices leading from the schema root
540
+ * to the targeted field.
541
+ */
542
+ segments: Array<string | number>;
543
+ }
527
544
 
528
545
  declare const defaultExtractSchema: z.ZodObject<{
529
546
  extraction: z.ZodString;
@@ -849,4 +866,4 @@ declare class Stagehand {
849
866
  get history(): ReadonlyArray<HistoryEntry>;
850
867
  }
851
868
 
852
- export { type ActOptions, type ActResult, type ActionExecutionResult, type AgentAction, type AgentClientOptions, type AgentConfig, type AgentExecuteOptions, type AgentExecuteParams, type AgentExecutionOptions, type AgentHandlerOptions, type AgentOptions, type AgentProviderType, type AgentResult, AgentScreenshotProviderError, type AgentType, AnnotatedScreenshotText, type AnthropicContentBlock, type AnthropicJsonSchemaObject, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResult, type AvailableModel, AvailableModelSchema, type Browser, type BrowserContext, type BrowserResult, BrowserbaseSessionNotFoundError, CaptchaTimeoutError, type ChatCompletionOptions, type ChatMessage, type ChatMessageContent, type ChatMessageImageContent, type ChatMessageTextContent, type ClientOptions, type ComputerCallItem, type ConstructorParams, type CreateChatCompletionOptions, CreateChatCompletionResponseError, type ExtractOptions, type ExtractResult, type FunctionCallItem, type GotoOptions, HandlerNotInitializedError, type HistoryEntry, type InitResult, LLMClient, type LLMResponse, LOG_LEVEL_NAMES, type LocalBrowserLaunchOptions, type LogLevel, type LogLine, type Logger, MissingEnvironmentVariableError, MissingLLMConfigurationError, type ModelProvider, type ObserveOptions, type ObserveResult, type OperatorResponse, type OperatorSummary, type Page, PlaywrightCommandException, PlaywrightCommandMethodNotSupportedException, type ResponseInputItem, type ResponseItem, Stagehand, StagehandAPIError, StagehandAPIUnauthorizedError, StagehandDefaultError, StagehandDeprecationError, StagehandDomProcessError, StagehandElementNotFoundError, StagehandEnvironmentError, StagehandError, StagehandEvalError, StagehandFunctionName, StagehandHttpError, StagehandInvalidArgumentError, type StagehandMetrics, StagehandMissingArgumentError, StagehandNotImplementedError, StagehandNotInitializedError, StagehandResponseBodyError, StagehandResponseParseError, StagehandServerError, type ToolUseItem, UnsupportedModelError, UnsupportedModelProviderError, defaultExtractSchema, operatorResponseSchema, operatorSummarySchema, pageTextSchema };
869
+ export { type ActOptions, type ActResult, type ActionExecutionResult, type AgentAction, type AgentClientOptions, type AgentConfig, type AgentExecuteOptions, type AgentExecuteParams, type AgentExecutionOptions, type AgentHandlerOptions, type AgentOptions, type AgentProviderType, type AgentResult, AgentScreenshotProviderError, type AgentType, AnnotatedScreenshotText, type AnthropicContentBlock, type AnthropicJsonSchemaObject, type AnthropicMessage, type AnthropicTextBlock, type AnthropicToolResult, type AvailableModel, AvailableModelSchema, type Browser, type BrowserContext, type BrowserResult, BrowserbaseSessionNotFoundError, CaptchaTimeoutError, type ChatCompletionOptions, type ChatMessage, type ChatMessageContent, type ChatMessageImageContent, type ChatMessageTextContent, type ClientOptions, type ComputerCallItem, type ConstructorParams, type CreateChatCompletionOptions, CreateChatCompletionResponseError, type ExtractOptions, type ExtractResult, type FunctionCallItem, type GotoOptions, HandlerNotInitializedError, type HistoryEntry, type InitResult, LLMClient, type LLMResponse, LOG_LEVEL_NAMES, type LocalBrowserLaunchOptions, type LogLevel, type LogLine, type Logger, MissingEnvironmentVariableError, MissingLLMConfigurationError, type ModelProvider, type ObserveOptions, type ObserveResult, type OperatorResponse, type OperatorSummary, type Page, PlaywrightCommandException, PlaywrightCommandMethodNotSupportedException, type ResponseInputItem, type ResponseItem, Stagehand, StagehandAPIError, StagehandAPIUnauthorizedError, StagehandDefaultError, StagehandDeprecationError, StagehandDomProcessError, StagehandElementNotFoundError, StagehandEnvironmentError, StagehandError, StagehandEvalError, StagehandFunctionName, StagehandHttpError, StagehandInvalidArgumentError, type StagehandMetrics, StagehandMissingArgumentError, StagehandNotImplementedError, StagehandNotInitializedError, StagehandResponseBodyError, StagehandResponseParseError, StagehandServerError, type ToolUseItem, UnsupportedModelError, UnsupportedModelProviderError, type ZodPathSegments, defaultExtractSchema, operatorResponseSchema, operatorSummarySchema, pageTextSchema };
package/dist/index.js CHANGED
@@ -188,7 +188,7 @@ Please keep the user's instructions in mind when performing actions. If the user
188
188
  User Instructions:
189
189
  ${userProvidedInstructions}`;
190
190
  }
191
- function buildExtractSystemPrompt(isUsingPrintExtractedDataTool = false, useTextExtract = true, userProvidedInstructions) {
191
+ function buildExtractSystemPrompt(isUsingPrintExtractedDataTool = false, useTextExtract = false, userProvidedInstructions) {
192
192
  const baseContent = `You are extracting content on behalf of a user.
193
193
  If a user asks you to extract a 'list' of information, or 'all' information,
194
194
  YOU MUST EXTRACT ALL OF THE INFORMATION THAT THE USER REQUESTS.
@@ -207,7 +207,7 @@ ONLY print the content using the print_extracted_data tool provided.
207
207
  `.trim() : "";
208
208
  const additionalInstructions = useTextExtract ? `Once you are given the text-rendered webpage,
209
209
  you must thoroughly and meticulously analyze it. Be very careful to ensure that you
210
- do not miss any important information.` : "";
210
+ do not miss any important information.` : "If a user is attempting to extract links or URLs, you MUST respond with ONLY the IDs of the link elements. \nDo not attempt to extract links directly from the text unless absolutely necessary. ";
211
211
  const userInstructions = buildUserInstructionsString(
212
212
  userProvidedInstructions
213
213
  );
@@ -261,7 +261,6 @@ Refined content:`
261
261
  }
262
262
  var metadataSystemPrompt = `You are an AI assistant tasked with evaluating the progress and completion status of an extraction task.
263
263
  Analyze the extraction response and determine if the task is completed or if more information is needed.
264
-
265
264
  Strictly abide by the following criteria:
266
265
  1. Once the instruction has been satisfied by the current extraction response, ALWAYS set completion status to true and stop processing, regardless of remaining chunks.
267
266
  2. Only set completion status to false if BOTH of these conditions are true:
@@ -1233,6 +1232,9 @@ var StagehandActHandler = class {
1233
1232
  }
1234
1233
  };
1235
1234
 
1235
+ // lib/handlers/extractHandler.ts
1236
+ var import_zod3 = require("zod");
1237
+
1236
1238
  // lib/inference.ts
1237
1239
  var import_zod2 = require("zod");
1238
1240
 
@@ -2500,11 +2502,13 @@ var StagehandExtractHandler = class {
2500
2502
  level: 1
2501
2503
  });
2502
2504
  const outputString = tree.simplified;
2505
+ const idToUrlMapping = tree.idToUrl;
2506
+ const [transformedSchema, urlFieldPaths] = transformUrlStringsToNumericIds(schema);
2503
2507
  const extractionResponse = yield extract({
2504
2508
  instruction,
2505
2509
  previouslyExtractedContent: content,
2506
2510
  domElements: outputString,
2507
- schema,
2511
+ schema: transformedSchema,
2508
2512
  chunksSeen: 1,
2509
2513
  chunksTotal: 1,
2510
2514
  llmClient,
@@ -2565,6 +2569,9 @@ var StagehandExtractHandler = class {
2565
2569
  }
2566
2570
  });
2567
2571
  }
2572
+ for (const { segments } of urlFieldPaths) {
2573
+ injectUrls(output, segments, idToUrlMapping);
2574
+ }
2568
2575
  return output;
2569
2576
  });
2570
2577
  }
@@ -2679,6 +2686,172 @@ var StagehandExtractHandler = class {
2679
2686
  return deduplicated;
2680
2687
  }
2681
2688
  };
2689
+ function transformUrlStringsToNumericIds(schema) {
2690
+ const shape = schema._def.shape();
2691
+ const newShape = {};
2692
+ const urlPaths = [];
2693
+ let changed = false;
2694
+ for (const [key, value] of Object.entries(shape)) {
2695
+ const [childTransformed, childPaths] = transformSchema(value, [key]);
2696
+ newShape[key] = childTransformed;
2697
+ if (childTransformed !== value) {
2698
+ changed = true;
2699
+ }
2700
+ if (childPaths.length > 0) {
2701
+ childPaths.forEach((cp) => {
2702
+ urlPaths.push({ segments: [key, ...cp.segments] });
2703
+ });
2704
+ }
2705
+ }
2706
+ const finalSchema = changed ? import_zod3.z.object(newShape) : schema;
2707
+ return [finalSchema, urlPaths];
2708
+ }
2709
+ function transformSchema(schema, currentPath) {
2710
+ var _a, _b;
2711
+ if (schema instanceof import_zod3.z.ZodString) {
2712
+ const hasUrlCheck = (_b = (_a = schema._def.checks) == null ? void 0 : _a.some((check) => check.kind === "url")) != null ? _b : false;
2713
+ if (hasUrlCheck) {
2714
+ return [
2715
+ import_zod3.z.number().describe("ID of element that points to a URL"),
2716
+ [{ segments: [] }]
2717
+ ];
2718
+ }
2719
+ return [schema, []];
2720
+ }
2721
+ if (schema instanceof import_zod3.z.ZodObject) {
2722
+ const shape = schema._def.shape();
2723
+ const newShape = {};
2724
+ const urlPaths = [];
2725
+ let changed = false;
2726
+ const shapeKeys = Object.keys(shape);
2727
+ for (const key of shapeKeys) {
2728
+ const child = shape[key];
2729
+ const [transformedChild, childPaths] = transformSchema(child, [
2730
+ ...currentPath,
2731
+ key
2732
+ ]);
2733
+ if (transformedChild !== child) {
2734
+ changed = true;
2735
+ }
2736
+ newShape[key] = transformedChild;
2737
+ if (childPaths.length > 0) {
2738
+ for (const cp of childPaths) {
2739
+ urlPaths.push({ segments: [key, ...cp.segments] });
2740
+ }
2741
+ }
2742
+ }
2743
+ if (changed) {
2744
+ return [import_zod3.z.object(newShape), urlPaths];
2745
+ }
2746
+ return [schema, urlPaths];
2747
+ }
2748
+ if (schema instanceof import_zod3.z.ZodArray) {
2749
+ const itemType = schema._def.type;
2750
+ const [transformedItem, childPaths] = transformSchema(itemType, [
2751
+ ...currentPath,
2752
+ "*"
2753
+ ]);
2754
+ const changed = transformedItem !== itemType;
2755
+ const arrayPaths = childPaths.map((cp) => ({
2756
+ segments: ["*", ...cp.segments]
2757
+ }));
2758
+ if (changed) {
2759
+ return [import_zod3.z.array(transformedItem), arrayPaths];
2760
+ }
2761
+ return [schema, arrayPaths];
2762
+ }
2763
+ if (schema instanceof import_zod3.z.ZodUnion) {
2764
+ const unionOptions = schema._def.options;
2765
+ const newOptions = [];
2766
+ let changed = false;
2767
+ let allPaths = [];
2768
+ unionOptions.forEach((option, idx) => {
2769
+ const [newOption, childPaths] = transformSchema(option, [
2770
+ ...currentPath,
2771
+ `union_${idx}`
2772
+ ]);
2773
+ if (newOption !== option) {
2774
+ changed = true;
2775
+ }
2776
+ newOptions.push(newOption);
2777
+ allPaths = [...allPaths, ...childPaths];
2778
+ });
2779
+ if (changed) {
2780
+ return [
2781
+ import_zod3.z.union(newOptions),
2782
+ allPaths
2783
+ ];
2784
+ }
2785
+ return [schema, allPaths];
2786
+ }
2787
+ if (schema instanceof import_zod3.z.ZodIntersection) {
2788
+ const leftType = schema._def.left;
2789
+ const rightType = schema._def.right;
2790
+ const [left, leftPaths] = transformSchema(leftType, [
2791
+ ...currentPath,
2792
+ "intersection_left"
2793
+ ]);
2794
+ const [right, rightPaths] = transformSchema(rightType, [
2795
+ ...currentPath,
2796
+ "intersection_right"
2797
+ ]);
2798
+ const changed = left !== leftType || right !== rightType;
2799
+ const allPaths = [...leftPaths, ...rightPaths];
2800
+ if (changed) {
2801
+ return [import_zod3.z.intersection(left, right), allPaths];
2802
+ }
2803
+ return [schema, allPaths];
2804
+ }
2805
+ if (schema instanceof import_zod3.z.ZodOptional) {
2806
+ const innerType = schema._def.innerType;
2807
+ const [inner, innerPaths] = transformSchema(innerType, currentPath);
2808
+ if (inner !== innerType) {
2809
+ return [import_zod3.z.optional(inner), innerPaths];
2810
+ }
2811
+ return [schema, innerPaths];
2812
+ }
2813
+ if (schema instanceof import_zod3.z.ZodNullable) {
2814
+ const innerType = schema._def.innerType;
2815
+ const [inner, innerPaths] = transformSchema(innerType, currentPath);
2816
+ if (inner !== innerType) {
2817
+ return [import_zod3.z.nullable(inner), innerPaths];
2818
+ }
2819
+ return [schema, innerPaths];
2820
+ }
2821
+ if (schema instanceof import_zod3.z.ZodEffects) {
2822
+ const baseSchema = schema._def.schema;
2823
+ const [newBaseSchema, basePaths] = transformSchema(baseSchema, currentPath);
2824
+ if (newBaseSchema !== baseSchema) {
2825
+ return [import_zod3.z.effect(newBaseSchema, schema._def.effect), basePaths];
2826
+ }
2827
+ return [schema, basePaths];
2828
+ }
2829
+ return [schema, []];
2830
+ }
2831
+ function injectUrls(obj, path4, idToUrlMapping) {
2832
+ if (path4.length === 0) return;
2833
+ const [key, ...rest] = path4;
2834
+ if (key === "*") {
2835
+ if (Array.isArray(obj)) {
2836
+ for (const item of obj) {
2837
+ injectUrls(item, rest, idToUrlMapping);
2838
+ }
2839
+ }
2840
+ return;
2841
+ }
2842
+ if (obj && typeof obj === "object") {
2843
+ const record = obj;
2844
+ if (path4.length === 1) {
2845
+ const fieldValue = record[key];
2846
+ if (typeof fieldValue === "number") {
2847
+ const mappedUrl = idToUrlMapping[String(fieldValue)];
2848
+ record[key] = mappedUrl != null ? mappedUrl : ``;
2849
+ }
2850
+ } else {
2851
+ injectUrls(record[key], rest, idToUrlMapping);
2852
+ }
2853
+ }
2854
+ }
2682
2855
 
2683
2856
  // lib/handlers/observeHandler.ts
2684
2857
  var StagehandObserveHandler = class {
@@ -5647,7 +5820,7 @@ var GroqClient = class extends LLMClient {
5647
5820
 
5648
5821
  // lib/llm/OpenAIClient.ts
5649
5822
  var import_openai3 = __toESM(require("openai"));
5650
- var import_zod3 = require("openai/helpers/zod");
5823
+ var import_zod4 = require("openai/helpers/zod");
5651
5824
  var import_zod_to_json_schema6 = __toESM(require("zod-to-json-schema"));
5652
5825
  var OpenAIClient = class extends LLMClient {
5653
5826
  constructor({
@@ -5835,7 +6008,7 @@ ${parsedSchema}
5835
6008
  throw error;
5836
6009
  }
5837
6010
  } else {
5838
- responseFormat = (0, import_zod3.zodResponseFormat)(
6011
+ responseFormat = (0, import_zod4.zodResponseFormat)(
5839
6012
  options.response_model.schema,
5840
6013
  options.response_model.name
5841
6014
  );
@@ -7697,12 +7870,12 @@ var StagehandAgentHandler = class {
7697
7870
  };
7698
7871
 
7699
7872
  // types/operator.ts
7700
- var import_zod4 = require("zod");
7701
- var operatorResponseSchema = import_zod4.z.object({
7702
- reasoning: import_zod4.z.string().describe(
7873
+ var import_zod5 = require("zod");
7874
+ var operatorResponseSchema = import_zod5.z.object({
7875
+ reasoning: import_zod5.z.string().describe(
7703
7876
  "The reasoning for the step taken. If this step's method is `close`, the goal was to extract data, and the task was successful, state the data that was extracted."
7704
7877
  ),
7705
- method: import_zod4.z.enum([
7878
+ method: import_zod5.z.enum([
7706
7879
  "act",
7707
7880
  "extract",
7708
7881
  "goto",
@@ -7718,7 +7891,7 @@ var operatorResponseSchema = import_zod4.z.object({
7718
7891
  wait: Wait for a period of time.
7719
7892
  navback: Navigate back to the previous page. Do not navigate back if you are already on the first page.
7720
7893
  refresh: Refresh the page.`),
7721
- parameters: import_zod4.z.string().describe(
7894
+ parameters: import_zod5.z.string().describe(
7722
7895
  `The parameter for the action. Only pass in a parameter for the following methods:
7723
7896
  - act: The action to perform. e.g. "click on the submit button" or "type [email] into the email input field and press enter"
7724
7897
  - extract: The data to extract. e.g. "the title of the article". If you want to extract all of the text on the page, leave this undefined.
@@ -7726,12 +7899,12 @@ var operatorResponseSchema = import_zod4.z.object({
7726
7899
  - goto: The URL to navigate to. e.g. "https://www.google.com"
7727
7900
  The other methods do not require a parameter.`
7728
7901
  ).optional(),
7729
- taskComplete: import_zod4.z.boolean().describe(
7902
+ taskComplete: import_zod5.z.boolean().describe(
7730
7903
  "Whether the task is complete. If true, the task is complete and no more steps are needed. If you chose to close the task because the goal is not achievable, set this to false."
7731
7904
  )
7732
7905
  });
7733
- var operatorSummarySchema = import_zod4.z.object({
7734
- answer: import_zod4.z.string().describe("The final answer to the original instruction.")
7906
+ var operatorSummarySchema = import_zod5.z.object({
7907
+ answer: import_zod5.z.string().describe("The final answer to the original instruction.")
7735
7908
  });
7736
7909
 
7737
7910
  // lib/handlers/operatorHandler.ts
@@ -8133,8 +8306,8 @@ var LOG_LEVEL_NAMES = {
8133
8306
  };
8134
8307
 
8135
8308
  // types/model.ts
8136
- var import_zod5 = require("zod");
8137
- var AvailableModelSchema = import_zod5.z.enum([
8309
+ var import_zod6 = require("zod");
8310
+ var AvailableModelSchema = import_zod6.z.enum([
8138
8311
  "gpt-4o",
8139
8312
  "gpt-4o-mini",
8140
8313
  "gpt-4o-2024-08-06",
@@ -8317,18 +8490,6 @@ function getBrowser(apiKey, projectId, env = "LOCAL", headless = false, logger,
8317
8490
  }
8318
8491
  }
8319
8492
  });
8320
- if (localBrowserLaunchOptions) {
8321
- logger({
8322
- category: "init",
8323
- message: "local browser launch options",
8324
- auxiliary: {
8325
- localLaunchOptions: {
8326
- value: JSON.stringify(localBrowserLaunchOptions),
8327
- type: "string"
8328
- }
8329
- }
8330
- });
8331
- }
8332
8493
  if (localBrowserLaunchOptions == null ? void 0 : localBrowserLaunchOptions.cdpUrl) {
8333
8494
  if (!localBrowserLaunchOptions.cdpUrl.includes("connect.connect")) {
8334
8495
  logger({
@@ -1,4 +1,5 @@
1
- import { z } from "zod";
1
+ import { z, ZodTypeAny } from "zod";
2
+ import { ZodPathSegments } from "../../types/stagehand";
2
3
  import { LLMClient } from "../llm/LLMClient";
3
4
  import { StagehandPage } from "../StagehandPage";
4
5
  import { Stagehand } from "../index";
@@ -97,3 +98,35 @@ export declare class StagehandExtractHandler {
97
98
  */
98
99
  private deduplicateAnnotations;
99
100
  }
101
+ /**
102
+ * Scans the provided Zod schema for any `z.string().url()` fields and
103
+ * replaces them with `z.number()`.
104
+ *
105
+ * @param schema - The Zod object schema to transform.
106
+ * @returns A tuple containing:
107
+ * 1. The transformed schema (or the original schema if no changes were needed).
108
+ * 2. An array of {@link ZodPathSegments} objects representing all the replaced URL fields,
109
+ * with each path segment showing where in the schema the replacement occurred.
110
+ */
111
+ export declare function transformUrlStringsToNumericIds<T extends z.ZodObject<z.ZodRawShape>>(schema: T): [T, ZodPathSegments[]];
112
+ /**
113
+ * Recursively traverses a given Zod schema, scanning for any fields of type `z.string().url()`.
114
+ * For each such field, it replaces the `z.string().url()` with `z.number()`.
115
+ *
116
+ * This function is used internally by higher-level utilities (e.g., transforming entire object schemas)
117
+ * and handles nested objects, arrays, unions, intersections, optionals.
118
+ *
119
+ * @param schema - The Zod schema to transform.
120
+ * @param currentPath - An array of string/number keys representing the current schema path (used internally for recursion).
121
+ * @returns A two-element tuple:
122
+ * 1. The updated Zod schema, with any `.url()` fields replaced by `z.number()`.
123
+ * 2. An array of {@link ZodPathSegments} objects representing each replaced field, including the path segments.
124
+ */
125
+ export declare function transformSchema(schema: ZodTypeAny, currentPath: Array<string | number>): [ZodTypeAny, ZodPathSegments[]];
126
+ /**
127
+ * Once we get the final extracted object that has numeric IDs in place of URLs,
128
+ * use `injectUrls` to walk the object and replace numeric IDs
129
+ * with the real URL strings from idToUrlMapping. The `path` may include `*`
130
+ * for array indices (indicating "all items in the array").
131
+ */
132
+ export declare function injectUrls(obj: unknown, path: Array<string | number>, idToUrlMapping: Record<string, string>): void;
@@ -269,3 +269,20 @@ export interface HistoryEntry {
269
269
  result: unknown;
270
270
  timestamp: string;
271
271
  }
272
+ /**
273
+ * Represents a path through a Zod schema from the root object down to a
274
+ * particular field. The `segments` array describes the chain of keys/indices.
275
+ *
276
+ * - **String** segments indicate object property names.
277
+ * - **Number** segments indicate array indices.
278
+ *
279
+ * For example, `["users", 0, "homepage"]` might describe reaching
280
+ * the `homepage` field in `schema.users[0].homepage`.
281
+ */
282
+ export interface ZodPathSegments {
283
+ /**
284
+ * The ordered list of keys/indices leading from the schema root
285
+ * to the targeted field.
286
+ */
287
+ segments: Array<string | number>;
288
+ }
@@ -1,5 +1,6 @@
1
- import { z } from "zod";
1
+ import { z, ZodTypeAny } from "zod";
2
2
  import { LogLine } from "../../types/log";
3
+ import { ZodPathSegments } from "../../types/stagehand";
3
4
  import { TextAnnotation } from "../../types/textannotation";
4
5
  import { extract } from "../inference";
5
6
  import { LLMClient } from "../llm/LLMClient";
@@ -420,12 +421,18 @@ export class StagehandExtractHandler {
420
421
  level: 1,
421
422
  });
422
423
  const outputString = tree.simplified;
424
+ const idToUrlMapping = tree.idToUrl;
423
425
 
426
+ // Transform user defined schema to replace string().url() with .number()
427
+ const [transformedSchema, urlFieldPaths] =
428
+ transformUrlStringsToNumericIds(schema);
429
+
430
+ // call extract inference with transformed schema
424
431
  const extractionResponse = await extract({
425
432
  instruction,
426
433
  previouslyExtractedContent: content,
427
434
  domElements: outputString,
428
- schema,
435
+ schema: transformedSchema,
429
436
  chunksSeen: 1,
430
437
  chunksTotal: 1,
431
438
  llmClient,
@@ -486,9 +493,14 @@ export class StagehandExtractHandler {
486
493
  },
487
494
  });
488
495
  }
489
- return output;
490
- }
491
496
 
497
+ // revert to original schema and populate with URLs
498
+ for (const { segments } of urlFieldPaths) {
499
+ injectUrls(output, segments, idToUrlMapping);
500
+ }
501
+
502
+ return output as z.infer<T>;
503
+ }
492
504
  /**
493
505
  * Get the width, height, and offsets of either the entire page or a specific element.
494
506
  * (Matches your existing getTargetDimensions logic, just adapted to accept a string | undefined.)
@@ -642,3 +654,243 @@ export class StagehandExtractHandler {
642
654
  return deduplicated;
643
655
  }
644
656
  }
657
+
658
+ /**
659
+ * Scans the provided Zod schema for any `z.string().url()` fields and
660
+ * replaces them with `z.number()`.
661
+ *
662
+ * @param schema - The Zod object schema to transform.
663
+ * @returns A tuple containing:
664
+ * 1. The transformed schema (or the original schema if no changes were needed).
665
+ * 2. An array of {@link ZodPathSegments} objects representing all the replaced URL fields,
666
+ * with each path segment showing where in the schema the replacement occurred.
667
+ */
668
+ export function transformUrlStringsToNumericIds<
669
+ T extends z.ZodObject<z.ZodRawShape>,
670
+ >(schema: T): [T, ZodPathSegments[]] {
671
+ const shape = schema._def.shape();
672
+ const newShape: Record<string, ZodTypeAny> = {};
673
+ const urlPaths: ZodPathSegments[] = [];
674
+ let changed = false;
675
+
676
+ for (const [key, value] of Object.entries(shape)) {
677
+ const [childTransformed, childPaths] = transformSchema(value, [key]);
678
+ newShape[key] = childTransformed;
679
+ if (childTransformed !== value) {
680
+ changed = true;
681
+ }
682
+ if (childPaths.length > 0) {
683
+ childPaths.forEach((cp) => {
684
+ urlPaths.push({ segments: [key, ...cp.segments] });
685
+ });
686
+ }
687
+ }
688
+
689
+ const finalSchema = changed ? z.object(newShape) : schema;
690
+ return [finalSchema as T, urlPaths];
691
+ }
692
+
693
+ /**
694
+ * Recursively traverses a given Zod schema, scanning for any fields of type `z.string().url()`.
695
+ * For each such field, it replaces the `z.string().url()` with `z.number()`.
696
+ *
697
+ * This function is used internally by higher-level utilities (e.g., transforming entire object schemas)
698
+ * and handles nested objects, arrays, unions, intersections, optionals.
699
+ *
700
+ * @param schema - The Zod schema to transform.
701
+ * @param currentPath - An array of string/number keys representing the current schema path (used internally for recursion).
702
+ * @returns A two-element tuple:
703
+ * 1. The updated Zod schema, with any `.url()` fields replaced by `z.number()`.
704
+ * 2. An array of {@link ZodPathSegments} objects representing each replaced field, including the path segments.
705
+ */
706
+ export function transformSchema(
707
+ schema: ZodTypeAny,
708
+ currentPath: Array<string | number>,
709
+ ): [ZodTypeAny, ZodPathSegments[]] {
710
+ // 1) If it's a string with .url(), convert to z.number()
711
+ if (schema instanceof z.ZodString) {
712
+ const hasUrlCheck =
713
+ schema._def.checks?.some((check) => check.kind === "url") ?? false;
714
+ if (hasUrlCheck) {
715
+ return [
716
+ z.number().describe("ID of element that points to a URL"),
717
+ [{ segments: [] }],
718
+ ];
719
+ }
720
+ return [schema, []];
721
+ }
722
+
723
+ // 2) If it's an object, transform each field
724
+ if (schema instanceof z.ZodObject) {
725
+ // The shape is a raw object containing fields keyed by string (no symbols):
726
+ const shape = schema._def.shape() as Record<string, ZodTypeAny>;
727
+ const newShape: Record<string, ZodTypeAny> = {};
728
+ const urlPaths: ZodPathSegments[] = [];
729
+ let changed = false;
730
+
731
+ const shapeKeys = Object.keys(shape);
732
+
733
+ for (const key of shapeKeys) {
734
+ const child = shape[key];
735
+ const [transformedChild, childPaths] = transformSchema(child, [
736
+ ...currentPath,
737
+ key,
738
+ ]);
739
+
740
+ if (transformedChild !== child) {
741
+ changed = true;
742
+ }
743
+ newShape[key] = transformedChild;
744
+
745
+ if (childPaths.length > 0) {
746
+ for (const cp of childPaths) {
747
+ urlPaths.push({ segments: [key, ...cp.segments] });
748
+ }
749
+ }
750
+ }
751
+
752
+ if (changed) {
753
+ return [z.object(newShape), urlPaths];
754
+ }
755
+ return [schema, urlPaths];
756
+ }
757
+
758
+ // 3) If it's an array, transform its item type
759
+ if (schema instanceof z.ZodArray) {
760
+ const itemType = schema._def.type as ZodTypeAny;
761
+ const [transformedItem, childPaths] = transformSchema(itemType, [
762
+ ...currentPath,
763
+ "*",
764
+ ]);
765
+ const changed = transformedItem !== itemType;
766
+ const arrayPaths: ZodPathSegments[] = childPaths.map((cp) => ({
767
+ segments: ["*", ...cp.segments],
768
+ }));
769
+
770
+ if (changed) {
771
+ return [z.array(transformedItem), arrayPaths];
772
+ }
773
+ return [schema, arrayPaths];
774
+ }
775
+
776
+ // 4) If it's a union, transform each option
777
+ if (schema instanceof z.ZodUnion) {
778
+ // Cast the union’s options to an array of ZodTypeAny
779
+ const unionOptions = schema._def.options as ZodTypeAny[];
780
+ const newOptions: ZodTypeAny[] = [];
781
+ let changed = false;
782
+ let allPaths: ZodPathSegments[] = [];
783
+
784
+ unionOptions.forEach((option: ZodTypeAny, idx: number) => {
785
+ const [newOption, childPaths] = transformSchema(option, [
786
+ ...currentPath,
787
+ `union_${idx}`,
788
+ ]);
789
+ if (newOption !== option) {
790
+ changed = true;
791
+ }
792
+ newOptions.push(newOption);
793
+ allPaths = [...allPaths, ...childPaths];
794
+ });
795
+
796
+ if (changed) {
797
+ // We assume at least two options remain:
798
+ return [
799
+ z.union(newOptions as [ZodTypeAny, ZodTypeAny, ...ZodTypeAny[]]),
800
+ allPaths,
801
+ ];
802
+ }
803
+ return [schema, allPaths];
804
+ }
805
+
806
+ // 5) If it's an intersection, transform left and right
807
+ if (schema instanceof z.ZodIntersection) {
808
+ const leftType = schema._def.left as ZodTypeAny;
809
+ const rightType = schema._def.right as ZodTypeAny;
810
+
811
+ const [left, leftPaths] = transformSchema(leftType, [
812
+ ...currentPath,
813
+ "intersection_left",
814
+ ]);
815
+ const [right, rightPaths] = transformSchema(rightType, [
816
+ ...currentPath,
817
+ "intersection_right",
818
+ ]);
819
+ const changed = left !== leftType || right !== rightType;
820
+ const allPaths = [...leftPaths, ...rightPaths];
821
+ if (changed) {
822
+ return [z.intersection(left, right), allPaths];
823
+ }
824
+ return [schema, allPaths];
825
+ }
826
+
827
+ // 6) If it's optional, transform inner
828
+ if (schema instanceof z.ZodOptional) {
829
+ const innerType = schema._def.innerType as ZodTypeAny;
830
+ const [inner, innerPaths] = transformSchema(innerType, currentPath);
831
+ if (inner !== innerType) {
832
+ return [z.optional(inner), innerPaths];
833
+ }
834
+ return [schema, innerPaths];
835
+ }
836
+
837
+ // 7) If it's nullable, transform inner
838
+ if (schema instanceof z.ZodNullable) {
839
+ const innerType = schema._def.innerType as ZodTypeAny;
840
+ const [inner, innerPaths] = transformSchema(innerType, currentPath);
841
+ if (inner !== innerType) {
842
+ return [z.nullable(inner), innerPaths];
843
+ }
844
+ return [schema, innerPaths];
845
+ }
846
+
847
+ // 8) If it's an effect, transform base schema
848
+ if (schema instanceof z.ZodEffects) {
849
+ const baseSchema = schema._def.schema as ZodTypeAny;
850
+ const [newBaseSchema, basePaths] = transformSchema(baseSchema, currentPath);
851
+ if (newBaseSchema !== baseSchema) {
852
+ return [z.effect(newBaseSchema, schema._def.effect), basePaths];
853
+ }
854
+ return [schema, basePaths];
855
+ }
856
+
857
+ // 9) If none of the above, return as-is
858
+ return [schema, []];
859
+ }
860
+
861
+ /**
862
+ * Once we get the final extracted object that has numeric IDs in place of URLs,
863
+ * use `injectUrls` to walk the object and replace numeric IDs
864
+ * with the real URL strings from idToUrlMapping. The `path` may include `*`
865
+ * for array indices (indicating "all items in the array").
866
+ */
867
+ export function injectUrls(
868
+ obj: unknown,
869
+ path: Array<string | number>,
870
+ idToUrlMapping: Record<string, string>,
871
+ ): void {
872
+ if (path.length === 0) return;
873
+ const [key, ...rest] = path;
874
+
875
+ if (key === "*") {
876
+ if (Array.isArray(obj)) {
877
+ for (const item of obj) {
878
+ injectUrls(item, rest, idToUrlMapping);
879
+ }
880
+ }
881
+ return;
882
+ }
883
+
884
+ if (obj && typeof obj === "object") {
885
+ const record = obj as Record<string | number, unknown>;
886
+ if (path.length === 1) {
887
+ const fieldValue = record[key];
888
+ if (typeof fieldValue === "number") {
889
+ const mappedUrl = idToUrlMapping[String(fieldValue)];
890
+ record[key] = mappedUrl ?? ``;
891
+ }
892
+ } else {
893
+ injectUrls(record[key], rest, idToUrlMapping);
894
+ }
895
+ }
896
+ }
package/lib/index.ts CHANGED
@@ -236,19 +236,6 @@ async function getBrowser(
236
236
  },
237
237
  });
238
238
 
239
- if (localBrowserLaunchOptions) {
240
- logger({
241
- category: "init",
242
- message: "local browser launch options",
243
- auxiliary: {
244
- localLaunchOptions: {
245
- value: JSON.stringify(localBrowserLaunchOptions),
246
- type: "string",
247
- },
248
- },
249
- });
250
- }
251
-
252
239
  if (localBrowserLaunchOptions?.cdpUrl) {
253
240
  if (!localBrowserLaunchOptions.cdpUrl.includes("connect.connect")) {
254
241
  logger({
package/lib/prompt.ts CHANGED
@@ -18,7 +18,7 @@ ${userProvidedInstructions}`;
18
18
  // extract
19
19
  export function buildExtractSystemPrompt(
20
20
  isUsingPrintExtractedDataTool: boolean = false,
21
- useTextExtract: boolean = true,
21
+ useTextExtract: boolean = false,
22
22
  userProvidedInstructions?: string,
23
23
  ): ChatMessage {
24
24
  const baseContent = `You are extracting content on behalf of a user.
@@ -51,7 +51,8 @@ ONLY print the content using the print_extracted_data tool provided.
51
51
  ? `Once you are given the text-rendered webpage,
52
52
  you must thoroughly and meticulously analyze it. Be very careful to ensure that you
53
53
  do not miss any important information.`
54
- : "";
54
+ : "If a user is attempting to extract links or URLs, you MUST respond with ONLY the IDs of the link elements. \n" +
55
+ "Do not attempt to extract links directly from the text unless absolutely necessary. ";
55
56
 
56
57
  const userInstructions = buildUserInstructionsString(
57
58
  userProvidedInstructions,
@@ -119,7 +120,6 @@ Refined content:`,
119
120
 
120
121
  const metadataSystemPrompt = `You are an AI assistant tasked with evaluating the progress and completion status of an extraction task.
121
122
  Analyze the extraction response and determine if the task is completed or if more information is needed.
122
-
123
123
  Strictly abide by the following criteria:
124
124
  1. Once the instruction has been satisfied by the current extraction response, ALWAYS set completion status to true and stop processing, regardless of remaining chunks.
125
125
  2. Only set completion status to false if BOTH of these conditions are true:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@browserbasehq/stagehand",
3
- "version": "2.1.0",
3
+ "version": "2.2.0-alpha-8814af9ece99fddc3dd9fb32671d0513a3a00c67",
4
4
  "description": "An AI web browsing framework focused on simplicity and extensibility.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",