Skip to content
This repository was archived by the owner on Nov 29, 2025. It is now read-only.

Commit 074e3e7

Browse files
authored
Merge pull request #89 from niranjan94/fix/browser-timeouts
feat: add configurable default timeout for browser tools
2 parents 3be07a6 + 9e43b11 commit 074e3e7

File tree

1 file changed

+47
-27
lines changed

1 file changed

+47
-27
lines changed

src/modules/tools/browser.py

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ class BrowserService(EventEmitter):
6565

6666
stagehand_config: StagehandConfig
6767
stagehand: Stagehand
68+
default_timeout: float
6869
artifacts_dir: str
6970
provider: str
7071
model: str
@@ -109,6 +110,7 @@ def __init__(
109110
"No artifacts_dir provided. Browser will not persist network traffic, profile, downloads to artifacts."
110111
)
111112

113+
self.default_timeout = float(os.getenv("BROWSER_DEFAULT_TIMEOUT", "120000"))
112114
self.stagehand_config = StagehandConfig(
113115
env="LOCAL",
114116
modelName=model,
@@ -120,6 +122,11 @@ def __init__(
120122
)
121123
self.stagehand = Stagehand(self.stagehand_config)
122124

125+
@asynccontextmanager
126+
async def timeout(self):
127+
async with asyncio.timeout((self.default_timeout / 1000) + 5):
128+
yield
129+
123130
@property
124131
def page_domain(self):
125132
"""
@@ -167,9 +174,16 @@ async def ensure_init(self):
167174
if self._initialized:
168175
return
169176

177+
logger.info("Initializing browser")
170178
await self.stagehand.init()
171179
self._initialized = True
172180

181+
self.context.set_default_timeout(self.default_timeout)
182+
self.context.set_default_navigation_timeout(self.default_timeout)
183+
184+
self.page.set_default_timeout(self.default_timeout)
185+
self.page.set_default_navigation_timeout(self.default_timeout)
186+
173187
async def handle_dialog(dialog: Dialog):
174188
"""Auto accept all dialogs"""
175189
await dialog.accept()
@@ -582,11 +596,9 @@ async def get_browser():
582596
"""
583597
if not _BROWSER:
584598
raise ValueError("Browser not initialized. Please call initialize_browser first.")
585-
logger.info("[BROWSER] ENtering get_browser context manager.")
599+
586600
async with _BROWSER_LOCK:
587-
logger.info("[BROWSER] ENtering get_browser context manager lock.")
588601
await _BROWSER.ensure_init()
589-
logger.info("[BROWSER] Done get_browser context manager ensure_init.")
590602
yield _BROWSER
591603

592604

@@ -686,20 +698,22 @@ async def browser_goto_url(url: str):
686698
async with browser.interaction_context_capture(
687699
only_domains=[browser.page_domain, extract_domain(url)]
688700
) as interaction_context:
689-
await browser.page.goto(url)
701+
async with browser.timeout():
702+
await browser.page.goto(url)
690703
interaction_context = interaction_context
691704

692705
# Eagerly returning relevant observations to reduce agent tool calls
693-
observations = "\n".join(
694-
map(
695-
lambda obs: obs.description,
696-
await browser.page.observe(
697-
f"{url} was just opened. "
698-
"give all important elements on the page that might be relevant to the next action. "
699-
"observe the overall state of the page to understand the purpose of the page."
700-
),
706+
async with browser.timeout():
707+
observations = "\n".join(
708+
map(
709+
lambda obs: obs.description,
710+
await browser.page.observe(
711+
f"{url} was just opened. "
712+
"give all important elements on the page that might be relevant to the next action. "
713+
"observe the overall state of the page to understand the purpose of the page."
714+
),
715+
)
701716
)
702-
)
703717
return f"<observations>\n{observations}\n</observations>\n{interaction_context}"
704718

705719

@@ -714,7 +728,8 @@ async def browser_get_page_html() -> str:
714728
The path of the downloaded HTML file artifact.
715729
"""
716730
async with get_browser() as browser:
717-
page_html = await browser.page.content()
731+
async with browser.timeout():
732+
page_html = await browser.page.content()
718733
html_artifact_file = os.path.join(browser.artifacts_dir, f"browser_page_{time.time_ns()}.html")
719734
with open(html_artifact_file, "w") as f:
720735
f.write(page_html)
@@ -740,7 +755,8 @@ async def browser_evaluate_js(expression: str):
740755
The result of the javascript expression.
741756
"""
742757
async with get_browser() as browser:
743-
return await browser.page.evaluate(expression)
758+
async with browser.timeout():
759+
return await browser.page.evaluate(expression)
744760

745761

746762
@tool
@@ -757,7 +773,8 @@ async def browser_get_cookies():
757773
a message string indicating this is returned.
758774
"""
759775
async with get_browser() as browser:
760-
cookies = await browser.context.cookies()
776+
async with browser.timeout():
777+
cookies = await browser.context.cookies()
761778
if len(cookies) == 0:
762779
return "No cookies found"
763780

@@ -798,22 +815,24 @@ async def browser_perform_action(action: str):
798815
"""
799816
async with get_browser() as browser:
800817
async with browser.interaction_context_capture(only_domains=[browser.page_domain]) as interaction_context:
801-
await browser.page.act(action)
818+
async with browser.timeout():
819+
await browser.page.act(action)
802820
with contextlib.suppress(TimeoutError):
803821
await browser.page.wait_for_load_state("networkidle", timeout=60000)
804822
interaction_context = interaction_context
805823

806824
# Eagerly returning relevant observations to reduce agent tool calls
807-
observations = "\n".join(
808-
map(
809-
lambda obs: obs.description,
810-
await browser.page.observe(
811-
f"`{action}` action was just performed. "
812-
"give all important elements on the page that might be relevant to the next action."
813-
"observe the overall state of the page to understand the purpose of the page."
814-
),
825+
async with browser.timeout():
826+
observations = "\n".join(
827+
map(
828+
lambda obs: obs.description,
829+
await browser.page.observe(
830+
f"`{action}` action was just performed. "
831+
"give all important elements on the page that might be relevant to the next action."
832+
"observe the overall state of the page to understand the purpose of the page."
833+
),
834+
)
815835
)
816-
)
817836
return f"<observations>\n{observations}\n</observations>\n{interaction_context}"
818837

819838

@@ -840,5 +859,6 @@ async def browser_observe_page(instruction: Optional[str] = None) -> list[str]:
840859
on the browser page.
841860
"""
842861
async with get_browser() as browser:
843-
observations = await browser.page.observe(instruction)
862+
async with browser.timeout():
863+
observations = await browser.page.observe(instruction)
844864
return [observation.description for observation in observations]

0 commit comments

Comments
 (0)