gl-browser-use-binary 0.0.0b5__py3-none-any.whl

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,639 @@
1
+ """Client implementation for GL Browser Use SDK.
2
+
3
+ Authors:
4
+ Reinhart Linanda (reinhart.linanda@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import contextlib
11
+ import dataclasses
12
+ import inspect
13
+ import logging
14
+ import os
15
+ from collections.abc import AsyncGenerator, Awaitable, Callable
16
+ from typing import Any
17
+
18
+ from browser_use import Agent
19
+ from browser_use.browser import BrowserSession
20
+ from browser_use.llm import ChatOpenAI
21
+ from browser_use.logging_config import setup_logging
22
+ from bubus import EventBus
23
+ from openai.types.shared_params.chat_model import ChatModel
24
+ from openai.types.shared_params.reasoning_effort import ReasoningEffort
25
+
26
+ from gl_browser_use.config import BrowserUseClientConfig
27
+ from gl_browser_use.errors import (
28
+ BrowserUseConfigurationError,
29
+ BrowserUseExecutionError,
30
+ BrowserUseRetryExhaustedError,
31
+ )
32
+ from gl_browser_use.events import (
33
+ RECORDING_MESSAGE,
34
+ STREAMING_MESSAGE,
35
+ TASK_COMPLETED_MESSAGE,
36
+ build_activity_event,
37
+ build_error_event,
38
+ build_retry_event,
39
+ build_step_content,
40
+ build_step_event,
41
+ build_thinking_event,
42
+ )
43
+ from gl_browser_use.infrastructure.base import (
44
+ BrowserInfrastructure,
45
+ BrowserInfrastructureContext,
46
+ )
47
+ from gl_browser_use.models import (
48
+ BrowserUseRunResult,
49
+ BrowserUseRunStatus,
50
+ BrowserUseStreamEvent,
51
+ )
52
+ from gl_browser_use.parsing import (
53
+ coerce_string,
54
+ extract_final_output,
55
+ extract_tool_calls,
56
+ summarize_terminal_error,
57
+ validate_structured_data_output,
58
+ )
59
+ from gl_browser_use.session_errors import is_recoverable_session_error
60
+ from gl_browser_use.storage.base import ObjectStorage
61
+
62
+ logger = logging.getLogger(__name__)
63
+
64
+ _AGENT_EVENTBUS_STOP_TIMEOUT_IN_S = 5.0
65
+
66
+ EmitCallable = Callable[[BrowserUseStreamEvent | None], Awaitable[None] | None]
67
+
68
+
69
+ async def _maybe_await(value: Any) -> None:
70
+ if inspect.isawaitable(value):
71
+ await value
72
+
73
+
74
+ def recording_metadata(
75
+ *,
76
+ infrastructure: BrowserInfrastructure | None,
77
+ storage: ObjectStorage | None,
78
+ browser_context: BrowserInfrastructureContext | None,
79
+ ) -> dict[str, Any]:
80
+ """Return recording status metadata aligned with implementation behavior."""
81
+ if infrastructure is None:
82
+ return {
83
+ "recording_status": "disabled",
84
+ "recording_message": "Browser infrastructure is not configured.",
85
+ }
86
+
87
+ if not infrastructure.supports_recording:
88
+ return {
89
+ "recording_status": "unsupported",
90
+ "recording_message": f"Infrastructure '{infrastructure.provider}' does not support recording.",
91
+ }
92
+
93
+ if storage is None:
94
+ return {
95
+ "recording_status": "disabled",
96
+ "recording_message": "Object storage is not configured.",
97
+ }
98
+
99
+ if not storage.is_available:
100
+ return {
101
+ "recording_status": "unavailable",
102
+ "recording_message": f"Object storage {storage.provider} is not available: {getattr(storage, 'last_error', None)}",
103
+ }
104
+
105
+ if browser_context is None:
106
+ return {
107
+ "recording_status": "disabled",
108
+ "recording_message": "Browser context is not available.",
109
+ }
110
+
111
+ return {
112
+ "recording_status": "scheduled",
113
+ "recording_message": "Recording has been scheduled as a background task.",
114
+ }
115
+
116
+
117
+ class BrowserUseClient:
118
+ """Facade for running browser automation tasks.
119
+
120
+ Note: ``infrastructure`` and ``storage`` attributes are read by ``run``/
121
+ ``run_once``/``run_sync`` on each call. Swapping them while a run is in
122
+ flight is undefined behavior.
123
+ """
124
+
125
+ def __init__(
126
+ self,
127
+ *,
128
+ config: BrowserUseClientConfig,
129
+ infrastructure: BrowserInfrastructure | None = None,
130
+ storage: ObjectStorage | None = None,
131
+ ) -> None:
132
+ """Initialize the client."""
133
+ config.validate(strict=True)
134
+ self.config = config
135
+ self.infrastructure = infrastructure
136
+ self.storage = storage
137
+
138
+ async def run(self, task: str) -> AsyncGenerator[BrowserUseStreamEvent, None]:
139
+ """Run the client."""
140
+ queue: asyncio.Queue[BrowserUseStreamEvent | None] = asyncio.Queue()
141
+ emit_lock = asyncio.Lock()
142
+
143
+ async def emit(event: BrowserUseStreamEvent | None) -> None:
144
+ async with emit_lock:
145
+ await queue.put(event)
146
+
147
+ async def run_and_signal() -> BrowserUseRunResult:
148
+ try:
149
+ return await self._run_with_events(task, emit)
150
+ finally:
151
+ await emit(None)
152
+
153
+ run_task = asyncio.create_task(run_and_signal())
154
+ try:
155
+ while True:
156
+ event = await queue.get()
157
+ if event is None:
158
+ break
159
+ yield event
160
+ await run_task
161
+ finally:
162
+ if not run_task.done():
163
+ run_task.cancel()
164
+ with contextlib.suppress(asyncio.CancelledError):
165
+ await run_task
166
+
167
+ async def run_once(self, task: str) -> BrowserUseRunResult:
168
+ """Run the client once and return a single aggregated result.
169
+
170
+ Raises:
171
+ ------
172
+ BrowserUseExecutionError
173
+ When the task input is invalid.
174
+ BrowserUseConfigurationError
175
+ When required configuration is missing.
176
+ BrowserUseRetryExhaustedError
177
+ When all recoverable session retries are exhausted.
178
+ """
179
+ events: list[BrowserUseStreamEvent] = []
180
+
181
+ async def emit(event: BrowserUseStreamEvent | None) -> None:
182
+ if event is not None:
183
+ events.append(event)
184
+
185
+ result = await self._run_with_events(task, emit)
186
+ return dataclasses.replace(result, events=events)
187
+
188
+ def run_sync(self, task: str) -> BrowserUseRunResult:
189
+ """Run the client synchronously.
190
+
191
+ Raises:
192
+ ------
193
+ BrowserUseExecutionError
194
+ When invoked from a thread that already has a running event loop,
195
+ or when task validation fails.
196
+ BrowserUseConfigurationError
197
+ When required configuration is missing.
198
+ BrowserUseRetryExhaustedError
199
+ When all recoverable session retries are exhausted.
200
+ """
201
+ try:
202
+ asyncio.get_running_loop()
203
+ except RuntimeError:
204
+ pass
205
+ else:
206
+ raise BrowserUseExecutionError(
207
+ "run_sync must not be called from a running event loop"
208
+ )
209
+ return asyncio.run(self.run_once(task))
210
+
211
+ async def _run_with_events(
212
+ self, task: str, emit: EmitCallable
213
+ ) -> BrowserUseRunResult:
214
+ task = self._validate_task(task)
215
+ max_attempts = self.config.max_session_retries + 1
216
+
217
+ for attempt in range(1, max_attempts + 1):
218
+ try:
219
+ result = await self._run_single_attempt(task, emit, attempt=attempt)
220
+ await _maybe_await(emit(None))
221
+ return dataclasses.replace(
222
+ result,
223
+ metadata={**result.metadata, "max_attempts": max_attempts},
224
+ )
225
+ except BrowserUseExecutionError as exc:
226
+ raw_reason = str(exc)
227
+ display_reason = summarize_terminal_error(raw_reason)
228
+ recoverable = is_recoverable_session_error(raw_reason)
229
+
230
+ if recoverable and attempt < max_attempts:
231
+ await _maybe_await(
232
+ emit(build_retry_event(attempt, max_attempts, display_reason))
233
+ )
234
+ delay = max(0.0, self.config.session_retry_delay_in_s)
235
+ if delay:
236
+ await asyncio.sleep(delay)
237
+ continue
238
+
239
+ details = exc.details if isinstance(exc.details, dict) else {}
240
+ recording_url_from_details = coerce_string(details.get("recording_url"))
241
+ resolved_recording_url = (
242
+ recording_url_from_details
243
+ or self._resolve_recording_url_from_details(details)
244
+ )
245
+
246
+ await _maybe_await(
247
+ emit(
248
+ build_error_event(
249
+ display_reason, recording_url=resolved_recording_url or ""
250
+ )
251
+ )
252
+ )
253
+ await _maybe_await(emit(None))
254
+
255
+ if recoverable:
256
+ raise BrowserUseRetryExhaustedError(
257
+ f"Session retries exhausted after {max_attempts} attempts: {display_reason}",
258
+ attempt=attempt,
259
+ max_attempts=max_attempts,
260
+ details={
261
+ "streaming_url": details.get("streaming_url"),
262
+ "recording_url": resolved_recording_url,
263
+ },
264
+ origin=exc,
265
+ ) from exc
266
+
267
+ return self._build_terminal_result(
268
+ task=task,
269
+ attempt=attempt,
270
+ max_attempts=max_attempts,
271
+ reason=display_reason,
272
+ details=details,
273
+ resolved_recording_url=resolved_recording_url,
274
+ )
275
+ except Exception as exc:
276
+ display_reason = summarize_terminal_error(str(exc))
277
+ await _maybe_await(emit(build_error_event(display_reason)))
278
+ await _maybe_await(emit(None))
279
+ return self._build_terminal_result(
280
+ task=task,
281
+ attempt=attempt,
282
+ max_attempts=max_attempts,
283
+ reason=display_reason,
284
+ details={},
285
+ resolved_recording_url=None,
286
+ )
287
+
288
+ # Defensive: the loop always returns or raises; this guard keeps
289
+ # type-checkers happy about the return contract.
290
+ await _maybe_await(emit(None))
291
+ raise BrowserUseRetryExhaustedError(
292
+ "run attempt loop exhausted unexpectedly",
293
+ attempt=max_attempts,
294
+ max_attempts=max_attempts,
295
+ )
296
+
297
+ def _build_terminal_result(
298
+ self,
299
+ *,
300
+ task: str,
301
+ attempt: int,
302
+ max_attempts: int,
303
+ reason: str,
304
+ details: dict[str, Any],
305
+ resolved_recording_url: str | None,
306
+ ) -> BrowserUseRunResult:
307
+ """Build a terminal error :class:`BrowserUseRunResult` from attempt metadata.
308
+
309
+ Note: when the triggering exception was not a :class:`BrowserUseExecutionError`,
310
+ ``details`` is empty and browser context cannot be recovered. In that case,
311
+ emit ``recording_status="unknown"`` to avoid misleadingly reporting
312
+ ``"disabled"`` when a recording side effect may still have been started.
313
+ """
314
+ browser_context: BrowserInfrastructureContext | None = None
315
+ streaming_url: str | None = None
316
+
317
+ browser_context_candidate = details.get("browser_context")
318
+ if isinstance(browser_context_candidate, BrowserInfrastructureContext):
319
+ browser_context = browser_context_candidate
320
+
321
+ streaming_url_candidate = details.get("streaming_url")
322
+ if streaming_url_candidate is not None:
323
+ streaming_url = coerce_string(streaming_url_candidate)
324
+
325
+ metadata = recording_metadata(
326
+ infrastructure=self.infrastructure,
327
+ storage=self.storage,
328
+ browser_context=browser_context,
329
+ )
330
+
331
+ if (
332
+ self.infrastructure is not None
333
+ and self.storage is not None
334
+ and browser_context is None
335
+ and details == {}
336
+ and metadata.get("recording_status") == "disabled"
337
+ and metadata.get("recording_message") == "Browser context is not available."
338
+ ):
339
+ metadata.update(
340
+ {
341
+ "recording_status": "unknown",
342
+ "recording_message": "Recording status could not be determined from terminal error context.",
343
+ }
344
+ )
345
+
346
+ return BrowserUseRunResult(
347
+ status="error",
348
+ task=task,
349
+ final_output="",
350
+ session_id=browser_context.session_id
351
+ if browser_context is not None
352
+ else None,
353
+ streaming_url=streaming_url,
354
+ recording_url=resolved_recording_url,
355
+ steps=0,
356
+ error=reason,
357
+ events=[],
358
+ metadata={
359
+ "attempt": attempt,
360
+ "max_attempts": max_attempts,
361
+ **metadata,
362
+ },
363
+ )
364
+
365
+ def _resolve_recording_url_from_details(
366
+ self, details: dict[str, Any] | Any
367
+ ) -> str | None:
368
+ if not isinstance(details, dict):
369
+ return None
370
+ candidate = details.get("browser_context")
371
+ if not isinstance(candidate, BrowserInfrastructureContext):
372
+ return None
373
+ if not self._can_record(candidate):
374
+ return None
375
+ assert self.infrastructure is not None
376
+ url = self.infrastructure.resolve_recording_url(
377
+ session_id=candidate.session_id, storage=self.storage
378
+ )
379
+ return coerce_string(url) if url is not None else None
380
+
381
+ async def _run_single_attempt(
382
+ self, task: str, emit: EmitCallable, *, attempt: int
383
+ ) -> BrowserUseRunResult:
384
+ if not self.config.llm_openai_api_key:
385
+ raise BrowserUseConfigurationError(
386
+ "llm_openai_api_key is required for browser execution."
387
+ )
388
+ if not self.config.page_extraction_llm_openai_api_key:
389
+ raise BrowserUseConfigurationError(
390
+ "page_extraction_llm_openai_api_key is required for extraction."
391
+ )
392
+
393
+ if self.infrastructure is None:
394
+ logger.debug("Browser infrastructure is not configured.")
395
+ if self.storage is None:
396
+ logger.debug("Object storage is not configured.")
397
+
398
+ if self.storage is not None and not self.storage.is_available:
399
+ logger.debug(
400
+ "Object storage is not available: %s",
401
+ getattr(self.storage, "last_error", None),
402
+ )
403
+
404
+ # NOTE: browser-use reads these variables at module import / setup time.
405
+ # Writing them here is a process-global side effect; concurrent
406
+ # BrowserUseClient instances configured with different values will race.
407
+ os.environ["BROWSER_USE_CLOUD_SYNC"] = (
408
+ "true" if self.config.enable_cloud_sync else "false"
409
+ )
410
+ os.environ["BROWSER_USE_LOGGING_LEVEL"] = self.config.logging_level
411
+ with contextlib.suppress(Exception):
412
+ setup_logging(log_level=self.config.logging_level, force_setup=True)
413
+
414
+ browser_context = None
415
+ browser_session = None
416
+ agent = None
417
+ streaming_url = None
418
+ recording_url = None
419
+ step_count = 0
420
+ status: BrowserUseRunStatus = "error"
421
+
422
+ def _build_execution_error(
423
+ message: str, *, origin: BaseException | None = None
424
+ ) -> BrowserUseExecutionError:
425
+ details = {
426
+ "browser_context": browser_context,
427
+ "streaming_url": streaming_url,
428
+ "recording_url": recording_url,
429
+ }
430
+ return BrowserUseExecutionError(message, details=details, origin=origin)
431
+
432
+ try:
433
+ if self.infrastructure is not None:
434
+ browser_context = await self.infrastructure.create_session()
435
+ streaming_url = coerce_string(browser_context.streaming_url)
436
+ if streaming_url:
437
+ await _maybe_await(
438
+ emit(build_activity_event(streaming_url, STREAMING_MESSAGE))
439
+ )
440
+
441
+ cdp_url = self.infrastructure.cdp_url(
442
+ session_id=browser_context.session_id
443
+ )
444
+ browser_session = BrowserSession(cdp_url=cdp_url)
445
+ # TODO(upstream: browser_use): `_owns_browser_resources` is a private
446
+ # attribute of BrowserSession; if it is renamed in 0.12+ this flag
447
+ # will silently become a no-op.
448
+ with contextlib.suppress(Exception):
449
+ setattr(browser_session, "_owns_browser_resources", True)
450
+
451
+ llm = self._build_llm(
452
+ model=self.config.llm_openai_model,
453
+ reasoning_effort=self.config.llm_openai_reasoning_effort,
454
+ temperature=self.config.llm_openai_temperature,
455
+ api_key=self.config.llm_openai_api_key,
456
+ base_url=self.config.llm_openai_base_url,
457
+ )
458
+ page_extraction_llm = self._build_llm(
459
+ model=self.config.page_extraction_llm_openai_model,
460
+ reasoning_effort=self.config.page_extraction_llm_openai_reasoning_effort,
461
+ temperature=self.config.page_extraction_llm_openai_temperature,
462
+ api_key=self.config.page_extraction_llm_openai_api_key,
463
+ base_url=self.config.page_extraction_llm_openai_base_url,
464
+ )
465
+
466
+ agent = Agent(
467
+ task=task,
468
+ llm=llm,
469
+ page_extraction_llm=page_extraction_llm,
470
+ browser_session=browser_session,
471
+ extend_system_message=self.config.extend_system_message,
472
+ vision_detail_level=self.config.vision_detail_level,
473
+ llm_timeout=self.config.llm_timeout_in_s,
474
+ step_timeout=self.config.step_timeout_in_s,
475
+ )
476
+
477
+ if browser_session is not None:
478
+ with contextlib.suppress(Exception):
479
+ await browser_session.start()
480
+
481
+ while True:
482
+ try:
483
+ is_done, _ = await agent.take_step()
484
+ step_count += 1
485
+ except Exception as exc:
486
+ raise _build_execution_error(
487
+ f"Error in step execution: {exc}", origin=exc
488
+ ) from exc
489
+
490
+ tool_calls = extract_tool_calls(agent)
491
+ extraction_error = validate_structured_data_output(tool_calls)
492
+ if extraction_error:
493
+ raise _build_execution_error(extraction_error)
494
+
495
+ step_content = build_step_content(tool_calls, is_done=is_done)
496
+ step_event = build_step_event(
497
+ tool_calls, step_content, is_final=is_done
498
+ )
499
+
500
+ if is_done:
501
+ await _maybe_await(emit(step_event))
502
+ status = "success"
503
+ break
504
+
505
+ await _maybe_await(
506
+ emit(
507
+ build_thinking_event("thinking_start", content="Thinking start")
508
+ )
509
+ )
510
+ await _maybe_await(emit(step_event))
511
+ await _maybe_await(
512
+ emit(build_thinking_event("thinking_end", content="Thinking end"))
513
+ )
514
+
515
+ final_output = extract_final_output(agent)
516
+ if not final_output:
517
+ final_output = TASK_COMPLETED_MESSAGE
518
+
519
+ if self._can_record(browser_context):
520
+ assert self.infrastructure is not None
521
+ recording_url = self.infrastructure.resolve_recording_url(
522
+ session_id=browser_context.session_id, storage=self.storage
523
+ )
524
+ if recording_url:
525
+ await _maybe_await(
526
+ emit(build_activity_event(recording_url, RECORDING_MESSAGE))
527
+ )
528
+
529
+ metadata = recording_metadata(
530
+ infrastructure=self.infrastructure,
531
+ storage=self.storage,
532
+ browser_context=browser_context,
533
+ )
534
+ return BrowserUseRunResult(
535
+ status=status,
536
+ task=task,
537
+ final_output=final_output,
538
+ session_id=browser_context.session_id
539
+ if browser_context is not None
540
+ else None,
541
+ streaming_url=streaming_url,
542
+ recording_url=recording_url,
543
+ steps=step_count,
544
+ error=None,
545
+ events=[],
546
+ metadata={
547
+ "attempt": attempt,
548
+ **metadata,
549
+ },
550
+ )
551
+ finally:
552
+ if self._can_record(browser_context):
553
+ assert self.infrastructure is not None
554
+ self.infrastructure.schedule_recording(
555
+ browser_context, storage=self.storage
556
+ )
557
+
558
+ if agent is not None:
559
+ agent_eventbus: EventBus | None = getattr(agent, "eventbus", None)
560
+ if agent_eventbus is not None:
561
+ try:
562
+ await agent_eventbus.stop(
563
+ clear=True, timeout=_AGENT_EVENTBUS_STOP_TIMEOUT_IN_S
564
+ )
565
+ except Exception as exc:
566
+ logger.debug(
567
+ "Failed to stop agent event bus during cleanup: %s",
568
+ exc,
569
+ exc_info=True,
570
+ )
571
+ try:
572
+ await agent.close()
573
+ except Exception as exc:
574
+ logger.debug(
575
+ "Failed to close agent during cleanup: %s", exc, exc_info=True
576
+ )
577
+ elif browser_session is not None:
578
+ try:
579
+ await browser_session.kill()
580
+ except Exception as exc:
581
+ logger.debug(
582
+ "Failed to stop browser session during cleanup: %s",
583
+ exc,
584
+ exc_info=True,
585
+ )
586
+
587
+ if self.infrastructure is not None and browser_context is not None:
588
+ try:
589
+ await self.infrastructure.release_session(browser_context)
590
+ except Exception as exc:
591
+ logger.debug(
592
+ "Failed to release browser infrastructure session %s during cleanup: %s",
593
+ browser_context.session_id,
594
+ exc,
595
+ exc_info=True,
596
+ )
597
+
598
+ def _build_llm(
599
+ self,
600
+ *,
601
+ model: ChatModel | str,
602
+ reasoning_effort: ReasoningEffort,
603
+ temperature: float | None,
604
+ api_key: str,
605
+ base_url: str | None,
606
+ ) -> ChatOpenAI:
607
+ llm_kwargs: dict[str, Any] = {
608
+ "model": model,
609
+ "reasoning_effort": reasoning_effort,
610
+ "api_key": api_key,
611
+ "base_url": base_url,
612
+ }
613
+ if self._supports_tunable_params(model):
614
+ llm_kwargs["temperature"] = temperature
615
+ else:
616
+ llm_kwargs["temperature"] = None
617
+ llm_kwargs["frequency_penalty"] = None
618
+ return ChatOpenAI(**llm_kwargs)
619
+
620
+ def _supports_tunable_params(self, model: ChatModel | str) -> bool:
621
+ model_name = coerce_string(model).lower()
622
+ if model_name.startswith("gpt-5"):
623
+ return False
624
+ short_name = model_name.split("-", 1)[0]
625
+ return not (short_name.startswith("o") and short_name[1:].isdigit())
626
+
627
+ def _can_record(self, browser_context: BrowserInfrastructureContext | None) -> bool:
628
+ return self.infrastructure is not None and self.infrastructure.can_record(
629
+ browser_context, self.storage
630
+ )
631
+
632
+ @staticmethod
633
+ def _validate_task(task: str) -> str:
634
+ if not isinstance(task, str):
635
+ raise BrowserUseExecutionError("task must be a non-empty string")
636
+ value = task.strip()
637
+ if not value:
638
+ raise BrowserUseExecutionError("task must be a non-empty string")
639
+ return value
@@ -0,0 +1,57 @@
1
+ from _typeshed import Incomplete
2
+ from collections.abc import AsyncGenerator, Awaitable, Callable
3
+ from gl_browser_use.config import BrowserUseClientConfig as BrowserUseClientConfig
4
+ from gl_browser_use.errors import BrowserUseConfigurationError as BrowserUseConfigurationError, BrowserUseExecutionError as BrowserUseExecutionError, BrowserUseRetryExhaustedError as BrowserUseRetryExhaustedError
5
+ from gl_browser_use.events import RECORDING_MESSAGE as RECORDING_MESSAGE, STREAMING_MESSAGE as STREAMING_MESSAGE, TASK_COMPLETED_MESSAGE as TASK_COMPLETED_MESSAGE, build_activity_event as build_activity_event, build_error_event as build_error_event, build_retry_event as build_retry_event, build_step_content as build_step_content, build_step_event as build_step_event, build_thinking_event as build_thinking_event
6
+ from gl_browser_use.infrastructure.base import BrowserInfrastructure as BrowserInfrastructure, BrowserInfrastructureContext as BrowserInfrastructureContext
7
+ from gl_browser_use.models import BrowserUseRunResult as BrowserUseRunResult, BrowserUseRunStatus as BrowserUseRunStatus, BrowserUseStreamEvent as BrowserUseStreamEvent
8
+ from gl_browser_use.parsing import coerce_string as coerce_string, extract_final_output as extract_final_output, extract_tool_calls as extract_tool_calls, summarize_terminal_error as summarize_terminal_error, validate_structured_data_output as validate_structured_data_output
9
+ from gl_browser_use.session_errors import is_recoverable_session_error as is_recoverable_session_error
10
+ from gl_browser_use.storage.base import ObjectStorage as ObjectStorage
11
+ from typing import Any
12
+
13
+ logger: Incomplete
14
+ EmitCallable = Callable[[BrowserUseStreamEvent | None], Awaitable[None] | None]
15
+
16
+ def recording_metadata(*, infrastructure: BrowserInfrastructure | None, storage: ObjectStorage | None, browser_context: BrowserInfrastructureContext | None) -> dict[str, Any]:
17
+ """Return recording status metadata aligned with implementation behavior."""
18
+
19
+ class BrowserUseClient:
20
+ """Facade for running browser automation tasks.
21
+
22
+ Note: ``infrastructure`` and ``storage`` attributes are read by ``run``/
23
+ ``run_once``/``run_sync`` on each call. Swapping them while a run is in
24
+ flight is undefined behavior.
25
+ """
26
+ config: Incomplete
27
+ infrastructure: Incomplete
28
+ storage: Incomplete
29
+ def __init__(self, *, config: BrowserUseClientConfig, infrastructure: BrowserInfrastructure | None = None, storage: ObjectStorage | None = None) -> None:
30
+ """Initialize the client."""
31
+ async def run(self, task: str) -> AsyncGenerator[BrowserUseStreamEvent, None]:
32
+ """Run the client."""
33
+ async def run_once(self, task: str) -> BrowserUseRunResult:
34
+ """Run the client once and return a single aggregated result.
35
+
36
+ Raises:
37
+ ------
38
+ BrowserUseExecutionError
39
+ When the task input is invalid.
40
+ BrowserUseConfigurationError
41
+ When required configuration is missing.
42
+ BrowserUseRetryExhaustedError
43
+ When all recoverable session retries are exhausted.
44
+ """
45
+ def run_sync(self, task: str) -> BrowserUseRunResult:
46
+ """Run the client synchronously.
47
+
48
+ Raises:
49
+ ------
50
+ BrowserUseExecutionError
51
+ When invoked from a thread that already has a running event loop,
52
+ or when task validation fails.
53
+ BrowserUseConfigurationError
54
+ When required configuration is missing.
55
+ BrowserUseRetryExhaustedError
56
+ When all recoverable session retries are exhausted.
57
+ """