Skip to content

Commit 1ce2853

Browse files
authored
Expose stream and watch methods (#43)
* Expose `stream` and `watch` * fixes
1 parent b92b144 commit 1ce2853

File tree

11 files changed

+367
-49
lines changed

11 files changed

+367
-49
lines changed

.fernignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Specify files that shouldn't be modified by Fern
22
src/wrapper/
3+
test/wrapper/
4+
src/index.ts
35
examples/
46
.vscode/
57

6-
.gitignore
8+
.gitignore
9+
jest.config.mjs

README.md

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -57,26 +57,78 @@ for (const post of result.parsed.posts) {
5757

5858
### Streaming Agent Updates
5959

60+
> You can use the `stream` method to get the latest step taken on every change.
61+
6062
```ts
6163
const task = await browseruse.tasks.createTask({
6264
task: "Search for the top 10 Hacker News posts and return the title and url.",
6365
schema: TaskOutput,
6466
});
6567

66-
for await (const msg of task.stream()) {
67-
switch (msg.status) {
68-
case "started":
69-
case "paused":
70-
case "stopped":
71-
console.log(`running: ${msg}`);
72-
break;
68+
for await (const step of task.stream()) {
69+
console.log(step);
70+
}
7371

74-
case "finished":
75-
console.log(`done:`);
72+
const result = await task.complete();
7673

77-
for (const post of msg.parsed.posts) {
78-
console.log(`${post.title} - ${post.url}`);
79-
}
74+
for (const post of result.parsed.posts) {
75+
console.log(`${post.title} - ${post.url}`);
76+
}
77+
```
78+
79+
### Watching Agent Updates
80+
81+
> You can use the `watch` method to get the latest update on every change.
82+
83+
```ts
84+
const task = await browseruse.tasks.createTask({
85+
task: "Search for the top 10 Hacker News posts and return the title and url.",
86+
schema: TaskOutput,
87+
});
88+
89+
for await (const update of task.watch()) {
90+
console.log(update);
91+
92+
if (update.data.status === "finished") {
93+
for (const post of update.data.parsed.posts) {
94+
console.log(`${post.title} - ${post.url}`);
95+
}
96+
}
97+
}
98+
```
99+
100+
## Webhook Verification
101+
102+
> We encourage you to use the SDK functions that verify and parse webhook events.
103+
104+
```ts
105+
import { verifyWebhookEventSignature, type WebhookAgentTaskStatusUpdatePayload } from "browser-use-sdk";
106+
107+
export async function POST(req: Request) {
108+
const signature = req.headers["x-browser-use-signature"] as string;
109+
const timestamp = req.headers["x-browser-use-timestamp"] as string;
110+
111+
const event = await verifyWebhookEventSignature(
112+
{
113+
body,
114+
signature,
115+
timestamp,
116+
},
117+
{
118+
secret: SECRET_KEY,
119+
},
120+
);
121+
122+
if (!event.ok) {
123+
return;
124+
}
125+
126+
switch (event.event.type) {
127+
case "agent.task.status_update":
128+
break;
129+
case "test":
130+
break;
131+
default:
80132
break;
81133
}
82134
}

examples/retrieve.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ env();
1111
// gets API Key from environment variable BROWSER_USE_API_KEY
1212
const browseruse = new BrowserUseClient({
1313
apiKey: process.env.BROWSER_USE_API_KEY!,
14-
environment: "https://api.browser-use.com/api/v2",
1514
});
1615

1716
// Basic ---------------------------------------------------------------------
@@ -23,7 +22,8 @@ async function basic() {
2322
// Create Task
2423
const rsp = await browseruse.tasks.createTask({
2524
task: "What's the weather in SF and what's the temperature?",
26-
agent: { llm: "gemini-2.5-flash" },
25+
llm: "gemini-2.5-flash",
26+
schema: TaskOutput,
2727
});
2828

2929
poll: do {
@@ -67,7 +67,7 @@ async function structured() {
6767
const rsp = await browseruse.tasks.createTask({
6868
task: "What's the weather in SF and what's the temperature?",
6969
schema: TaskOutput,
70-
agent: { llm: "gpt-4.1" },
70+
llm: "gpt-4.1",
7171
});
7272

7373
poll: do {

examples/run.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ env();
1010
// gets API Key from environment variable BROWSER_USE_API_KEY
1111
const browseruse = new BrowserUseClient({
1212
apiKey: process.env.BROWSER_USE_API_KEY!,
13-
environment: "https://api.browser-use.com/api/v2",
1413
});
1514

1615
// Basic ---------------------------------------------------------------------
@@ -48,7 +47,7 @@ async function structured() {
4847
const rsp = await browseruse.tasks.createTask({
4948
task: "Search for the top 10 Hacker News posts and return the title and url!",
5049
schema: TaskOutput,
51-
agent: { llm: "gpt-4.1" },
50+
llm: "gpt-4.1",
5251
});
5352

5453
const result = await rsp.complete();

examples/stream.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ env();
1010

1111
const browseruse = new BrowserUseClient({
1212
apiKey: process.env.BROWSER_USE_API_KEY!,
13-
environment: "https://api.browser-use.com/api/v2",
1413
});
1514

1615
// Basic ---------------------------------------------------------------------
@@ -19,16 +18,39 @@ async function basic() {
1918
console.log("Basic: Creating task and starting stream...");
2019

2120
const task = await browseruse.tasks.createTask({
22-
task: "What's the weather in SF and what's the temperature?",
23-
agent: { llm: "gemini-2.5-flash" },
21+
task: "What's the weather and temperature in SF, NY, and LA?",
22+
llm: "gemini-2.5-flash",
2423
});
2524

26-
for await (const msg of task.stream()) {
27-
console.log(msg);
25+
console.log(`task.id: ${task.id}`);
26+
27+
const counter = { current: 0 };
28+
29+
for await (const step of task.stream()) {
30+
console.log(`STREAM 1: ${step.number}`);
31+
32+
counter.current++;
33+
34+
if (counter.current === 2) {
35+
break;
36+
}
37+
}
38+
39+
for await (const step of task.stream()) {
40+
counter.current++;
41+
42+
console.log(`STREAM 2: ${step.number}`);
2843
}
2944

3045
const result = await task.complete();
3146

47+
if (counter.current <= result.steps.length || counter.current !== result.steps.length + 2) {
48+
console.log(`counter.current: ${counter.current}, result.steps.length: ${result.steps.length}`);
49+
throw new Error(
50+
"Basic: Stream does not run as expected! Each step should be relogged whenever stream restarts!",
51+
);
52+
}
53+
3254
console.log("Basic: Stream completed");
3355
console.log(result.output);
3456
}
@@ -51,8 +73,8 @@ async function structured() {
5173

5274
const task = await browseruse.tasks.createTask({
5375
task: "Extract top 10 Hacker News posts and return the title, url, and score",
76+
llm: "gpt-4.1",
5477
schema: TaskOutput,
55-
agent: { llm: "gpt-4.1" },
5678
});
5779

5880
for await (const msg of task.stream()) {

examples/watch.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ env();
1010
// gets API Key from environment variable BROWSER_USE_API_KEY
1111
const browseruse = new BrowserUseClient({
1212
apiKey: process.env.BROWSER_USE_API_KEY!,
13-
environment: "https://api.browser-use.com/api/v2",
1413
});
1514

1615
// Basic ---------------------------------------------------------------------
@@ -20,7 +19,7 @@ async function basic() {
2019

2120
const task = await browseruse.tasks.createTask({
2221
task: "What's the weather in SF and what's the temperature?",
23-
agent: { llm: "gemini-2.5-flash" },
22+
llm: "gemini-2.5-flash",
2423
});
2524

2625
for await (const msg of task.watch()) {
@@ -53,7 +52,7 @@ async function structured() {
5352
const task = await browseruse.tasks.createTask({
5453
task: "Extract top 10 Hacker News posts and return the title, url, and score",
5554
schema: TaskOutput,
56-
agent: { llm: "gpt-4.1" },
55+
llm: "gpt-4.1",
5756
});
5857

5958
for await (const msg of task.watch()) {

jest.config.mjs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export default {
1111
"^(\.{1,2}/.*)\.js$": "$1",
1212
},
1313
roots: ["<rootDir>/tests"],
14-
testPathIgnorePatterns: ["\.browser\.(spec|test)\.[jt]sx?$", "/tests/wire/"],
14+
testPathIgnorePatterns: ["\.browser\.(spec|test)\.[jt]sx?$", "/tests/wire/", "/tests/wrapper"],
1515
setupFilesAfterEnv: [],
1616
},
1717
{
@@ -25,7 +25,6 @@ export default {
2525
testMatch: ["<rootDir>/tests/unit/**/?(*.)+(browser).(spec|test).[jt]s?(x)"],
2626
setupFilesAfterEnv: [],
2727
},
28-
,
2928
{
3029
displayName: "wire",
3130
preset: "ts-jest",
@@ -36,6 +35,13 @@ export default {
3635
roots: ["<rootDir>/tests/wire"],
3736
setupFilesAfterEnv: ["<rootDir>/tests/mock-server/setup.ts"],
3837
},
38+
{
39+
displayName: "wrapper",
40+
preset: "ts-jest",
41+
testEnvironment: "node",
42+
moduleNameMapper: {},
43+
roots: ["<rootDir>/tests/wrapper"],
44+
},
3945
],
4046
workerThreads: false,
4147
passWithNoTests: true,

src/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
export * as BrowserUse from "./api/index.js";
2-
export { BrowserUseError, BrowserUseTimeoutError } from "./errors/index.js";
3-
export { BrowserUseClient } from "./Client.js";
42
export { BrowserUseEnvironment } from "./environments.js";
3+
export { BrowserUseError, BrowserUseTimeoutError } from "./errors/index.js";
4+
export { BrowserUseClient } from "./wrapper/BrowserUseClient.js";
5+
export type { WrappedTaskFnsWithoutSchema, WrappedTaskFnsWithSchema } from "./wrapper/lib/parse.js";
6+
export { createWebhookSignature, verifyWebhookEventSignature } from "./wrapper/lib/webhooks.js";
7+
export type { Webhook, WebhookAgentTaskStatusUpdatePayload, WebhookTestPayload } from "./wrapper/lib/webhooks.js";

src/wrapper/lib/parse.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,25 @@ export function wrapCreateTaskResponse(
169169
} while (true);
170170
}
171171

172+
/**
173+
* Streams the steps of the task and closes when the task is finished.
174+
*
175+
* @description Logs each step of the task exactly once. If you start the stream again, it will log the steps again.
176+
*/
177+
async function* stream(
178+
config?: { interval?: number },
179+
options?: RequestOptions,
180+
): AsyncGenerator<BrowserUse.TaskStepView> {
181+
const steps: { total: number } = { total: 0 };
182+
183+
for await (const msg of _watch(response.id, config, options)) {
184+
for (let i = steps.total; i < msg.data.steps.length; i++) {
185+
yield msg.data.steps[i] satisfies BrowserUse.TaskStepView;
186+
}
187+
steps.total = msg.data.steps.length;
188+
}
189+
}
190+
172191
function watch<T extends ZodType>(
173192
schema: T,
174193
config?: PollConfig,
@@ -252,24 +271,6 @@ export function wrapCreateTaskResponse(
252271
throw new Error("Task did not finish");
253272
}
254273

255-
async function* stream(
256-
config?: { interval?: number },
257-
options?: RequestOptions,
258-
): AsyncGenerator<BrowserUse.TaskStepView> {
259-
const step: { current: number } = { current: 0 };
260-
261-
const interval = config?.interval ?? 2000;
262-
263-
for await (const msg of _watch(response.id, { interval }, options)) {
264-
if (msg.data.steps.length > step.current) {
265-
step.current = msg.data.steps.length;
266-
267-
const lastStepIdx = msg.data.steps.length - 1;
268-
yield msg.data.steps[lastStepIdx] satisfies BrowserUse.TaskStepView;
269-
}
270-
}
271-
}
272-
273274
// NOTE: Finally, we return the wrapped task response.
274275

275276
if (schema == null) {

0 commit comments

Comments
 (0)