Skip to content

Commit 11e015d

Browse files
add history primitive (#600)
* expose history * add eval * changeset * handle history call on uninitialized page * export history entry type * add extract with no args to history --------- Co-authored-by: Sean McGuire <[email protected]>
1 parent 2a14a60 commit 11e015d

File tree

6 files changed

+130
-7
lines changed

6 files changed

+130
-7
lines changed

Diff for: .changeset/empty-spoons-float.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": minor
3+
---
4+
5+
Added a `stagehand.history` array which stores an array of `act`, `extract`, `observe`, and `goto` calls made. Since this history array is stored on the `StagehandPage` level, it will capture methods even if indirectly called by an agent.

Diff for: evals/evals.config.json

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
{
22
"tasks": [
3+
{
4+
"name": "history",
5+
"categories": ["combination"]
6+
},
37
{
48
"name": "expect_act_timeout",
59
"categories": ["act"]

Diff for: evals/tasks/history.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { initStagehand } from "@/evals/initStagehand";
2+
import { EvalFunction } from "@/types/evals";
3+
4+
export const history: EvalFunction = async ({ modelName, logger }) => {
5+
const { stagehand, initResponse } = await initStagehand({
6+
modelName,
7+
logger,
8+
});
9+
10+
const { debugUrl, sessionUrl } = initResponse;
11+
12+
await stagehand.page.goto("https://docs.stagehand.dev");
13+
14+
await stagehand.page.act("click on the 'Quickstart' tab");
15+
16+
await stagehand.page.extract("Extract the title of the page");
17+
18+
await stagehand.page.observe("Find all links on the page");
19+
20+
const history = stagehand.history;
21+
22+
const hasCorrectNumberOfEntries = history.length === 4;
23+
24+
const hasNavigateEntry = history[0].method === "navigate";
25+
const hasActEntry = history[1].method === "act";
26+
const hasExtractEntry = history[2].method === "extract";
27+
const hasObserveEntry = history[3].method === "observe";
28+
29+
const allEntriesHaveTimestamps = history.every(
30+
(entry) =>
31+
typeof entry.timestamp === "string" && entry.timestamp.length > 0,
32+
);
33+
const allEntriesHaveResults = history.every(
34+
(entry) => entry.result !== undefined,
35+
);
36+
37+
await stagehand.close();
38+
39+
const success =
40+
hasCorrectNumberOfEntries &&
41+
hasNavigateEntry &&
42+
hasActEntry &&
43+
hasExtractEntry &&
44+
hasObserveEntry &&
45+
allEntriesHaveTimestamps &&
46+
allEntriesHaveResults;
47+
48+
return {
49+
_success: success,
50+
debugUrl,
51+
sessionUrl,
52+
logs: logger.getLogs(),
53+
};
54+
};

Diff for: lib/StagehandPage.ts

+49-7
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Page, defaultExtractSchema } from "../types/page";
66
import {
77
ExtractOptions,
88
ExtractResult,
9+
HistoryEntry,
910
ObserveOptions,
1011
ObserveResult,
1112
} from "../types/stagehand";
@@ -39,6 +40,11 @@ export class StagehandPage {
3940
private userProvidedInstructions?: string;
4041
private waitForCaptchaSolves: boolean;
4142
private initialized: boolean = false;
43+
private _history: Array<HistoryEntry> = [];
44+
45+
public get history(): ReadonlyArray<HistoryEntry> {
46+
return this._history;
47+
}
4248

4349
constructor(
4450
page: PlaywrightPage,
@@ -294,6 +300,8 @@ export class StagehandPage {
294300
? await this.api.goto(url, options)
295301
: await target.goto(url, options);
296302

303+
this.addToHistory("navigate", { url, options }, result);
304+
297305
if (this.waitForCaptchaSolves) {
298306
try {
299307
await this.waitForCaptchaSolve(1000);
@@ -440,6 +448,19 @@ export class StagehandPage {
440448
}
441449
}
442450

451+
private addToHistory(
452+
method: HistoryEntry["method"],
453+
parameters: unknown,
454+
result?: unknown,
455+
): void {
456+
this._history.push({
457+
method,
458+
parameters,
459+
result: result ?? null,
460+
timestamp: new Date().toISOString(),
461+
});
462+
}
463+
443464
async act(
444465
actionOrOptions: string | ActOptions | ObserveResult,
445466
): Promise<ActResult> {
@@ -501,6 +522,7 @@ export class StagehandPage {
501522
if (this.api) {
502523
const result = await this.api.act(actionOrOptions);
503524
await this._refreshPageFromAPI();
525+
this.addToHistory("act", actionOrOptions, result);
504526
return result;
505527
}
506528

@@ -539,7 +561,7 @@ export class StagehandPage {
539561
});
540562

541563
// `useVision` is no longer passed to the handler
542-
return this.actHandler
564+
const result = await this.actHandler
543565
.act({
544566
action,
545567
llmClient,
@@ -574,6 +596,10 @@ export class StagehandPage {
574596
action: action,
575597
};
576598
});
599+
600+
this.addToHistory("act", actionOrOptions, result);
601+
602+
return result;
577603
}
578604

579605
async extract<T extends z.AnyZodObject = typeof defaultExtractSchema>(
@@ -587,10 +613,14 @@ export class StagehandPage {
587613

588614
// check if user called extract() with no arguments
589615
if (!instructionOrOptions) {
616+
let result: ExtractResult<T>;
590617
if (this.api) {
591-
return this.api.extract<T>({});
618+
result = await this.api.extract<T>({});
619+
} else {
620+
result = await this.extractHandler.extract();
592621
}
593-
return this.extractHandler.extract();
622+
this.addToHistory("extract", instructionOrOptions, result);
623+
return result;
594624
}
595625

596626
const options: ExtractOptions<T> =
@@ -620,7 +650,9 @@ export class StagehandPage {
620650
}
621651

622652
if (this.api) {
623-
return this.api.extract<T>(options);
653+
const result = await this.api.extract<T>(options);
654+
this.addToHistory("extract", instructionOrOptions, result);
655+
return result;
624656
}
625657

626658
const requestId = Math.random().toString(36).substring(2);
@@ -648,7 +680,7 @@ export class StagehandPage {
648680
},
649681
});
650682

651-
return this.extractHandler
683+
const result = await this.extractHandler
652684
.extract({
653685
instruction,
654686
schema,
@@ -681,6 +713,10 @@ export class StagehandPage {
681713

682714
throw e;
683715
});
716+
717+
this.addToHistory("extract", instructionOrOptions, result);
718+
719+
return result;
684720
}
685721

686722
async observe(
@@ -734,7 +770,9 @@ export class StagehandPage {
734770
}
735771

736772
if (this.api) {
737-
return this.api.observe(options);
773+
const result = await this.api.observe(options);
774+
this.addToHistory("observe", instructionOrOptions, result);
775+
return result;
738776
}
739777

740778
const requestId = Math.random().toString(36).substring(2);
@@ -766,7 +804,7 @@ export class StagehandPage {
766804
},
767805
});
768806

769-
return this.observeHandler
807+
const result = await this.observeHandler
770808
.observe({
771809
instruction,
772810
llmClient,
@@ -807,6 +845,10 @@ export class StagehandPage {
807845

808846
throw e;
809847
});
848+
849+
this.addToHistory("observe", instructionOrOptions, result);
850+
851+
return result;
810852
}
811853

812854
async getCDPClient(): Promise<CDPSession> {

Diff for: lib/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
AgentConfig,
2929
StagehandMetrics,
3030
StagehandFunctionName,
31+
HistoryEntry,
3132
} from "../types/stagehand";
3233
import { StagehandContext } from "./StagehandContext";
3334
import { StagehandPage } from "./StagehandPage";
@@ -899,6 +900,16 @@ export class Stagehand {
899900
},
900901
};
901902
}
903+
904+
public get history(): ReadonlyArray<HistoryEntry> {
905+
if (!this.stagehandPage) {
906+
throw new Error(
907+
"History is only available after a page has been initialized",
908+
);
909+
}
910+
911+
return this.stagehandPage.history;
912+
}
902913
}
903914

904915
export * from "../types/browser";

Diff for: types/stagehand.ts

+7
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,10 @@ export enum StagehandFunctionName {
244244
EXTRACT = "EXTRACT",
245245
OBSERVE = "OBSERVE",
246246
}
247+
248+
export interface HistoryEntry {
249+
method: "act" | "extract" | "observe" | "navigate";
250+
parameters: unknown;
251+
result: unknown;
252+
timestamp: string;
253+
}

0 commit comments

Comments
 (0)