Skip to content

Commit 7850b58

Browse files
committed
Clarify deterministic rerun docs
1 parent ed01472 commit 7850b58

6 files changed

Lines changed: 284 additions & 66 deletions

File tree

browser-use-python/src/browser_use_sdk/v3/client.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import os
4-
from collections.abc import Awaitable
54
from typing import Any, TypeVar, overload
65
from uuid import UUID
76

@@ -63,6 +62,7 @@ def run(
6362
workspace_id: str | None = ...,
6463
enable_recording: bool | None = ...,
6564
cache_script: bool | None = ...,
65+
auto_heal: bool | None = ...,
6666
**extra: Any,
6767
) -> SessionResult[T]: ...
6868

@@ -81,6 +81,7 @@ def run(
8181
workspace_id: str | None = ...,
8282
enable_recording: bool | None = ...,
8383
cache_script: bool | None = ...,
84+
auto_heal: bool | None = ...,
8485
**extra: Any,
8586
) -> SessionResult[T]: ...
8687

@@ -98,6 +99,7 @@ def run(
9899
workspace_id: str | None = ...,
99100
enable_recording: bool | None = ...,
100101
cache_script: bool | None = ...,
102+
auto_heal: bool | None = ...,
101103
**extra: Any,
102104
) -> SessionResult[str]: ...
103105

@@ -116,6 +118,7 @@ def run(
116118
workspace_id: str | None = None,
117119
enable_recording: bool | None = None,
118120
cache_script: bool | None = None,
121+
auto_heal: bool | None = None,
119122
**extra: Any,
120123
) -> Any:
121124
"""Run a task and block until complete. Returns a SessionResult.
@@ -127,7 +130,9 @@ def run(
127130
- False: force-disable caching.
128131
129132
When active, the first call runs the full agent and saves a reusable script.
130-
Subsequent calls with the same task template execute the script with $0 LLM cost.
133+
Subsequent calls with the same task template execute the script. By default,
134+
auto_heal may use lightweight validation or regenerate the script if output
135+
looks wrong; set auto_heal=False to return the raw script output.
131136
"""
132137
if cache_script is True and not workspace_id:
133138
raise ValueError("workspace_id is required when cache_script=True")
@@ -158,6 +163,7 @@ def run(
158163
workspace_id=workspace_id,
159164
enable_recording=enable_recording,
160165
cache_script=cache_script,
166+
auto_heal=auto_heal,
161167
**extra,
162168
)
163169
return _poll_output(self.sessions, str(data.id), resolved_schema)
@@ -177,6 +183,7 @@ def stream(
177183
workspace_id: str | None = None,
178184
enable_recording: bool | None = None,
179185
cache_script: bool | None = None,
186+
auto_heal: bool | None = None,
180187
**extra: Any,
181188
) -> SessionStream[Any]:
182189
"""Run a task and yield messages as they happen.
@@ -224,9 +231,12 @@ def stream(
224231
workspace_id=workspace_id,
225232
enable_recording=enable_recording,
226233
cache_script=cache_script,
234+
auto_heal=auto_heal,
227235
**extra,
228236
)
229-
return SessionStream(data, self.sessions, resolved_schema, _start_cursor=start_cursor)
237+
return SessionStream(
238+
data, self.sessions, resolved_schema, _start_cursor=start_cursor
239+
)
230240

231241
def close(self) -> None:
232242
"""Close the underlying HTTP client."""
@@ -280,6 +290,7 @@ def run(
280290
workspace_id: str | None = ...,
281291
enable_recording: bool | None = ...,
282292
cache_script: bool | None = ...,
293+
auto_heal: bool | None = ...,
283294
**extra: Any,
284295
) -> AsyncSessionRun[T]: ...
285296

@@ -298,6 +309,7 @@ def run(
298309
workspace_id: str | None = ...,
299310
enable_recording: bool | None = ...,
300311
cache_script: bool | None = ...,
312+
auto_heal: bool | None = ...,
301313
**extra: Any,
302314
) -> AsyncSessionRun[T]: ...
303315

@@ -315,6 +327,7 @@ def run(
315327
workspace_id: str | None = ...,
316328
enable_recording: bool | None = ...,
317329
cache_script: bool | None = ...,
330+
auto_heal: bool | None = ...,
318331
**extra: Any,
319332
) -> AsyncSessionRun[str]: ...
320333

@@ -333,6 +346,7 @@ def run(
333346
workspace_id: str | None = None,
334347
enable_recording: bool | None = None,
335348
cache_script: bool | None = None,
349+
auto_heal: bool | None = None,
336350
**extra: Any,
337351
) -> AsyncSessionRun[Any]:
338352
"""Run a task. Await the result for a SessionResult.
@@ -344,7 +358,9 @@ def run(
344358
- False: force-disable caching.
345359
346360
When active, the first call runs the full agent and saves a reusable script.
347-
Subsequent calls with the same task template execute the script with $0 LLM cost.
361+
Subsequent calls with the same task template execute the script. By default,
362+
auto_heal may use lightweight validation or regenerate the script if output
363+
looks wrong; set auto_heal=False to return the raw script output.
348364
"""
349365
if cache_script is True and not workspace_id:
350366
raise ValueError("workspace_id is required when cache_script=True")
@@ -386,10 +402,16 @@ async def create_fn() -> SessionResponse:
386402
workspace_id=workspace_id,
387403
enable_recording=enable_recording,
388404
cache_script=cache_script,
405+
auto_heal=auto_heal,
389406
**extra,
390407
)
391408

392-
return AsyncSessionRun(create_fn, self.sessions, resolved_schema, _start_cursor_ref=lambda: start_cursor)
409+
return AsyncSessionRun(
410+
create_fn,
411+
self.sessions,
412+
resolved_schema,
413+
_start_cursor_ref=lambda: start_cursor,
414+
)
393415

394416
async def close(self) -> None:
395417
"""Close the underlying HTTP client."""

browser-use-python/src/browser_use_sdk/v3/resources/sessions.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def create(
3535
enable_scheduled_tasks: bool | None = None,
3636
enable_recording: bool | None = None,
3737
cache_script: bool | None = None,
38+
auto_heal: bool | None = None,
3839
**extra: Any,
3940
) -> SessionResponse:
4041
"""Create a session and optionally dispatch a task."""
@@ -52,7 +53,11 @@ def create(
5253
if profile_id is not None:
5354
body["profileId"] = profile_id
5455
if proxy_country_code is not _UNSET:
55-
body["proxyCountryCode"] = proxy_country_code.lower() if isinstance(proxy_country_code, str) else proxy_country_code
56+
body["proxyCountryCode"] = (
57+
proxy_country_code.lower()
58+
if isinstance(proxy_country_code, str)
59+
else proxy_country_code
60+
)
5661
if output_schema is not None:
5762
body["outputSchema"] = output_schema
5863
if workspace_id is not None:
@@ -63,6 +68,8 @@ def create(
6368
body["enableRecording"] = enable_recording
6469
if cache_script is not None:
6570
body["cacheScript"] = cache_script
71+
if auto_heal is not None:
72+
body["autoHeal"] = auto_heal
6673
body.update(extra)
6774
return SessionResponse.model_validate(
6875
self._http.request("POST", "/sessions", json=body)
@@ -92,7 +99,9 @@ def get(self, session_id: str | UUID) -> SessionResponse:
9299
self._http.request("GET", f"/sessions/{session_id}")
93100
)
94101

95-
def stop(self, session_id: str | UUID, *, strategy: str | None = None, **extra: Any) -> SessionResponse:
102+
def stop(
103+
self, session_id: str | UUID, *, strategy: str | None = None, **extra: Any
104+
) -> SessionResponse:
96105
"""Stop a session or the running task."""
97106
body: dict[str, Any] | None = None
98107
if strategy is not None or extra:
@@ -172,6 +181,7 @@ async def create(
172181
enable_scheduled_tasks: bool | None = None,
173182
enable_recording: bool | None = None,
174183
cache_script: bool | None = None,
184+
auto_heal: bool | None = None,
175185
**extra: Any,
176186
) -> SessionResponse:
177187
"""Create a session and optionally dispatch a task."""
@@ -189,7 +199,11 @@ async def create(
189199
if profile_id is not None:
190200
body["profileId"] = profile_id
191201
if proxy_country_code is not _UNSET:
192-
body["proxyCountryCode"] = proxy_country_code.lower() if isinstance(proxy_country_code, str) else proxy_country_code
202+
body["proxyCountryCode"] = (
203+
proxy_country_code.lower()
204+
if isinstance(proxy_country_code, str)
205+
else proxy_country_code
206+
)
193207
if output_schema is not None:
194208
body["outputSchema"] = output_schema
195209
if workspace_id is not None:
@@ -200,6 +214,8 @@ async def create(
200214
body["enableRecording"] = enable_recording
201215
if cache_script is not None:
202216
body["cacheScript"] = cache_script
217+
if auto_heal is not None:
218+
body["autoHeal"] = auto_heal
203219
body.update(extra)
204220
return SessionResponse.model_validate(
205221
await self._http.request("POST", "/sessions", json=body)
@@ -229,7 +245,9 @@ async def get(self, session_id: str | UUID) -> SessionResponse:
229245
await self._http.request("GET", f"/sessions/{session_id}")
230246
)
231247

232-
async def stop(self, session_id: str | UUID, *, strategy: str | None = None, **extra: Any) -> SessionResponse:
248+
async def stop(
249+
self, session_id: str | UUID, *, strategy: str | None = None, **extra: Any
250+
) -> SessionResponse:
233251
"""Stop a session or the running task."""
234252
body: dict[str, Any] | None = None
235253
if strategy is not None or extra:

browser-use-python/tests/test_vibe.py

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import inspect
1010
import json
11+
import os
1112
import typing
1213
from pathlib import Path
1314
from typing import Any, Dict, List, Set, Tuple
@@ -17,10 +18,15 @@
1718
# ---------------------------------------------------------------------------
1819
# Locate spec files via CLOUD_REPO_PATH in .env
1920
# ---------------------------------------------------------------------------
20-
_SDK_REPO = Path(__file__).resolve().parents[2] # browser-use-python/tests -> sdk repo root
21+
_SDK_REPO = (
22+
Path(__file__).resolve().parents[2]
23+
) # browser-use-python/tests -> sdk repo root
2124

2225

2326
def _get_cloud_repo_path() -> Path:
27+
if env_path := os.environ.get("CLOUD_REPO_PATH"):
28+
return Path(env_path)
29+
2430
env_file = _SDK_REPO / ".env"
2531
for line in env_file.read_text().splitlines():
2632
line = line.strip()
@@ -89,7 +95,10 @@ def _load_spec(path: Path) -> Dict[str, Any]:
8995
("post", "/skills/{skill_id}/refine"): ("skills", "refine"),
9096
("post", "/skills/{skill_id}/rollback"): ("skills", "rollback"),
9197
("get", "/skills/{skill_id}/executions"): ("skills", "executions"),
92-
("get", "/skills/{skill_id}/executions/{execution_id}/output"): ("skills", "execution_output"),
98+
("get", "/skills/{skill_id}/executions/{execution_id}/output"): (
99+
"skills",
100+
"execution_output",
101+
),
93102
# marketplace
94103
("get", "/marketplace/skills"): ("marketplace", "list"),
95104
("get", "/marketplace/skills/{skill_slug}"): ("marketplace", "get"),
@@ -158,10 +167,6 @@ def test_all_spec_endpoints_mapped(self) -> None:
158167
assert not missing, f"Unmapped v2 endpoints: {missing}"
159168

160169
def test_sdk_methods_exist(self) -> None:
161-
from browser_use_sdk.v2.client import BrowserUse
162-
163-
client = BrowserUse.__new__(BrowserUse)
164-
# Manually set up resource stubs so we can inspect
165170
from browser_use_sdk.v2 import resources
166171

167172
for resource_attr, method_name in _V2_MAP.values():
@@ -181,9 +186,7 @@ def test_sdk_methods_exist(self) -> None:
181186
f"{cls.__name__} missing method '{method_name}'"
182187
)
183188
method = getattr(cls, method_name)
184-
assert callable(method), (
185-
f"{cls.__name__}.{method_name} is not callable"
186-
)
189+
assert callable(method), f"{cls.__name__}.{method_name} is not callable"
187190

188191
def test_async_sdk_methods_exist(self) -> None:
189192
from browser_use_sdk.v2 import resources
@@ -221,7 +224,13 @@ def test_all_spec_endpoints_mapped(self) -> None:
221224
assert not missing, f"Unmapped v3 endpoints: {missing}"
222225

223226
def test_sdk_methods_exist(self) -> None:
224-
from browser_use_sdk.v3.resources import billing, browsers, profiles, sessions, workspaces
227+
from browser_use_sdk.v3.resources import (
228+
billing,
229+
browsers,
230+
profiles,
231+
sessions,
232+
workspaces,
233+
)
225234

226235
resource_classes = {
227236
"billing": billing.Billing,
@@ -237,7 +246,13 @@ def test_sdk_methods_exist(self) -> None:
237246
)
238247

239248
def test_async_sdk_methods_exist(self) -> None:
240-
from browser_use_sdk.v3.resources import billing, browsers, profiles, sessions, workspaces
249+
from browser_use_sdk.v3.resources import (
250+
billing,
251+
browsers,
252+
profiles,
253+
sessions,
254+
workspaces,
255+
)
241256

242257
async_classes = {
243258
"billing": billing.AsyncBilling,
@@ -302,11 +317,7 @@ def _load(self) -> None:
302317

303318
def _get_query_params(self, method: str, path: str) -> Set[str]:
304319
op = self.spec["paths"].get(path, {}).get(method, {})
305-
return {
306-
p["name"]
307-
for p in op.get("parameters", [])
308-
if p.get("in") == "query"
309-
}
320+
return {p["name"] for p in op.get("parameters", []) if p.get("in") == "query"}
310321

311322
def _resolve_ref(self, ref: str) -> Dict[str, Any]:
312323
parts = ref.lstrip("#/").split("/")
@@ -408,10 +419,15 @@ def test_task_action_variants(self) -> None:
408419
if not method_name:
409420
missing.append(f"No SDK method mapping for action '{action}'")
410421
continue
411-
for label, classes_fn in [("sync", _get_resource_classes), ("async", _get_async_resource_classes)]:
422+
for label, classes_fn in [
423+
("sync", _get_resource_classes),
424+
("async", _get_async_resource_classes),
425+
]:
412426
cls = classes_fn()["tasks"]
413427
if not hasattr(cls, method_name):
414-
missing.append(f"{cls.__name__} missing '{method_name}' for action '{action}'")
428+
missing.append(
429+
f"{cls.__name__} missing '{method_name}' for action '{action}'"
430+
)
415431

416432
assert not missing, "Missing action variants:\n" + "\n".join(missing)
417433

@@ -435,8 +451,14 @@ def test_v2_resources_attached(self) -> None:
435451

436452
client = BrowserUse(api_key="test-key")
437453
expected = [
438-
"billing", "tasks", "sessions", "files",
439-
"profiles", "browsers", "skills", "marketplace",
454+
"billing",
455+
"tasks",
456+
"sessions",
457+
"files",
458+
"profiles",
459+
"browsers",
460+
"skills",
461+
"marketplace",
440462
]
441463
for attr in expected:
442464
assert hasattr(client, attr), f"BrowserUse missing .{attr}"
@@ -449,3 +471,35 @@ def test_v3_resources_attached(self) -> None:
449471
assert hasattr(client, "sessions")
450472
assert hasattr(client, "workspaces")
451473
client.close()
474+
475+
476+
class TestV3SessionPayloads:
477+
def test_sessions_create_serializes_auto_heal(self) -> None:
478+
from browser_use_sdk.v3.resources.sessions import Sessions
479+
480+
class FakeHttp:
481+
def __init__(self) -> None:
482+
self.calls: list[dict[str, Any]] = []
483+
484+
def request(self, method: str, path: str, *, json=None, params=None):
485+
self.calls.append(
486+
{"method": method, "path": path, "json": json, "params": params}
487+
)
488+
return {
489+
"id": "00000000-0000-0000-0000-000000000001",
490+
"status": "created",
491+
"model": "gemini-3-flash",
492+
"createdAt": "2026-05-26T00:00:00Z",
493+
"updatedAt": "2026-05-26T00:00:00Z",
494+
}
495+
496+
http = FakeHttp()
497+
Sessions(http).create(
498+
"Fetch https://httpbin.org/anything?item=@{{alpha}}",
499+
workspace_id="00000000-0000-0000-0000-000000000002",
500+
cache_script=True,
501+
auto_heal=False,
502+
)
503+
504+
assert http.calls[0]["json"]["cacheScript"] is True
505+
assert http.calls[0]["json"]["autoHeal"] is False

0 commit comments

Comments
 (0)