Files
wehub-resource-sync 4cd2d4af2b
Test Browser Use CLI Install / uv pip install (macos-latest) (push) Waiting to run
Test Browser Use CLI Install / uv pip install (windows-latest) (push) Waiting to run
Test Browser Use CLI Install / uv pip install (ubuntu-latest) (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use from local wheel (push) Failing after 1s
Test Browser Use CLI Install / uvx browser-use[cli] from PyPI (push) Failing after 1s
package / pip-install-on-macos-latest-py-3.11 (push) Has been skipped
package / pip-install-on-macos-latest-py-3.13 (push) Has been skipped
package / pip-install-on-ubuntu-latest-py-3.11 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.13 (push) Has been skipped
cloud_evals / trigger_cloud_eval_image_build (push) Failing after 1s
docker / build_publish_image (push) Failing after 1s
Test Browser Use CLI Install / browser-use skill sync (push) Failing after 1s
lint / code-style (push) Failing after 0s
lint / type-checker (push) Failing after 1s
package / pip-build (push) Failing after 1s
lint / syntax-errors (push) Failing after 3s
package / pip-install-on-ubuntu-latest-py-3.13 (push) Has been skipped
package / pip-install-on-windows-latest-py-3.11 (push) Has been skipped
test / ${{ matrix.test_filename }} (push) Has been skipped
test / evaluate-tasks (push) Has been skipped
test / setup-chromium (push) Failing after 2s
test / find_tests (push) Failing after 2s
chore: import upstream snapshot with attribution
2026-07-13 12:02:32 +08:00

7908 lines
241 KiB
Python

from __future__ import annotations
import asyncio
import inspect
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any, cast, get_args, get_origin, get_type_hints
import pytest
from pydantic import BaseModel
@pytest.fixture(autouse=True)
def _disable_beta_agent_latest_version_check(monkeypatch):
import browser_use.beta.service as beta_service
async def no_latest_version():
return None
monkeypatch.setattr(beta_service, 'check_latest_browser_use_version', no_latest_version)
monkeypatch.delenv('DEFAULT_LLM', raising=False)
monkeypatch.setenv('BROWSER_USE_API_KEY', 'test-browser-use-api-key')
def test_top_level_agent_preserves_python_service():
from browser_use import Agent as TopLevelAgent
from browser_use.agent.service import Agent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
assert TopLevelAgent is BrowserUseAgent
assert TopLevelAgent is not BetaAgent
def test_agent_package_export_preserves_python_service():
from browser_use.agent import Agent as AgentPackageAgent
from browser_use.agent.service import Agent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
assert AgentPackageAgent is BrowserUseAgent
assert AgentPackageAgent is not BetaAgent
def test_agent_service_export_preserves_python_service():
from browser_use.agent.service import Agent as ServiceAgent
from browser_use.beta import Agent as BetaAgent
assert ServiceAgent is not BetaAgent
def test_beta_package_reexports_chat_models_and_browser_config():
from browser_use import Browser as TopLevelBrowser
from browser_use import BrowserProfile as TopLevelBrowserProfile
from browser_use import BrowserSession as TopLevelBrowserSession
from browser_use import ChatBrowserUse as TopLevelChatBrowserUse
from browser_use.beta import (
Browser,
BrowserProfile,
BrowserSession,
ChatAnthropic,
ChatBrowserUse,
ChatGoogle,
ChatOpenAI,
)
from browser_use.llm.anthropic.chat import ChatAnthropic as LlmChatAnthropic
from browser_use.llm.google.chat import ChatGoogle as LlmChatGoogle
from browser_use.llm.openai.chat import ChatOpenAI as LlmChatOpenAI
assert Browser is TopLevelBrowser
assert BrowserProfile is TopLevelBrowserProfile
assert BrowserSession is TopLevelBrowserSession
assert ChatBrowserUse is TopLevelChatBrowserUse
assert ChatGoogle is LlmChatGoogle
assert ChatAnthropic is LlmChatAnthropic
assert ChatOpenAI is LlmChatOpenAI
def test_beta_agent_class_metadata_matches_browser_use_service_surface():
from browser_use import Agent as TopLevelAgent
from browser_use.agent.service import Agent as ServiceAgent
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
assert TopLevelAgent is ServiceAgent is BrowserUseAgent
assert BetaAgent is not BrowserUseAgent
assert BetaAgent.__name__ == BrowserUseAgent.__name__ == 'Agent'
assert BetaAgent.__qualname__ == BrowserUseAgent.__qualname__ == 'Agent'
assert BetaAgent.__module__ == BrowserUseAgent.__module__ == 'browser_use.agent.service'
assert BetaAgent.__doc__ == BrowserUseAgent.__doc__
assert repr(BetaAgent) == repr(BrowserUseAgent)
def test_beta_agent_generic_subscription_matches_browser_use():
from browser_use.agent.service import Agent as ServiceAgent
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class Answer(BaseModel):
answer: str
browser_use_alias = BrowserUseAgent[dict, Answer]
beta_alias = BetaAgent[dict, Answer]
service_alias = ServiceAgent[dict, Answer]
assert get_args(beta_alias) == get_args(browser_use_alias) == (dict, Answer)
assert get_args(service_alias) == (dict, Answer)
assert get_origin(beta_alias) is BetaAgent
assert get_origin(service_alias) is BrowserUseAgent
with pytest.raises(TypeError, match='Too few arguments'):
BetaAgent[Answer]
def test_beta_agent_constructor_signature_matches_browser_use_order(tmp_path):
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
browser_use_params = list(inspect.signature(BrowserUseAgent.__init__).parameters)
beta_params = list(inspect.signature(BetaAgent.__init__).parameters)
assert beta_params == browser_use_params
values = []
for param in list(inspect.signature(BetaAgent.__init__).parameters.values())[1:]:
if param.name == 'task':
values.append('Check constructor parity.')
elif param.name == 'llm':
values.append(type('LLM', (), {'model': 'gpt-test', 'provider': 'test-provider'})())
elif param.name == 'source':
values.append('signature-source')
elif param.name == 'file_system_path':
values.append(str(tmp_path / 'agent-files'))
elif param.name == 'task_id':
values.append('signature-task-id')
break
else:
assert param.default is not inspect.Parameter.empty
values.append(param.default)
agent = BetaAgent(*values)
assert agent.source == 'signature-source'
assert agent.file_system_path == str(tmp_path / 'agent-files')
assert agent.task_id == 'signature-task-id'
def test_beta_agent_runtime_signatures_match_browser_use_callable_surface():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
assert inspect.signature(BetaAgent) == inspect.signature(BrowserUseAgent)
assert inspect.signature(BetaAgent.__init__) == inspect.signature(BrowserUseAgent.__init__)
browser_use_callables = {
name for name, value in vars(BrowserUseAgent).items() if callable(value) and not name.startswith('__')
}
beta_callables = {name for name, value in vars(BetaAgent).items() if callable(value) and not name.startswith('__')}
for method_name in sorted(browser_use_callables & beta_callables):
assert inspect.signature(getattr(BetaAgent, method_name)) == inspect.signature(getattr(BrowserUseAgent, method_name))
def test_beta_agent_constructor_type_hints_match_browser_use_core_params():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
from browser_use.tools.service import Tools
browser_use_hints = get_type_hints(BrowserUseAgent.__init__)
beta_hints = get_type_hints(BetaAgent.__init__)
for name in (
'llm',
'register_new_step_callback',
'register_done_callback',
'output_model_schema',
'page_extraction_llm',
'sample_images',
):
assert beta_hints[name] == browser_use_hints[name]
def assert_tools_context_hint(annotation):
inner = [arg for arg in get_args(annotation) if arg is not type(None)]
assert len(inner) == 1
assert get_origin(inner[0]) is Tools
type_args = get_args(inner[0])
assert len(type_args) == 1
assert type_args[0].__name__ == 'Context'
for name in ('tools', 'controller'):
assert_tools_context_hint(browser_use_hints[name])
assert_tools_context_hint(beta_hints[name])
assert 'kwargs' not in beta_hints
assert 'return' not in beta_hints
def test_beta_agent_run_type_hints_match_browser_use_hooks():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
for method_name in ('run', 'run_sync'):
browser_use_hints = get_type_hints(getattr(BrowserUseAgent, method_name))
beta_hints = get_type_hints(getattr(BetaAgent, method_name))
assert beta_hints == browser_use_hints
def test_beta_agent_action_model_helper_type_hints_match_browser_use():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
for method_name in ('_convert_initial_actions', 'multi_act'):
browser_use_hints = get_type_hints(getattr(BrowserUseAgent, method_name))
beta_hints = get_type_hints(getattr(BetaAgent, method_name))
assert beta_hints == browser_use_hints
def test_beta_agent_browser_state_helper_type_hints_match_browser_use():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
for method_name in ('_finalize', '_get_next_action', '_log_step_context', '_make_history_item', '_prepare_context'):
browser_use_hints = get_type_hints(getattr(BrowserUseAgent, method_name))
beta_hints = get_type_hints(getattr(BetaAgent, method_name))
assert beta_hints == browser_use_hints
def test_beta_agent_llm_message_helper_type_hints_match_browser_use():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
for method_name in (
'_get_model_output_with_retry',
'_handle_post_llm_processing',
'_process_messsages_and_replace_long_urls_shorter_ones',
'get_model_output',
):
browser_use_hints = get_type_hints(getattr(BrowserUseAgent, method_name))
beta_hints = get_type_hints(getattr(BetaAgent, method_name))
assert beta_hints == browser_use_hints
def test_beta_agent_unannotated_helper_type_hints_match_browser_use():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
for method_name in ('_log_action', '_verify_and_setup_llm', 'close', 'load_and_rerun'):
browser_use_hints = get_type_hints(getattr(BrowserUseAgent, method_name))
beta_hints = get_type_hints(getattr(BetaAgent, method_name))
assert beta_hints == browser_use_hints
def test_rust_events_reconstruct_browser_use_history():
from browser_use.beta.service import _history_from_events
events = [
{
'event_type': 'browser.state',
'payload': {'url': 'https://example.com', 'title': 'Example'},
},
{
'event_type': 'model.usage',
'payload': {'input_tokens': 11, 'cached_input_tokens': 3, 'output_tokens': 7, 'cost_usd': 0.12},
},
{
'event_type': 'session.done',
'payload': {'result': 'final answer'},
},
]
history = _history_from_events(
events,
model='gpt-test',
started=1.0,
finished=2.5,
output_model_schema=None,
process_error=None,
)
assert history.final_result() == 'final answer'
assert history.is_done() is True
assert history.is_successful() is True
assert history.urls() == ['https://example.com']
assert history.usage is not None
assert history.usage.total_prompt_tokens == 11
assert history.usage.total_prompt_cached_tokens == 3
assert history.usage.total_completion_tokens == 7
def test_rust_history_ignores_internal_browser_connection_url():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'browser.connected', 'payload': {'url': 'http://127.0.0.1:41335'}},
{
'event_type': 'tool.output',
'payload': {'name': 'browser', 'text': 'Connected to browser at http://127.0.0.1:41335'},
},
{'event_type': 'browser.state', 'payload': {'url': 'https://example.com', 'title': 'Example'}},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
local_page_history = _history_from_events(
[
{'event_type': 'browser.state', 'payload': {'url': 'http://127.0.0.1:8000/', 'title': 'Local app'}},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.urls() == ['https://example.com']
assert local_page_history.urls() == ['http://127.0.0.1:8000/']
def test_rust_history_reconstructs_terminal_browser_script_urls():
from browser_use.beta.service import _history_from_events
output_history = _history_from_events(
[
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'ok': True,
'outputs': [
{
'label': 'start',
'summary': {
'kind': 'page',
'title': 'AND Digital',
'url': 'https://www.and.digital/',
},
'value': {
'title': 'AND Digital',
'url': 'https://www.and.digital/',
'target': {'target_id': 'target-1'},
},
}
],
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert output_history.urls() == ['https://www.and.digital/']
assert output_history.history[0].state.title == 'AND Digital'
assert output_history.history[0].state.tabs[0].url == 'https://www.and.digital/'
navigation_history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'arguments': {'code': "info = new_tab('https://example.com')\nwait_for_load(timeout=10)"},
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert navigation_history.urls() == ['https://example.com']
def test_rust_history_reconstructs_terminal_browser_live_url():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'browser.live_url',
'payload': {
'live_url': 'https://live.browser-use.com/watch/session-123',
'url': 'https://live.browser-use.com/watch/session-123',
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.urls() == ['https://live.browser-use.com/watch/session-123']
assert history.history[0].state.url == 'https://live.browser-use.com/watch/session-123'
assert history.history[0].state.tabs[0].url == 'https://live.browser-use.com/watch/session-123'
def test_rust_history_reconstructs_terminal_screenshot_paths(tmp_path):
import base64
from browser_use.beta.service import _history_from_events
screenshot_path = tmp_path / 'terminal-shot.png'
screenshot_bytes = b'\x89PNG\r\n\x1a\nterminal-shot'
screenshot_path.write_bytes(screenshot_bytes)
history = _history_from_events(
[
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'images': [{'path': str(screenshot_path), 'mime_type': 'image/png'}],
},
},
{
'type': 'tool.image',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'image': {'path': str(screenshot_path), 'mime_type': 'image/png'},
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.screenshot_paths() == [str(screenshot_path)]
assert history.screenshots() == [base64.b64encode(screenshot_bytes).decode('utf-8')]
def test_rust_history_attaches_terminal_tool_images_to_actions(tmp_path):
from browser_use.beta.service import _history_from_events
screenshot_path = tmp_path / 'tool-image.png'
screenshot_path.write_bytes(b'\x89PNG\r\n\x1a\nterminal-image')
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': 'observe_page()'},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'text': 'Observed page.',
'images': [{'path': str(screenshot_path), 'mime_type': 'image/png'}],
},
},
{
'type': 'tool.image',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'image': {'path': str(screenshot_path), 'mime_type': 'image/png'},
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
results = history.action_results()
assert history.action_names() == ['browser_script', 'done']
assert results[0].attachments == [str(screenshot_path)]
assert results[0].extracted_content == 'Observed page.'
assert results[-1].attachments is None
assert history.screenshot_paths() == [str(screenshot_path)]
def test_rust_history_compacts_large_terminal_tool_memory():
from browser_use.beta.service import _history_from_events
large_output = 'large browser output\n' + ('row,value\n' * 200)
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': 'print(document.body.innerText)'},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'text': large_output,
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
result = history.action_results()[0]
assert result.extracted_content == large_output.strip()
assert result.include_extracted_content_only_once is True
assert result.long_term_memory is not None
assert 'browser_script returned' in result.long_term_memory
assert large_output.strip() not in result.long_term_memory
def test_rust_history_reconstructs_terminal_tool_call_actions():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'ok': True,
'text': 'Opened Example Domain',
},
},
{
'type': 'tool.started',
'payload': {
'name': 'done',
'tool_call_id': 'call-done',
'arguments': {'text': 'Example Domain', 'success': True},
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
actions = history.model_actions()
action_history = history.action_history()
assert history.action_names() == ['browser_script', 'done']
assert actions[0]['browser_script']['code'] == "goto_url('https://example.com')"
assert actions[1]['done'] == {'text': 'Example Domain', 'success': True}
assert history.last_action() == {'done': {'text': 'Example Domain', 'success': True}}
assert action_history[0][0]['result'] == 'Opened Example Domain'
assert history.final_result() == 'Example Domain'
assert history.action_results()[-1].is_done is True
def test_rust_history_reconstructs_eval_visible_multi_turn_actions_and_usage():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'type': 'model.turn.request', 'ts_ms': 1_000, 'payload': {'model': 'claude-sonnet-4-6'}},
{'type': 'browser.state', 'ts_ms': 1_010, 'payload': {'url': 'about:blank', 'title': ''}},
{
'type': 'tool.started',
'ts_ms': 1_020,
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-open',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'type': 'tool.output',
'ts_ms': 1_030,
'payload': {'name': 'browser_script', 'tool_call_id': 'call-open', 'text': 'Opened Example Domain'},
},
{'type': 'model.usage', 'payload': {'input_tokens': 100, 'cached_input_tokens': 20, 'output_tokens': 15}},
{'type': 'model.turn.request', 'ts_ms': 2_000, 'payload': {'model': 'claude-sonnet-4-6'}},
{'type': 'browser.state', 'ts_ms': 2_010, 'payload': {'url': 'https://example.com', 'title': 'Example Domain'}},
{
'type': 'model.tool_call',
'ts_ms': 2_020,
'payload': {
'id': 'call-extract',
'name': 'browser_script',
'arguments': {'code': 'emit_output(page_info(), label="page_info")'},
},
},
{
'type': 'tool.output',
'ts_ms': 2_030,
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-extract',
'text': 'Page title: Example Domain',
},
},
{'type': 'model.usage', 'payload': {'input_tokens': 140, 'cached_input_tokens': 90, 'output_tokens': 25}},
{
'type': 'tool.started',
'ts_ms': 2_040,
'payload': {
'name': 'done',
'tool_call_id': 'call-done',
'arguments': {'text': 'Example Domain', 'success': True},
},
},
{'type': 'session.done', 'ts_ms': 2_050, 'payload': {'result': 'Example Domain'}},
],
model='claude-sonnet-4-6',
started=1.0,
finished=2.5,
output_model_schema=None,
process_error=None,
)
dump = history.model_dump()
action_history = history.action_history()
assert history.number_of_steps() == 2
assert len(dump['history']) == 2
assert history.action_names() == ['browser_script', 'browser_script', 'done']
assert action_history[0][0]['browser_script']['code'] == "goto_url('https://example.com')"
assert action_history[0][0]['result'] == 'Opened Example Domain'
assert action_history[1][0]['browser_script']['code'] == 'emit_output(page_info(), label="page_info")'
assert action_history[1][0]['result'] == 'Page title: Example Domain'
assert action_history[1][-1]['done'] == {'text': 'Example Domain', 'success': True}
assert history.urls() == ['https://example.com', 'https://example.com']
assert history.final_result() == 'Example Domain'
assert history.usage is not None
assert history.usage.total_prompt_tokens == 240
assert history.usage.total_prompt_cached_tokens == 110
assert history.usage.total_completion_tokens == 40
assert history.usage.total_tokens == 280
assert history.usage.entry_count == 2
def test_rust_history_reconstructs_terminal_model_tool_call_actions():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'model.tool_call',
'payload': {
'id': 'call-browser',
'name': 'browser_script',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': 'duplicate_should_not_replace_original()'},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'text': 'Opened Example Domain',
},
},
{
'type': 'model.response.output_item',
'payload': {
'item': {
'type': 'function_call',
'call_id': 'call-done',
'name': 'done',
'arguments': '{"text": "Example Domain", "success": true}',
}
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
actions = history.model_actions()
assert history.action_names() == ['browser_script', 'done']
assert actions[0]['browser_script']['code'] == "goto_url('https://example.com')"
assert actions[1]['done'] == {'text': 'Example Domain', 'success': True}
assert history.action_history()[0][0]['result'] == 'Opened Example Domain'
assert history.last_action() == {'done': {'text': 'Example Domain', 'success': True}}
def test_rust_history_reconstructs_terminal_response_input_item_tool_results():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'model.tool_call',
'payload': {
'id': 'call-browser',
'name': 'browser_script',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'type': 'model.response.input_item',
'payload': {
'call_id': 'call-browser',
'name': 'browser_script',
'item': {
'type': 'function_call_output',
'call_id': 'call-browser',
'output': [
{'type': 'input_text', 'text': 'Opened '},
{'type': 'input_text', 'text': 'Example Domain'},
{'type': 'input_image', 'image_url': 'data:image/png;base64,AAAA'},
],
},
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
first_result = history.action_results()[0]
assert history.action_names() == ['browser_script', 'done']
assert first_result.extracted_content == 'Opened Example Domain'
assert first_result.long_term_memory == 'Opened Example Domain'
assert history.action_history()[0][0]['result'] == 'Opened Example Domain'
assert history.final_result() == 'Example Domain'
def test_rust_history_reconstructs_terminal_tool_finished_results():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'read_file',
'tool_call_id': 'call-read',
'arguments': {'path': 'README.md'},
},
},
{
'type': 'tool.finished',
'payload': {
'name': 'read_file',
'tool_call_id': 'call-read',
},
},
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'text': 'Opened Example Domain',
},
},
{
'type': 'tool.finished',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
results = history.action_results()
assert history.action_names() == ['read_file', 'browser_script', 'done']
assert results[0].extracted_content == 'read_file completed'
assert results[0].long_term_memory == 'read_file completed'
assert results[1].extracted_content == 'Opened Example Domain'
assert history.action_history()[0][0]['result'] == 'read_file completed'
assert history.action_history()[0][1]['result'] == 'Opened Example Domain'
assert history.final_result() == 'Example Domain'
def test_rust_history_reconstructs_terminal_tool_output_deltas():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'python',
'tool_call_id': 'call-python',
'arguments': {'code': "print('hello world')"},
},
},
{
'type': 'tool.output_delta',
'payload': {
'name': 'python',
'tool_call_id': 'call-python',
'stream': 'stdout',
'text': 'hello',
},
},
{
'type': 'tool.output_delta',
'payload': {
'name': 'python',
'tool_call_id': 'call-python',
'stream': 'stdout',
'text': ' world\n',
},
},
{
'type': 'tool.finished',
'payload': {
'name': 'python',
'tool_call_id': 'call-python',
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
first_result = history.action_results()[0]
assert history.action_names() == ['python', 'done']
assert history.model_actions()[0]['python']['code'] == "print('hello world')"
assert first_result.extracted_content == 'hello world'
assert first_result.long_term_memory == 'hello world'
assert history.action_history()[0][0]['result'] == 'hello world'
assert history.final_result() == 'final answer'
def test_rust_history_reconstructs_terminal_exec_command_output_deltas():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'arguments': {'cmd': "printf 'hello world'"},
},
},
{
'type': 'tool.output_delta',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'process_id': '1000',
'session_id': 1000,
'stream': 'stdout',
'text': 'hello ',
},
},
{
'type': 'exec_command.output_delta',
'payload': {
'call_id': 'call-exec',
'process_id': '1000',
'session_id': 1000,
'stream': 'stdout',
'chunk': 'hello ',
},
},
{
'type': 'exec_command.output_delta',
'payload': {
'call_id': 'call-exec',
'process_id': '1000',
'session_id': 1000,
'stream': 'stdout',
'chunk': 'world\n',
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
first_result = history.action_results()[0]
assert history.action_names() == ['exec_command', 'done']
assert history.model_actions()[0]['exec_command']['cmd'] == "printf 'hello world'"
assert first_result.extracted_content == 'hello world'
assert first_result.long_term_memory == 'hello world'
assert history.action_history()[0][0]['result'] == 'hello world'
assert history.final_result() == 'final answer'
def test_rust_history_reconstructs_terminal_exec_command_end_output():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'arguments': {'cmd': "printf 'final stdout'"},
},
},
{
'type': 'exec_command.end',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'process_id': '1000',
'session_id': 1000,
'exit_code': 0,
'wall_time_seconds': 0.1,
'output': 'final stdout\n',
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
first_result = history.action_results()[0]
assert history.action_names() == ['exec_command', 'done']
assert history.model_actions()[0]['exec_command']['cmd'] == "printf 'final stdout'"
assert first_result.extracted_content == 'final stdout'
assert first_result.long_term_memory == 'final stdout'
assert history.action_history()[0][0]['result'] == 'final stdout'
assert history.final_result() == 'final answer'
def test_rust_history_surfaces_terminal_exec_command_end_failure():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{
'type': 'tool.started',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'arguments': {'cmd': 'exit 2'},
},
},
{
'type': 'exec_command.end',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'process_id': '1000',
'session_id': 1000,
'exit_code': 2,
'wall_time_seconds': 0.1,
'output': 'command failed\n',
},
},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
error = 'exec_command failed: exit code 2: command failed'
first_result = history.action_results()[0]
assert history.final_result() is None
assert history.is_done() is False
assert history.errors() == [error]
assert first_result.error == error
assert first_result.extracted_content == 'command failed'
assert first_result.long_term_memory == 'command failed'
assert history.action_history()[0][0]['result'] == 'command failed'
assert history.action_results()[-1].error == error
def test_rust_history_reconstructs_terminal_command_waiting_result():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'arguments': {'cmd': 'sleep 60 && echo done', 'yield_time_ms': 250},
},
},
{
'type': 'tool.output_delta',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'process_id': '1000',
'session_id': 1000,
'stream': 'stdout',
'text': 'started\n',
},
},
{
'type': 'command.waiting',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'process_id': '1000',
'session_id': 1000,
},
},
{'event_type': 'session.done', 'payload': {'result': 'command is still running'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
first_result = history.action_results()[0]
waiting_result = 'started\n\nProcess running with session ID 1000'
assert history.action_names() == ['exec_command', 'done']
assert first_result.extracted_content == waiting_result
assert first_result.long_term_memory == waiting_result
assert first_result.error is None
assert history.action_history()[0][0]['result'] == waiting_result
assert history.final_result() == 'command is still running'
def test_rust_history_reconstructs_terminal_unkeyed_tool_results():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{
'type': 'tool.started',
'payload': {
'name': 'python',
'arguments': {'code': "print('full output')"},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'python',
'stream': True,
'text': 'partial output',
},
},
{
'type': 'tool.output',
'payload': {
'name': 'python',
'ok': True,
'text': 'full output',
},
},
{
'type': 'tool.finished',
'payload': {
'name': 'python',
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
first_result = history.action_results()[0]
assert history.action_names() == ['python', 'done']
assert history.model_actions()[0]['python']['code'] == "print('full output')"
assert first_result.extracted_content == 'full output'
assert first_result.long_term_memory == 'full output'
assert history.action_history()[0][0]['result'] == 'full output'
assert 'partial output' not in json.dumps(history.action_history(), sort_keys=True)
assert history.final_result() == 'final answer'
def test_rust_history_reconstructs_terminal_structured_tool_output_results():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': 'return page.summary()'},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'ok': True,
'text': '',
'summary': {
'kind': 'page',
'title': 'Example Domain',
'url': 'https://example.com',
},
'data': {'ignored_when_summary_exists': True},
'outputs': [{'label': 'page', 'value': {'url': 'https://example.com'}}],
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
first_result = history.action_results()[0]
structured_result = json.loads(first_result.extracted_content or '{}')
assert history.action_names() == ['browser_script', 'done']
assert structured_result == {
'kind': 'page',
'title': 'Example Domain',
'url': 'https://example.com',
}
assert first_result.long_term_memory == first_result.extracted_content
assert json.loads(history.action_history()[0][0]['result'])['url'] == 'https://example.com'
assert history.final_result() == 'Example Domain'
def test_rust_history_synthesizes_done_action_from_terminal_completion():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'text': 'Opened Example Domain',
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.is_done() is True
assert history.action_names() == ['browser_script', 'done']
assert history.last_action() == {'done': {'text': 'Example Domain', 'success': True}}
assert history.model_actions_filtered(['done']) == [
{'done': {'text': 'Example Domain', 'success': True}, 'interacted_element': None}
]
assert history.action_history()[0][-1]['result'] == 'Example Domain'
def test_rust_history_reconstructs_terminal_model_turn_steps():
from browser_use.beta.service import _history_from_events
events = [
{'type': 'session.created', 'ts_ms': 1_000, 'payload': {}},
{'type': 'model.turn.request', 'ts_ms': 2_000, 'payload': {'model': 'gpt-test', 'attempt': 0}},
{'type': 'model.stream_delta', 'ts_ms': 2_010, 'payload': {'text': 'Connect the browser.'}},
{
'type': 'tool.started',
'ts_ms': 2_020,
'payload': {
'name': 'browser',
'tool_call_id': 'call-browser',
'arguments': {'cmd': 'connect managed --headless'},
},
},
{
'type': 'tool.output',
'ts_ms': 2_030,
'payload': {'name': 'browser', 'tool_call_id': 'call-browser', 'text': 'Connected browser'},
},
{
'type': 'browser.state',
'ts_ms': 2_040,
'payload': {'url': 'https://example.com', 'title': 'Example Domain'},
},
{'type': 'model.turn.request', 'ts_ms': 3_000, 'payload': {'model': 'gpt-test', 'attempt': 0}},
{'type': 'model.stream_delta', 'ts_ms': 3_010, 'payload': {'text': 'Return the page title.'}},
{
'type': 'tool.started',
'ts_ms': 3_020,
'payload': {
'name': 'done',
'tool_call_id': 'call-done',
'arguments': {'text': 'Example Domain', 'success': True},
},
},
{
'type': 'tool.output',
'ts_ms': 3_030,
'payload': {'name': 'done', 'tool_call_id': 'call-done', 'text': 'done:Example Domain'},
},
{'type': 'session.done', 'ts_ms': 3_040, 'payload': {'result': 'Example Domain'}},
]
history = _history_from_events(
events,
model='gpt-test',
started=1.0,
finished=4.0,
output_model_schema=None,
process_error=None,
)
action_history = history.action_history()
assert history.number_of_steps() == 2
assert history.urls() == ['https://example.com', 'https://example.com']
assert history.action_names() == ['browser', 'done']
assert history.model_outputs()[0].memory == 'Connect the browser.'
assert history.model_outputs()[1].memory == 'Return the page title.'
assert action_history[0][0]['result'] == 'Connected browser'
assert action_history[1][0]['result'] == 'Example Domain'
assert history.final_result() == 'Example Domain'
assert history.is_done() is True
def test_rust_history_prefers_done_tool_text_over_session_summary():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'type': 'model.turn.request', 'ts_ms': 1_000, 'payload': {'model': 'claude-sonnet-4-0', 'attempt': 0}},
{
'type': 'tool.started',
'ts_ms': 1_010,
'payload': {
'name': 'done',
'tool_call_id': 'call-done',
'arguments': {'text': 'Paramjit Uppal, Founder', 'success': True},
},
},
{
'type': 'tool.output',
'ts_ms': 1_020,
'payload': {'name': 'done', 'tool_call_id': 'call-done', 'text': 'done:Paramjit Uppal, Founder'},
},
{
'type': 'session.done',
'ts_ms': 1_030,
'payload': {'result': 'I found the founder and will now provide the requested final format.'},
},
],
model='claude-sonnet-4-0',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.final_result() == 'Paramjit Uppal, Founder'
assert history.action_results()[-1].extracted_content == 'Paramjit Uppal, Founder'
assert history.is_successful() is True
def test_rust_history_applies_terminal_session_rollback():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'session.input', 'seq': 1, 'payload': {'text': 'first task'}},
{'event_type': 'model.turn.request', 'seq': 2, 'ts_ms': 2_000, 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.stream_delta', 'seq': 3, 'payload': {'text': 'First turn'}},
{
'event_type': 'tool.started',
'seq': 4,
'payload': {
'name': 'browser',
'tool_call_id': 'call-first',
'arguments': {'cmd': 'connect managed --headless'},
},
},
{
'event_type': 'tool.output',
'seq': 5,
'payload': {'name': 'browser', 'tool_call_id': 'call-first', 'text': 'First kept'},
},
{'event_type': 'session.followup', 'seq': 6, 'payload': {'text': 'second task'}},
{'event_type': 'model.turn.request', 'seq': 7, 'ts_ms': 3_000, 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.stream_delta', 'seq': 8, 'payload': {'text': 'Second rolled back'}},
{
'event_type': 'tool.started',
'seq': 9,
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-second',
'arguments': {'code': "goto_url('https://rolled-back.example')"},
},
},
{
'event_type': 'tool.output',
'seq': 10,
'payload': {'name': 'browser_script', 'tool_call_id': 'call-second', 'text': 'Second removed'},
},
{'event_type': 'session.done', 'seq': 11, 'payload': {'result': 'Rolled Back Result'}},
{'event_type': 'session.rollback', 'seq': 12, 'payload': {'num_turns': 1}},
{'event_type': 'session.followup', 'seq': 13, 'payload': {'text': 'third task'}},
{'event_type': 'model.turn.request', 'seq': 14, 'ts_ms': 4_000, 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.stream_delta', 'seq': 15, 'payload': {'text': 'Third turn'}},
{
'event_type': 'tool.started',
'seq': 16,
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-third',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'event_type': 'tool.output',
'seq': 17,
'payload': {'name': 'browser_script', 'tool_call_id': 'call-third', 'text': 'Third kept'},
},
{'event_type': 'session.done', 'seq': 18, 'payload': {'result': 'Third result'}},
],
model='gpt-test',
started=1.0,
finished=5.0,
output_model_schema=None,
process_error=None,
)
serialized_actions = json.dumps(history.model_actions(), sort_keys=True)
assert history.number_of_steps() == 2
assert history.action_names() == ['browser', 'browser_script', 'done']
assert [output.memory for output in history.model_outputs()] == ['First turn', 'Third turn']
assert history.action_history()[0][0]['result'] == 'First kept'
assert history.action_history()[1][0]['result'] == 'Third kept'
assert history.final_result() == 'Third result'
assert 'Second rolled back' not in serialized_actions
assert 'rolled-back.example' not in serialized_actions
def test_rust_history_applies_terminal_session_compaction_boundary():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'session.input', 'seq': 1, 'payload': {'text': 'old task'}},
{'event_type': 'model.turn.request', 'seq': 2, 'ts_ms': 2_000, 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.stream_delta', 'seq': 3, 'payload': {'text': 'Old turn'}},
{
'event_type': 'tool.started',
'seq': 4,
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-old',
'arguments': {'code': "goto_url('https://old.example')"},
},
},
{
'event_type': 'tool.output',
'seq': 5,
'payload': {'name': 'browser_script', 'tool_call_id': 'call-old', 'text': 'Old removed'},
},
{'event_type': 'session.done', 'seq': 6, 'payload': {'result': 'Old result'}},
{
'event_type': 'session.compacted',
'seq': 7,
'payload': {
'replacement_messages': [{'role': 'user', 'content': 'compacted summary'}],
},
},
{'event_type': 'session.followup', 'seq': 8, 'payload': {'text': 'post-compaction task'}},
{'event_type': 'model.turn.request', 'seq': 9, 'ts_ms': 3_000, 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.stream_delta', 'seq': 10, 'payload': {'text': 'Post compaction turn'}},
{
'event_type': 'tool.started',
'seq': 11,
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-post',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'event_type': 'tool.output',
'seq': 12,
'payload': {'name': 'browser_script', 'tool_call_id': 'call-post', 'text': 'Post kept'},
},
{'event_type': 'session.done', 'seq': 13, 'payload': {'result': 'Post result'}},
],
model='gpt-test',
started=1.0,
finished=4.0,
output_model_schema=None,
process_error=None,
)
serialized_actions = json.dumps(history.model_actions(), sort_keys=True)
assert history.number_of_steps() == 1
assert history.action_names() == ['browser_script', 'done']
assert history.model_outputs()[0].memory == 'Post compaction turn'
assert history.action_history()[0][0]['result'] == 'Post kept'
assert history.final_result() == 'Post result'
assert 'Old turn' not in serialized_actions
assert 'old.example' not in serialized_actions
assert 'Old result' not in (history.final_result() or '')
def test_rust_history_reconstructs_terminal_streamed_model_thoughts():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.stream_delta', 'payload': {'text': 'stale text'}},
{'event_type': 'model.thinking_delta', 'payload': {'text': 'stale thinking'}},
{'event_type': 'model.turn.error', 'payload': {'transient': True, 'error': 'retry'}},
{'event_type': 'model.turn.retry', 'payload': {'attempt': 1, 'max_retries': 3}},
{'event_type': 'model.thinking_delta', 'payload': {'text': 'checking '}},
{'event_type': 'model.thinking_delta', 'payload': {'text': 'checking page'}},
{'event_type': 'model.stream_delta', 'payload': {'text': 'Opening '}},
{'event_type': 'model.stream_delta', 'payload': {'text': 'Opening Example Domain'}},
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
output = history.model_outputs()[0]
thought = history.model_thoughts()[0]
assert output.thinking == 'checking page'
assert output.memory == 'Opening Example Domain'
assert thought.thinking == 'checking page'
assert thought.memory == 'Opening Example Domain'
assert 'stale' not in thought.memory
assert 'stale' not in (thought.thinking or '')
def test_rust_history_reconstructs_terminal_response_item_model_text():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.stream_delta', 'payload': {'text': 'I will '}},
{
'event_type': 'model.response.output_item',
'payload': {
'item': {
'type': 'message',
'role': 'assistant',
'content': [
{'type': 'output_text', 'text': 'I will inspect the page, '},
{'type': 'text', 'text': 'then report the title.'},
],
}
},
},
{
'type': 'model.tool_call',
'payload': {
'id': 'call-browser',
'name': 'browser_script',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'text': 'Opened Example Domain',
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
output = history.model_outputs()[0]
assert output.memory == 'I will inspect the page, then report the title.'
assert output.current_state.memory == 'I will inspect the page, then report the title.'
assert history.action_history()[0][0]['result'] == 'Opened Example Domain'
def test_rust_history_reconstructs_terminal_response_item_reasoning():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.thinking_delta', 'payload': {'text': 'stale thinking'}},
{'event_type': 'model.turn.retry', 'payload': {'attempt': 1, 'max_retries': 3}},
{
'event_type': 'model.response.output_item',
'payload': {
'item': {
'type': 'reasoning',
'summary': [
{'type': 'summary_text', 'text': 'checking '},
{'type': 'summary_text', 'text': 'page'},
],
}
},
},
{
'event_type': 'model.response.output_item',
'payload': {
'item': {
'type': 'message',
'role': 'assistant',
'content': [{'type': 'output_text', 'text': 'I will inspect the page.'}],
}
},
},
{
'type': 'model.tool_call',
'payload': {
'id': 'call-browser',
'name': 'browser_script',
'arguments': {'code': "goto_url('https://example.com')"},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'text': 'Opened Example Domain',
},
},
{'event_type': 'session.done', 'payload': {'result': 'Example Domain'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
output = history.model_outputs()[0]
thought = history.model_thoughts()[0]
assert output.thinking == 'checking page'
assert output.memory == 'I will inspect the page.'
assert thought.thinking == 'checking page'
assert 'stale' not in (thought.thinking or '')
assert history.action_history()[0][0]['result'] == 'Opened Example Domain'
def test_rust_history_reconstructs_terminal_token_count_usage():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 2,
'input_tokens': 5,
'output_tokens': 7,
'total_tokens': 12,
},
'total_token_usage': {
'cached_input_tokens': 13,
'input_tokens': 29,
'output_tokens': 31,
'total_tokens': 60,
},
}
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.usage is not None
assert history.usage.total_prompt_tokens == 29
assert history.usage.total_prompt_cached_tokens == 13
assert history.usage.total_completion_tokens == 31
assert history.usage.total_tokens == 60
assert history.usage.entry_count == 1
assert history.usage.by_model['gpt-test'].prompt_tokens == 29
assert history.usage.by_model['gpt-test'].completion_tokens == 31
def test_rust_history_sums_token_count_last_usage_when_latest_total_underreports():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 20,
'input_tokens': 100,
'output_tokens': 10,
'total_tokens': 110,
},
'total_token_usage': {
'cached_input_tokens': 20,
'input_tokens': 100,
'output_tokens': 10,
'total_tokens': 110,
},
}
},
},
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 80,
'input_tokens': 200,
'output_tokens': 20,
'total_tokens': 220,
},
# Some terminal recompute paths preserve only the latest context
# counters here. Dashboard usage should still reflect the full run.
'total_token_usage': {
'cached_input_tokens': 80,
'input_tokens': 200,
'output_tokens': 20,
'total_tokens': 220,
},
}
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.usage is not None
assert history.usage.total_prompt_tokens == 300
assert history.usage.total_prompt_cached_tokens == 100
assert history.usage.total_completion_tokens == 30
assert history.usage.total_tokens == 330
assert history.usage.entry_count == 2
def test_rust_history_reconstructs_terminal_reasoning_token_usage():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 2,
'input_tokens': 50,
'output_tokens': 20,
'reasoning_output_tokens': 5,
'total_tokens': 75,
},
'total_token_usage': {
'cached_input_tokens': 10,
'input_tokens': 100,
'output_tokens': 40,
'reasoning_output_tokens': 15,
'total_tokens': 155,
},
}
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.usage is not None
assert history.usage.total_prompt_tokens == 100
assert history.usage.total_prompt_cached_tokens == 10
assert history.usage.total_completion_tokens == 55
assert history.usage.total_tokens == 155
assert history.usage.entry_count == 1
assert history.usage.by_model['gpt-test'].completion_tokens == 55
assert history.usage.by_model['gpt-test'].total_tokens == 155
def test_rust_history_reconstructs_terminal_nested_model_usage():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'event_type': 'model.usage',
'payload': {
'usage': {
'input_tokens': 17,
'cached_input_tokens': 5,
'output_tokens': 11,
},
'cost_usd': 0.0123,
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.usage is not None
assert history.usage.total_prompt_tokens == 17
assert history.usage.total_prompt_cached_tokens == 5
assert history.usage.total_completion_tokens == 11
assert history.usage.total_tokens == 28
assert history.usage.total_cost == 0.0123
assert history.usage.entry_count == 1
assert history.usage.by_model['gpt-test'].prompt_tokens == 17
assert history.usage.by_model['gpt-test'].completion_tokens == 11
assert history.usage.by_model['gpt-test'].cost == 0.0123
def test_rust_history_token_count_does_not_shrink_model_usage_totals():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'event_type': 'model.usage',
'payload': {
'input_tokens': 120_000,
'cached_input_tokens': 119_000,
'input_cache_creation_tokens': 4_000,
'output_tokens': 2_000,
'total_tokens': 126_000,
},
},
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'input_tokens': 8_000,
'cached_input_tokens': 7_000,
'output_tokens': 200,
'total_tokens': 8_200,
},
'total_token_usage': {
'input_tokens': 8_000,
'cached_input_tokens': 7_000,
'output_tokens': 200,
'total_tokens': 8_200,
},
},
},
},
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.usage is not None
assert history.usage.total_prompt_tokens == 120_000
assert history.usage.total_prompt_cached_tokens == 119_000
assert history.usage.total_prompt_cache_creation_tokens == 4_000
assert history.usage.total_completion_tokens == 2_000
assert history.usage.total_tokens == 126_000
assert history.usage.entry_count == 1
async def test_rust_terminal_usage_prices_token_count_events(monkeypatch):
from browser_use.beta.service import _usage_from_events_with_costs
from browser_use.tokens.service import TokenCost
async def fail_fetch(_self):
raise AssertionError('custom model pricing should not fetch remote pricing')
monkeypatch.setattr(TokenCost, '_fetch_and_cache_pricing_data', fail_fetch)
events = [
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 20,
'input_tokens': 100,
'output_tokens': 10,
'total_tokens': 110,
},
'total_token_usage': {
'cached_input_tokens': 20,
'input_tokens': 100,
'output_tokens': 10,
'total_tokens': 110,
},
},
},
},
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 0,
'input_tokens': 200,
'output_tokens': 20,
'total_tokens': 220,
},
'total_token_usage': {
'cached_input_tokens': 20,
'input_tokens': 300,
'output_tokens': 30,
'total_tokens': 330,
},
},
},
},
]
summary = await _usage_from_events_with_costs(events, 'claude-sonnet-4-6', TokenCost(include_cost=True))
assert summary.total_prompt_tokens == 300
assert summary.total_prompt_cached_tokens == 20
assert summary.total_prompt_cache_creation_tokens == 0
assert summary.total_completion_tokens == 30
assert summary.total_tokens == 330
assert summary.entry_count == 2
assert summary.total_prompt_cost == pytest.approx(0.000846)
assert summary.total_prompt_cached_cost == pytest.approx(0.000006)
assert summary.total_prompt_cache_creation_cost == pytest.approx(0.0)
assert summary.total_completion_cost == pytest.approx(0.00045)
assert summary.total_cost == pytest.approx(0.001296)
assert summary.by_model['claude-sonnet-4-6'].cost == pytest.approx(0.001296)
async def test_rust_terminal_usage_prices_anthropic_raw_cache_reads(monkeypatch):
from browser_use.beta.service import _usage_from_events_with_costs
from browser_use.tokens.service import TokenCost
async def fail_fetch(_self):
raise AssertionError('custom model pricing should not fetch remote pricing')
monkeypatch.setattr(TokenCost, '_fetch_and_cache_pricing_data', fail_fetch)
events = [
{
'event_type': 'model.usage',
'payload': {
'usage': {
'input_tokens': 12,
'cache_read_input_tokens': 183250,
'cache_creation_input_tokens': 44,
'output_tokens': 3088,
},
},
},
]
summary = await _usage_from_events_with_costs(events, 'claude-sonnet-4-6', TokenCost(include_cost=True))
assert summary.total_prompt_tokens == 183262
assert summary.total_prompt_cached_tokens == 183250
assert summary.total_prompt_cache_creation_tokens == 44
assert summary.total_completion_tokens == 3088
assert summary.total_tokens == 186394
assert summary.total_prompt_cost == pytest.approx(0.055176)
assert summary.total_prompt_cached_cost == pytest.approx(0.054975)
assert summary.total_prompt_cache_creation_cost == pytest.approx(0.000165)
assert summary.total_completion_cost == pytest.approx(0.04632)
assert summary.total_cost == pytest.approx(0.101496)
assert summary.by_model['claude-sonnet-4-6'].prompt_tokens == 183262
assert summary.by_model['claude-sonnet-4-6'].cost == pytest.approx(0.101496)
async def test_rust_terminal_usage_sums_token_count_cache_creation(monkeypatch):
from browser_use.beta.service import _usage_from_events_with_costs
from browser_use.tokens.service import TokenCost
async def fail_fetch(_self):
raise AssertionError('custom model pricing should not fetch remote pricing')
monkeypatch.setattr(TokenCost, '_fetch_and_cache_pricing_data', fail_fetch)
events = [
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 0,
'input_cache_creation_tokens': 18462,
'input_tokens': 3,
'output_tokens': 71,
'total_tokens': 18536,
},
'total_token_usage': {
'cached_input_tokens': 0,
'input_cache_creation_tokens': 18462,
'input_tokens': 3,
'output_tokens': 71,
'total_tokens': 18536,
},
},
},
},
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 18462,
'input_cache_creation_tokens': 1506,
'input_tokens': 18463,
'output_tokens': 177,
'total_tokens': 20146,
},
'total_token_usage': {
'cached_input_tokens': 18462,
'input_tokens': 18466,
'output_tokens': 248,
'total_tokens': 38682,
},
},
},
},
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 18462,
'input_cache_creation_tokens': 5067,
'input_tokens': 18463,
'output_tokens': 182,
'total_tokens': 23712,
},
'total_token_usage': {
'cached_input_tokens': 36924,
'input_tokens': 36929,
'output_tokens': 430,
'total_tokens': 62394,
},
},
},
},
]
summary = await _usage_from_events_with_costs(events, 'claude-sonnet-4-6', TokenCost(include_cost=True))
assert summary.total_prompt_tokens == 36929
assert summary.total_prompt_cached_tokens == 36924
assert summary.total_prompt_cache_creation_tokens == 25035
assert summary.total_completion_tokens == 430
assert summary.total_tokens == 62394
assert summary.total_prompt_cost == pytest.approx(0.10497345)
assert summary.total_prompt_cached_cost == pytest.approx(0.0110772)
assert summary.total_prompt_cache_creation_cost == pytest.approx(0.09388125)
assert summary.total_completion_cost == pytest.approx(0.00645)
assert summary.total_cost == pytest.approx(0.11142345)
async def test_rust_terminal_usage_priced_summary_sums_cache_read_tokens(monkeypatch):
from browser_use.beta.service import _usage_from_events_with_costs
from browser_use.tokens.service import TokenCost
async def fail_fetch(_self):
raise AssertionError('custom model pricing should not fetch remote pricing')
monkeypatch.setattr(TokenCost, '_fetch_and_cache_pricing_data', fail_fetch)
events = [
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 1000,
'input_cache_creation_tokens': 200,
'input_tokens': 1001,
'output_tokens': 10,
'total_tokens': 1211,
},
'total_token_usage': {
'cached_input_tokens': 1000,
'input_cache_creation_tokens': 200,
'input_tokens': 1001,
'output_tokens': 10,
'total_tokens': 1211,
},
},
},
},
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 1000,
'input_cache_creation_tokens': 50,
'input_tokens': 1001,
'output_tokens': 20,
'total_tokens': 1071,
},
'total_token_usage': {
'cached_input_tokens': 1000,
'input_cache_creation_tokens': 200,
'input_tokens': 1001,
'output_tokens': 30,
'total_tokens': 1231,
},
},
},
},
]
summary = await _usage_from_events_with_costs(events, 'claude-sonnet-4-6', TokenCost(include_cost=True))
assert summary.entry_count == 2
assert summary.total_prompt_tokens == 2002
assert summary.total_prompt_cached_tokens == 2000
assert summary.total_prompt_cache_creation_tokens == 250
assert summary.total_completion_tokens == 30
assert summary.total_tokens == 2282
assert summary.total_prompt_cached_cost == pytest.approx(2000 * (0.30 / 1_000_000))
assert summary.total_prompt_cache_creation_cost == pytest.approx(250 * (3.75 / 1_000_000))
assert summary.total_completion_cost == pytest.approx(30 * (15 / 1_000_000))
assert summary.by_model['claude-sonnet-4-6'].prompt_tokens == 2002
assert summary.by_model['claude-sonnet-4-6'].total_tokens == 2282
assert summary.by_model['claude-sonnet-4-6'].invocations == 2
def test_rust_terminal_usage_mixed_events_do_not_shrink_totals():
from browser_use.beta.service import _usage_from_events
summary = _usage_from_events(
[
{
'event_type': 'model.usage',
'payload': {
'usage': {
'input_tokens': 500,
'cached_input_tokens': 200,
'input_cache_creation_tokens': 50,
'output_tokens': 20,
'total_tokens': 570,
}
},
},
{
'event_type': 'token_count',
'payload': {
'info': {
'total_token_usage': {
'input_tokens': 10,
'cached_input_tokens': 5,
'output_tokens': 1,
'total_tokens': 11,
}
}
},
},
],
'claude-sonnet-4-6',
)
assert summary.total_prompt_tokens == 500
assert summary.total_prompt_cached_tokens == 200
assert summary.total_prompt_cache_creation_tokens == 50
assert summary.total_completion_tokens == 20
assert summary.total_tokens == 570
def test_rust_sdk_event_dedupe_removes_projected_usage_duplicates():
from browser_use.beta.service import _dedupe_sdk_events, _usage_from_events
usage_payload = {
'info': {
'last_token_usage': {
'input_tokens': 100,
'cached_input_tokens': 40,
'input_cache_creation_tokens': 10,
'output_tokens': 5,
'total_tokens': 115,
},
'total_token_usage': {
'input_tokens': 100,
'cached_input_tokens': 40,
'input_cache_creation_tokens': 10,
'output_tokens': 5,
'total_tokens': 115,
},
}
}
events = [
{'seq': 1, 'id': 'usage-id', 'event_type': 'token_count', 'payload': usage_payload},
{'seq': 1, 'event_type': 'token_count', 'payload': usage_payload},
{'seq': 2, 'event_type': 'session.done', 'payload': {'result': 'done'}},
{'seq': 2, 'id': 'done-id', 'event_type': 'session.done', 'payload': {'result': 'done'}},
]
deduped = _dedupe_sdk_events(events)
summary = _usage_from_events(deduped, 'claude-sonnet-4-6')
assert len(deduped) == 2
assert deduped[0]['id'] == 'usage-id'
assert deduped[1]['id'] == 'done-id'
assert summary.entry_count == 1
assert summary.total_prompt_tokens == 100
assert summary.total_prompt_cached_tokens == 40
assert summary.total_prompt_cache_creation_tokens == 10
assert summary.total_completion_tokens == 5
assert summary.total_tokens == 115
async def test_rust_terminal_priced_usage_prefers_model_usage_over_token_count(monkeypatch):
from browser_use.beta.service import _usage_from_events_with_costs
from browser_use.tokens.service import TokenCost
async def fail_fetch(_self):
raise AssertionError('custom model pricing should not fetch remote pricing')
monkeypatch.setattr(TokenCost, '_fetch_and_cache_pricing_data', fail_fetch)
summary = await _usage_from_events_with_costs(
[
{
'event_type': 'model.usage',
'payload': {
'input_tokens': 500,
'cached_input_tokens': 200,
'input_cache_creation_tokens': 50,
'output_tokens': 20,
'total_tokens': 570,
},
},
{
'event_type': 'token_count',
'payload': {
'info': {
'last_token_usage': {
'input_tokens': 500,
'cached_input_tokens': 200,
'input_cache_creation_tokens': 50,
'output_tokens': 20,
'total_tokens': 570,
},
'total_token_usage': {
'input_tokens': 500,
'cached_input_tokens': 200,
'input_cache_creation_tokens': 50,
'output_tokens': 20,
'total_tokens': 570,
},
}
},
},
],
'claude-sonnet-4-6',
TokenCost(include_cost=True),
)
assert summary.total_prompt_tokens == 500
assert summary.total_prompt_cached_tokens == 200
assert summary.total_prompt_cache_creation_tokens == 50
assert summary.total_completion_tokens == 20
assert summary.total_tokens == 570
assert summary.entry_count == 1
assert summary.total_prompt_cached_cost == pytest.approx(200 * (0.30 / 1_000_000))
assert summary.total_prompt_cache_creation_cost == pytest.approx(50 * (3.75 / 1_000_000))
assert summary.total_completion_cost == pytest.approx(20 * (15 / 1_000_000))
assert summary.total_cost == pytest.approx((300 * 3 + 200 * 0.30 + 50 * 3.75 + 20 * 15) / 1_000_000)
async def test_rust_token_summary_does_not_double_count_cache_reads(monkeypatch):
from browser_use.llm.views import ChatInvokeUsage
from browser_use.tokens.service import TokenCost
async def fail_fetch(_self):
raise AssertionError('custom model pricing should not fetch remote pricing')
monkeypatch.setattr(TokenCost, '_fetch_and_cache_pricing_data', fail_fetch)
token_cost = TokenCost(include_cost=True)
token_cost.add_usage(
'claude-sonnet-4-6',
ChatInvokeUsage(
prompt_tokens=110,
prompt_cached_tokens=100,
prompt_cache_creation_tokens=40,
prompt_image_tokens=None,
completion_tokens=20,
total_tokens=170,
),
)
summary = await token_cost.get_usage_summary()
assert summary.total_prompt_cost == pytest.approx(0.00021)
assert summary.total_prompt_cached_cost == pytest.approx(0.00003)
assert summary.total_prompt_cache_creation_tokens == 40
assert summary.total_prompt_cache_creation_cost == pytest.approx(0.00015)
assert summary.total_completion_cost == pytest.approx(0.0003)
assert summary.total_cost == pytest.approx(0.00051)
assert summary.by_model['claude-sonnet-4-6'].cost == pytest.approx(summary.total_cost)
def test_rust_history_supports_structured_output():
from browser_use.beta.service import _history_from_events
class Answer(BaseModel):
answer: str
history = _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': '{"answer": "ok"}'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=Answer,
process_error=None,
)
assert isinstance(history.structured_output, Answer)
assert history.structured_output.answer == 'ok'
def test_rust_history_extracts_fenced_structured_output():
from browser_use.beta.service import _history_from_events
class Answer(BaseModel):
answer: str
history = _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'Here is the answer:\n```json\n{"answer": "ok"}\n```'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=Answer,
process_error=None,
)
assert history.final_result() == '{"answer": "ok"}'
assert isinstance(history.structured_output, Answer)
assert history.structured_output.answer == 'ok'
def test_rust_history_reconstructs_terminal_agent_completed_result():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'event_type': 'agent.completed',
'payload': {
'child_session_id': 'child-1',
'status': 'done',
'payload': {'result': 'child answer\n'},
},
}
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
session_done_history = _history_from_events(
[
{
'event_type': 'agent.completed',
'payload': {
'child_session_id': 'child-1',
'status': 'done',
'payload': {'result': 'child answer'},
},
},
{'event_type': 'session.done', 'payload': {'result': 'parent answer'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.final_result() == 'child answer'
assert history.is_done() is True
assert history.action_names() == ['done']
assert history.last_action() == {'done': {'text': 'child answer', 'success': True}}
assert session_done_history.final_result() == 'parent answer'
def test_rust_history_exposes_result_file_attachments():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'event_type': 'session.done',
'payload': {
'result': 'Saved structured report.',
'result_file': {'url': 'file:///tmp/report.json', 'path': '/tmp/report.json', 'bytes': 123},
},
}
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
results = history.action_results()
assert history.final_result() == 'Saved structured report.'
assert results[0].attachments == ['file:///tmp/report.json']
def test_rust_history_reconstructs_terminal_artifact_attachments():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-artifacts',
'arguments': {'code': "write_file('report.csv', rows)"},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-artifacts',
'text': 'Generated report.csv',
'artifacts': [
{'path': '/tmp/report.csv', 'kind': 'file', 'mime': 'text/csv'},
{'path': '/tmp/screenshot.png', 'kind': 'image', 'mime_type': 'image/png'},
],
},
},
{
'type': 'artifact.created',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-artifacts',
'artifact': {'path': '/tmp/report.csv', 'kind': 'file', 'mime': 'text/csv'},
},
},
{
'type': 'tool.output_spilled',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-large',
'artifact': {'path': '/tmp/large-output.txt', 'original_tokens_estimate': 75000},
},
},
{
'event_type': 'session.done',
'payload': {
'result': 'Generated reports.',
'result_file': {'url': 'file:///tmp/final.json', 'path': '/tmp/final.json'},
},
},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
attachments = ['/tmp/report.csv', '/tmp/large-output.txt', 'file:///tmp/final.json']
assert history.final_result() == 'Generated reports.'
assert history.action_results()[-1].attachments == attachments
assert history.last_action() == {'done': {'text': 'Generated reports.', 'success': True, 'files_to_display': attachments}}
def test_rust_history_reconstructs_terminal_capture_curation_gif_attachment():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'event_type': 'capture.curation',
'payload': {
'source': 'fallback_uncurated',
'gif_path': '/tmp/browser-use-agent/capture-summary.gif',
},
},
{
'event_type': 'artifact.created',
'payload': {
'name': 'capture',
'artifact': {
'path': '/tmp/browser-use-agent/capture-summary.gif',
'kind': 'summary_gif',
'mime': 'image/gif',
},
},
},
{'event_type': 'session.done', 'payload': {'result': 'Generated browser summary.'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
attachments = ['/tmp/browser-use-agent/capture-summary.gif']
assert history.action_results()[-1].attachments == attachments
assert history.last_action() == {
'done': {'text': 'Generated browser summary.', 'success': True, 'files_to_display': attachments}
}
def test_rust_history_reconstructs_terminal_text_artifact_attachments():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{
'type': 'tool.started',
'payload': {
'name': 'python',
'arguments': {'code': "print('large report')"},
},
},
{
'type': 'tool.output',
'payload': {
'name': 'python',
'ok': True,
'text': 'large report...',
'text_truncated': True,
'text_artifact': {
'artifact_id': 'tool-output-python-1234',
'path': '/tmp/artifacts/session/tool-output-python-1234.txt',
'file_name': 'tool-output-python-1234.txt',
'bytes': 8192,
'preview': 'large report...',
'token_budget': 2048,
},
},
},
{'event_type': 'session.done', 'payload': {'result': 'Generated large report.'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
attachments = ['/tmp/artifacts/session/tool-output-python-1234.txt']
assert history.action_history()[0][0]['result'] == 'large report...'
assert history.action_results()[-1].attachments == attachments
assert history.last_action() == {
'done': {'text': 'Generated large report.', 'success': True, 'files_to_display': attachments}
}
def test_beta_agent_translates_browser_use_args_to_terminal(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
class Browser:
cdp_url = 'wss://browser.example/devtools/browser/1'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(
task='report title',
llm=LLM(),
browser_session=Browser(),
initial_actions=[{'go_to_url': {'url': 'https://example.com'}}],
)
env = agent._run_env()
server_argv = agent._sdk_server_argv()
assert server_argv == ['/tmp/browser-use-terminal', 'sdk-server', '--transport', 'stdio']
assert "First navigate to 'https://example.com'" in agent.task
assert env['BU_CDP_URL'] == 'wss://browser.example/devtools/browser/1'
assert env['LLM_BROWSER_BROWSER_MODE'] == 'remote-cdp'
assert env['BROWSER_USE_PYTHON'] == sys.executable
assert 'BUT_FULL_LLM_INPUT_EVENTS' not in env
params = agent._sdk_run_params(max_steps=12, task=agent.task)
assert params['max_steps'] == 12
assert params['browser_mode'] == 'remote-cdp'
assert params['llm'] == {'provider': 'browser-use', 'model': 'gpt-test', 'timeout': 75}
assert params['browser']['cdp_url'] == 'wss://browser.example/devtools/browser/1'
assert 'cdp_headers' not in params['browser']
assert params['config_overrides'] == {'full_llm_input_events': True}
assert "First navigate to 'https://example.com'" in params['task']
def test_rust_terminal_binary_missing_error_mentions_install(monkeypatch):
import browser_use.beta.service as beta_service
monkeypatch.delenv('BROWSER_USE_TERMINAL_BINARY', raising=False)
monkeypatch.setitem(sys.modules, 'browser_use_core', None)
monkeypatch.setattr(beta_service.Path, 'exists', lambda self: False)
monkeypatch.setattr(beta_service.shutil, 'which', lambda binary: None)
with pytest.raises(beta_service.BetaAgentError) as exc_info:
beta_service.find_browser_use_terminal_binary()
message = str(exc_info.value)
assert 'https://browser-use.com/terminal/install.sh' in message
assert 'browser-use-core' in message
assert 'BROWSER_USE_TERMINAL_BINARY' in message
def test_rust_terminal_binary_prefers_packaged_binary(monkeypatch):
import browser_use.beta.service as beta_service
class PackagedCore:
@staticmethod
def binary_path(binary_name):
assert binary_name == 'browser-use-terminal'
return '/tmp/packaged-browser-use-terminal'
monkeypatch.delenv('BROWSER_USE_TERMINAL_BINARY', raising=False)
monkeypatch.setitem(sys.modules, 'browser_use_core', PackagedCore)
monkeypatch.setattr(beta_service.Path, 'exists', lambda self: False)
monkeypatch.setattr(beta_service.shutil, 'which', lambda binary_name: None)
assert beta_service.find_browser_use_terminal_binary() == '/tmp/packaged-browser-use-terminal'
def test_beta_agent_run_env_sets_packaged_agent_tools_dir(monkeypatch, tmp_path):
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
package_dir = tmp_path / 'browser_use_core'
terminal = package_dir / 'bin' / 'browser-use-terminal'
agent_tools = package_dir / 'bin' / 'agent-tools'
terminal.parent.mkdir(parents=True)
agent_tools.mkdir(parents=True)
terminal.write_text('#!/bin/sh\n')
(agent_tools / 'rg').write_text('#!/bin/sh\n')
class PackagedCore:
@staticmethod
def agent_tools_dir():
return str(agent_tools)
@staticmethod
def binary_path(binary_name):
assert binary_name == 'browser-use-terminal'
return str(terminal)
monkeypatch.delenv('BROWSER_USE_TERMINAL_BINARY', raising=False)
monkeypatch.delenv('BUT_AGENT_TOOLS_DIR', raising=False)
monkeypatch.setenv('PATH', '/usr/bin')
monkeypatch.setitem(sys.modules, 'browser_use_core', PackagedCore)
agent = Agent(task='report title', llm=LLM())
env = agent._run_env()
assert env['BUT_AGENT_TOOLS_DIR'] == str(agent_tools)
assert env['PATH'].split(os.pathsep)[0] == str(agent_tools)
def test_beta_agent_run_env_preserves_explicit_agent_tools_dir(monkeypatch, tmp_path):
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
agent_tools = tmp_path / 'agent-tools'
agent_tools.mkdir()
(agent_tools / 'rg').write_text('#!/bin/sh\n')
monkeypatch.setenv('BUT_AGENT_TOOLS_DIR', str(agent_tools))
monkeypatch.setenv('PATH', '/usr/bin')
agent = Agent(task='report title', llm=LLM())
env = agent._run_env()
assert env['BUT_AGENT_TOOLS_DIR'] == str(agent_tools)
assert env['PATH'].split(os.pathsep)[0] == str(agent_tools)
def test_rust_terminal_binary_finds_default_terminal_install(monkeypatch, tmp_path):
import browser_use.beta.service as beta_service
binary = tmp_path / '.browser-use-terminal' / 'packages' / 'standalone' / 'current' / 'bin' / 'browser-use-terminal'
binary.parent.mkdir(parents=True)
binary.write_text('#!/bin/sh\nif [ "$1" = "--help" ]; then\n echo "sdk-server"\nfi\n')
binary.chmod(0o755)
monkeypatch.delenv('BROWSER_USE_TERMINAL_BINARY', raising=False)
monkeypatch.setitem(sys.modules, 'browser_use_core', None)
monkeypatch.chdir(tmp_path)
monkeypatch.setenv('BUT_HOME', str(tmp_path / '.browser-use-terminal'))
monkeypatch.setenv('BUT_INSTALL_DIR', str(tmp_path / '.local' / 'bin'))
monkeypatch.setattr(beta_service.shutil, 'which', lambda binary_name: None)
assert beta_service.find_browser_use_terminal_binary() == str(binary)
def test_rust_terminal_binary_from_installed_browser_use_core_answers_ping(monkeypatch):
browser_use_core = pytest.importorskip('browser_use_core')
import browser_use.beta.service as beta_service
monkeypatch.delenv('BROWSER_USE_TERMINAL_BINARY', raising=False)
binary = beta_service.find_browser_use_terminal_binary()
assert binary == browser_use_core.binary_path('browser-use-terminal')
proc = subprocess.run(
[binary, 'sdk-server', '--transport', 'stdio'],
input='{"jsonrpc":"2.0","id":1,"method":"runtime.ping","params":{}}\n',
text=True,
capture_output=True,
timeout=10,
check=True,
)
response = json.loads(proc.stdout.strip())
assert response['result']['ok'] is True
assert response['result']['sdk_protocol_version'] == 1
async def test_rust_sdk_client_start_wraps_missing_binary_error():
import browser_use.beta.service as beta_service
client = beta_service.RustSdkClient(['/definitely/missing-browser-use-terminal'], env={})
with pytest.raises(beta_service.BetaAgentError) as exc_info:
await client.start()
message = str(exc_info.value)
assert '/definitely/missing-browser-use-terminal' in message
assert 'https://browser-use.com/terminal/install.sh' in message
def test_beta_agent_sdk_params_leave_terminal_tools_unrestricted(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='use the best available terminal tools', llm=LLM(), directly_open_url=False)
params = agent._sdk_run_params(max_steps=8, task=agent.task)
assert params['config_overrides'] == {'full_llm_input_events': True}
assert 'tool_allowlist' not in json.dumps(params)
def test_beta_agent_sdk_browser_payload_includes_profile_domains_window_and_proxy(monkeypatch, tmp_path):
from browser_use.beta import Agent
class BrowserProfile:
cdp_url = 'http://127.0.0.1:9222'
allowed_domains = ['example.com']
prohibited_domains = ['*.tracking.example']
window_size = {'width': 1440, 'height': 900}
proxy_country_code = 'DE'
state_dir = tmp_path
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', llm=type('LLM', (), {'model': 'gpt-test'})(), browser_profile=BrowserProfile())
params = agent._sdk_run_params(max_steps=4, task=agent.task)
assert params['browser_mode'] == 'remote-cdp'
assert params['browser']['cdp_url'] == 'http://127.0.0.1:9222'
assert params['browser']['allowed_domains'] == ['example.com']
assert params['browser']['blocked_domains'] == ['*.tracking.example']
assert params['browser']['window_size'] == {'width': 1440, 'height': 900}
assert params['browser']['proxy_country_code'] == 'DE'
assert params['browser']['state_dir'] == str(tmp_path)
async def test_beta_agent_runs_through_sdk_and_reuses_session_for_followup(monkeypatch):
from browser_use.beta import Agent, BrowserProfile, BrowserSession
class LLM:
model = 'fake'
class FakeSdk:
def __init__(self):
self.calls = []
self.stderr_lines = []
async def call(self, method, params):
self.calls.append((method, params))
if method == 'agent.run_task':
return {
'agent_id': 'agent-1',
'session_id': 'session-1',
'browser_id': 'browser-1',
'history': {
'output': 'first done',
'success': True,
'done': True,
'errors': [],
'events': [
{'event_type': 'session.input', 'payload': {'text': params['task']}},
{'event_type': 'session.done', 'payload': {'result': 'first done', 'success': True}},
],
},
}
if method == 'agent.run':
return {
'agent_id': params['agent_id'],
'session_id': 'session-1',
'browser_id': params['browser_id'],
'history': {
'output': 'followup done',
'success': True,
'done': True,
'errors': [],
'events': [
{'event_type': 'session.followup', 'payload': {'text': params['followups'][0]}},
{'event_type': 'session.done', 'payload': {'result': 'followup done', 'success': True}},
],
},
}
raise AssertionError(f'unexpected SDK method {method}')
fake_sdk = FakeSdk()
async def fake_ensure_sdk_client(self):
self._sdk_client = fake_sdk
return fake_sdk
monkeypatch.setattr(Agent, '_ensure_sdk_client', fake_ensure_sdk_client)
browser_session = BrowserSession(browser_profile=BrowserProfile(keep_alive=True), id='browser-session-1')
agent = Agent(task='answer first', llm=LLM(), browser_session=browser_session, directly_open_url=False)
first = await agent.run(max_steps=7)
followup = await agent.follow_up('answer second', max_steps=5)
assert first.final_result() == 'first done'
assert followup.final_result() == 'followup done'
assert fake_sdk.calls[0][0] == 'agent.run_task'
assert fake_sdk.calls[0][1]['task'] == 'answer first'
assert fake_sdk.calls[0][1]['max_steps'] == 7
assert fake_sdk.calls[1][0] == 'agent.run'
assert fake_sdk.calls[1][1]['agent_id'] == 'agent-1'
assert fake_sdk.calls[1][1]['browser_id'] == 'browser-1'
assert fake_sdk.calls[1][1]['followups'] == ['answer second']
assert fake_sdk.calls[1][1]['max_steps'] == 5
async def test_beta_agent_prices_sdk_child_usage_events_without_overriding_parent_result(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'fake'
class FakeSdk:
stderr_lines = []
notifications = []
async def call(self, method, params):
assert method == 'agent.run_task'
parent_events = [
{'event_type': 'model.usage', 'payload': {'input_tokens': 7000, 'output_tokens': 20}},
{'event_type': 'session.done', 'payload': {'result': 'parent answer', 'success': True}},
]
child_events = [
{
'event_type': 'model.usage',
'session_id': 'child-1',
'payload': {'input_tokens': 3000, 'cached_input_tokens': 1000, 'output_tokens': 10},
},
{'event_type': 'session.done', 'session_id': 'child-1', 'payload': {'result': 'child answer'}},
]
return {
'agent_id': 'agent-1',
'session_id': 'parent-1',
'browser_id': 'browser-1',
'history': {
'output': 'parent answer',
'success': True,
'done': True,
'errors': [],
'events': parent_events,
'child_events': child_events,
'usage_events': [*parent_events, *child_events],
},
}
fake_sdk = FakeSdk()
async def fake_ensure_sdk_client(self):
return fake_sdk
monkeypatch.setattr(Agent, '_ensure_sdk_client', fake_ensure_sdk_client)
agent = Agent(task='parent task', llm=LLM(), directly_open_url=False)
history = await agent.run(max_steps=3)
assert history.final_result() == 'parent answer'
assert agent.last_events[-1]['payload']['result'] == 'parent answer'
assert agent.last_child_events[-1]['payload']['result'] == 'child answer'
assert history.usage is not None
assert history.usage.total_prompt_tokens == 10_000
assert history.usage.total_prompt_cached_tokens == 1000
assert history.usage.total_completion_tokens == 30
assert history.usage.entry_count == 2
async def test_beta_agent_recovers_final_result_from_sdk_notifications_after_transport_error(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'fake'
class FakeSdk:
stderr_lines = []
def __init__(self):
self.notifications = [
{
'method': 'agent.event',
'params': {'event': {'seq': 1, 'id': 'event-1', 'event_type': 'session.input', 'payload': {'text': 'task'}}},
},
{
'method': 'agent.event',
'params': {
'event': {
'seq': 2,
'id': 'event-2',
'event_type': 'session.done',
'payload': {'result': 'final from notification', 'success': True},
}
},
},
]
async def call(self, method, params):
raise RuntimeError('Rust SDK JSON-RPC line exceeded 536870912 bytes without newline')
fake_sdk = FakeSdk()
async def fake_ensure_sdk_client(self):
return fake_sdk
monkeypatch.setattr(Agent, '_ensure_sdk_client', fake_ensure_sdk_client)
agent = Agent(task='task', llm=LLM(), directly_open_url=False)
history = await agent.run(max_steps=3)
assert history.final_result() == 'final from notification'
assert history.is_successful() is True
assert [event['event_type'] for event in agent.last_events] == ['session.input', 'session.done']
async def test_beta_agent_recovers_nested_sdk_notification_events(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'fake'
class FakeSdk:
stderr_lines = []
def __init__(self):
self.notifications = [
{
'method': 'agent.event',
'params': {
'event': {
'kind': 'observed',
'payload': {
'seq': 1,
'id': 'event-1',
'event_type': 'session.input',
'payload': {'text': 'task'},
},
}
},
},
{
'method': 'agent.event',
'params': {
'event': {
'kind': 'observed',
'payload': {
'seq': 2,
'id': 'event-2',
'event_type': 'session.done',
'payload': {'result': 'final from nested notification', 'success': True},
},
}
},
},
]
async def call(self, method, params):
return {'history': {'success': True, 'done': True, 'events': []}}
fake_sdk = FakeSdk()
async def fake_ensure_sdk_client(self):
return fake_sdk
monkeypatch.setattr(Agent, '_ensure_sdk_client', fake_ensure_sdk_client)
agent = Agent(task='task', llm=LLM(), directly_open_url=False)
history = await agent.run(max_steps=3)
assert history.final_result() == 'final from nested notification'
assert history.is_successful() is True
assert [event['event_type'] for event in agent.last_events] == ['session.input', 'session.done']
async def test_beta_agent_recovers_projected_sdk_final_events(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'fake'
class FakeSdk:
stderr_lines = []
def __init__(self):
self.notifications = [
{
'method': 'agent.projected_event',
'params': {
'event': {
'kind': 'observed',
'payload': {
'event_type': 'session.input',
'payload': {'text': 'task'},
},
}
},
},
{
'method': 'agent.projected_event',
'params': {
'event': {
'kind': 'observed',
'payload': {
'event_type': 'session.done',
'payload': {'result': 'final from projected notification', 'success': True},
},
}
},
},
]
async def call(self, method, params):
return {'history': {'success': True, 'done': True, 'events': []}}
fake_sdk = FakeSdk()
async def fake_ensure_sdk_client(self):
return fake_sdk
monkeypatch.setattr(Agent, '_ensure_sdk_client', fake_ensure_sdk_client)
agent = Agent(task='task', llm=LLM(), directly_open_url=False)
history = await agent.run(max_steps=3)
assert history.final_result() == 'final from projected notification'
assert history.is_successful() is True
assert [event['event_type'] for event in agent.last_events] == ['session.input', 'session.done']
async def test_beta_agent_prefers_notification_final_when_response_history_lacks_result(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'fake'
def notification(seq: int, event_type: str, payload: dict):
return {
'method': 'agent.event',
'params': {
'event': {
'seq': seq,
'id': f'event-{seq}',
'event_type': event_type,
'payload': payload,
}
},
}
class FakeSdk:
stderr_lines = []
def __init__(self):
self.notifications = [
notification(1, 'session.input', {'text': 'task'}),
notification(
2,
'token_count',
{
'info': {
'last_token_usage': {
'input_tokens': 100,
'cached_input_tokens': 40,
'output_tokens': 10,
'total_tokens': 110,
},
'total_token_usage': {
'input_tokens': 100,
'cached_input_tokens': 40,
'output_tokens': 10,
'total_tokens': 110,
},
}
},
),
notification(
3,
'token_count',
{
'info': {
'last_token_usage': {
'input_tokens': 200,
'cached_input_tokens': 60,
'output_tokens': 20,
'total_tokens': 220,
},
'total_token_usage': {
'input_tokens': 300,
'cached_input_tokens': 100,
'output_tokens': 30,
'total_tokens': 330,
},
}
},
),
notification(4, 'session.done', {'result': 'final from retained notifications', 'success': True}),
]
async def call(self, method, params):
return {
'history': {
'success': False,
'done': True,
'errors': ['compacted response did not include final tail'],
'events': [
{'event_type': 'session.input', 'payload': {'text': 'task'}},
{'event_type': 'browser.navigated', 'payload': {'url': 'https://example.com'}},
{'event_type': 'sdk.transport.truncated', 'payload': {'omitted_events': 100}},
{'event_type': 'browser.released', 'payload': {}},
{'event_type': 'agent.turn.completed', 'payload': {}},
],
'usage_events': [],
}
}
fake_sdk = FakeSdk()
async def fake_ensure_sdk_client(self):
return fake_sdk
monkeypatch.setattr(Agent, '_ensure_sdk_client', fake_ensure_sdk_client)
agent = Agent(task='task', llm=LLM(), directly_open_url=False)
history = await agent.run(max_steps=3)
assert history.final_result() == 'final from retained notifications'
assert history.is_successful() is True
assert history.usage is not None
assert history.usage.total_prompt_tokens == 300
assert history.usage.total_prompt_cached_tokens == 100
assert history.usage.total_completion_tokens == 30
assert history.usage.total_tokens == 330
assert history.usage.entry_count == 2
assert [event['event_type'] for event in agent.last_events] == [
'session.input',
'token_count',
'token_count',
'session.done',
]
async def test_beta_agent_uses_sdk_history_usage_when_events_do_not_include_usage(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'fake'
class FakeSdk:
stderr_lines = []
notifications = []
async def call(self, method, params):
return {
'history': {
'success': True,
'done': True,
'errors': [],
'events': [{'event_type': 'session.done', 'payload': {'result': 'answer', 'success': True}}],
'usage_events': [],
'usage': {
'input_tokens': 120,
'cached_input_tokens': 30,
'input_cache_creation_tokens': 10,
'output_tokens': 15,
'total_tokens': 145,
},
}
}
fake_sdk = FakeSdk()
async def fake_ensure_sdk_client(self):
return fake_sdk
monkeypatch.setattr(Agent, '_ensure_sdk_client', fake_ensure_sdk_client)
agent = Agent(task='task', llm=LLM(), directly_open_url=False)
history = await agent.run(max_steps=3)
assert history.final_result() == 'answer'
assert history.usage is not None
assert history.usage.total_prompt_tokens == 120
assert history.usage.total_prompt_cached_tokens == 30
assert history.usage.total_prompt_cache_creation_tokens == 10
assert history.usage.total_completion_tokens == 15
assert history.usage.total_tokens == 145
async def test_beta_agent_preserves_sdk_notification_history_on_cancel(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'fake'
class FakeSdk:
stderr_lines = []
def __init__(self):
self.notifications = [
{
'method': 'agent.event',
'params': {'event': {'seq': 1, 'id': 'event-1', 'event_type': 'session.input', 'payload': {'text': 'task'}}},
},
{
'method': 'agent.event',
'params': {
'event': {
'seq': 2,
'id': 'event-2',
'event_type': 'tool.output',
'payload': {'name': 'browser_script', 'output': 'partial evidence'},
}
},
},
]
async def call(self, method, params):
raise asyncio.CancelledError
fake_sdk = FakeSdk()
async def fake_ensure_sdk_client(self):
return fake_sdk
monkeypatch.setattr(Agent, '_ensure_sdk_client', fake_ensure_sdk_client)
agent = Agent(task='task', llm=LLM(), directly_open_url=False)
with pytest.raises(asyncio.CancelledError):
await agent.run(max_steps=3)
assert [event['event_type'] for event in agent.last_events] == ['session.input', 'tool.output']
assert any('CancelledError' in error for error in agent.history.errors())
async def test_rust_sdk_client_reads_large_json_rpc_lines(monkeypatch):
import browser_use.beta.service as beta_service
monkeypatch.setenv('BROWSER_USE_SDK_STREAM_LIMIT_BYTES', '4096')
monkeypatch.setenv('BROWSER_USE_SDK_READ_CHUNK_BYTES', '1024')
script = """
import json
import sys
sys.stdin.readline()
print(json.dumps({"jsonrpc": "2.0", "id": 1, "result": {"text": "x" * 70000}}), flush=True)
"""
client = beta_service.RustSdkClient([sys.executable, '-c', script], {'PYTHONUNBUFFERED': '1'})
try:
result = await client.call('large.response')
finally:
await client.close()
assert result == {'text': 'x' * 70000}
async def test_rust_sdk_client_queues_agent_notifications_before_response():
import browser_use.beta.service as beta_service
script = r"""
import json
import sys
sys.stdin.readline()
print(json.dumps({
"jsonrpc": "2.0",
"method": "agent.event",
"params": {
"run_id": "run-1",
"session_id": "session-1",
"event": {"kind": "AgentStarted", "payload": {"task": "inspect"}}
},
}), flush=True)
print(json.dumps({
"jsonrpc": "2.0",
"method": "agent.projected_event",
"params": {
"run_id": "run-1",
"session_id": "session-1",
"event": {"kind": "item_started", "payload": {"name": "browser_script"}}
},
}), flush=True)
print(json.dumps({"jsonrpc": "2.0", "id": 1, "result": {"ok": True}}), flush=True)
"""
client = beta_service.RustSdkClient([sys.executable, '-c', script], {'PYTHONUNBUFFERED': '1'})
try:
result = await client.call('run.with.notifications')
first_notification = await client.notification_queue.get()
second_notification = await client.notification_queue.get()
finally:
await client.close()
assert result == {'ok': True}
assert [item['method'] for item in client.notifications] == ['agent.event', 'agent.projected_event']
assert first_notification['params']['event']['kind'] == 'AgentStarted'
assert second_notification['params']['event']['kind'] == 'item_started'
assert beta_service._sdk_notification_summary(first_notification) == 'AgentStarted task=inspect'
assert beta_service._sdk_notification_summary(second_notification) == 'projected.item_started name=browser_script'
async def test_beta_agent_rejects_incompatible_sdk_protocol(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
script = r"""
import json
import sys
request = json.loads(sys.stdin.readline())
print(json.dumps({"jsonrpc": "2.0", "id": request["id"], "result": {"ok": True}}), flush=True)
"""
agent = Agent(task='answer', llm=LLM())
monkeypatch.setattr(agent, '_sdk_server_argv', lambda: [sys.executable, '-c', script])
with pytest.raises(beta_service.BetaAgentError) as exc_info:
await agent._ensure_sdk_client()
assert 'Unsupported browser-use-terminal SDK protocol' in str(exc_info.value)
async def test_beta_agent_clears_sdk_client_when_protocol_ping_fails(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
script = r"""
import json
import sys
request = json.loads(sys.stdin.readline())
print(json.dumps({
"jsonrpc": "2.0",
"id": request["id"],
"error": {"code": -32000, "message": "ping failed"},
}), flush=True)
"""
agent = Agent(task='answer', llm=LLM())
monkeypatch.setattr(agent, '_sdk_server_argv', lambda: [sys.executable, '-c', script])
with pytest.raises(beta_service.BetaAgentError) as exc_info:
await agent._ensure_sdk_client()
assert 'Failed to negotiate browser-use-terminal SDK protocol via runtime.ping' in str(exc_info.value)
assert agent._sdk_client is None
def test_beta_agent_bridges_llm_credentials_to_terminal_env(monkeypatch):
from browser_use.beta import Agent
class LLM:
def __init__(self, provider, model, api_key=None, base_url=None):
self.provider = provider
self.model = model
self.api_key = api_key
self.base_url = base_url
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
for key in (
'LLM_BROWSER_OPENAI_BASE_URL',
'LLM_BROWSER_ANTHROPIC_BASE_URL',
'LLM_BROWSER_OPENAI_COMPAT_API_KEY',
'LLM_BROWSER_OPENAI_COMPAT_BASE_URL',
'OPENROUTER_API_KEY',
'OPENROUTER_BASE_URL',
'DEEPSEEK_API_KEY',
'LLM_BROWSER_BROWSER_USE_API_KEY',
'LLM_BROWSER_BROWSER_USE_BASE_URL',
):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv('LLM_BROWSER_OPENAI_API_KEY', 'ambient-openai-key')
monkeypatch.setenv('LLM_BROWSER_ANTHROPIC_API_KEY', 'ambient-anthropic-key')
openai_env = Agent(
task='OpenAI credentials.',
llm=LLM('openai', 'gpt-test', api_key='llm-openai-key', base_url='https://openai.example/v1'),
)._run_env()
openai_agent = Agent(
task='OpenAI command.',
llm=LLM('openai', 'gpt-test', api_key='llm-openai-key'),
)
anthropic_agent = Agent(
task='Anthropic credentials.',
llm=LLM('anthropic', 'claude-test', api_key='llm-anthropic-key', base_url='https://anthropic.example'),
)
openrouter_agent = Agent(
task='OpenRouter credentials.',
llm=LLM('openrouter', 'openrouter/model', api_key='llm-openrouter-key', base_url='https://openrouter.example/api/v1'),
)
deepseek_agent = Agent(
task='DeepSeek credentials.',
llm=LLM('deepseek', 'deepseek-chat', api_key='llm-deepseek-key', base_url='https://ignored.example'),
)
browser_use_agent = Agent(
task='Browser Use credentials.',
llm=LLM('browser-use', 'bu-2-0', api_key='llm-browser-use-key', base_url='https://llm.example/v1'),
)
ambient_env = Agent(
task='Ambient credentials.',
llm=LLM('anthropic', 'claude-test'),
)._run_env()
anthropic_env = anthropic_agent._run_env()
openrouter_env = openrouter_agent._run_env()
deepseek_env = deepseek_agent._run_env()
browser_use_env = browser_use_agent._run_env()
assert openai_env['LLM_BROWSER_OPENAI_API_KEY'] == 'llm-openai-key'
assert openai_env['LLM_BROWSER_OPENAI_BASE_URL'] == 'https://openai.example/v1'
assert anthropic_env['LLM_BROWSER_ANTHROPIC_API_KEY'] == 'llm-anthropic-key'
assert anthropic_env['LLM_BROWSER_ANTHROPIC_BASE_URL'] == 'https://anthropic.example'
assert openrouter_env['OPENROUTER_API_KEY'] == 'llm-openrouter-key'
assert openrouter_env['OPENROUTER_BASE_URL'] == 'https://openrouter.example/api/v1'
assert deepseek_env['DEEPSEEK_API_KEY'] == 'llm-deepseek-key'
assert 'LLM_BROWSER_OPENAI_COMPAT_BASE_URL' not in deepseek_env
assert browser_use_env['LLM_BROWSER_BROWSER_USE_API_KEY'] == 'llm-browser-use-key'
assert browser_use_env['LLM_BROWSER_BROWSER_USE_BASE_URL'] == 'https://llm.example'
assert ambient_env['LLM_BROWSER_ANTHROPIC_API_KEY'] == 'ambient-anthropic-key'
assert openai_agent._sdk_run_params(max_steps=3, task=openai_agent.task)['llm'] | {'timeout': None} == {
'provider': 'openai',
'model': 'gpt-test',
'timeout': None,
}
assert anthropic_agent._sdk_run_params(max_steps=3, task=anthropic_agent.task)['llm'] | {'timeout': None} == {
'provider': 'anthropic',
'model': 'claude-test',
'timeout': None,
}
assert openrouter_agent._sdk_run_params(max_steps=3, task=openrouter_agent.task)['llm'] | {'timeout': None} == {
'provider': 'openrouter',
'model': 'openrouter/model',
'timeout': None,
}
assert deepseek_agent._sdk_run_params(max_steps=3, task=deepseek_agent.task)['llm'] | {'timeout': None} == {
'provider': 'deepseek',
'model': 'deepseek-chat',
'timeout': None,
}
assert browser_use_agent._sdk_run_params(max_steps=3, task=browser_use_agent.task)['llm'] | {'timeout': None} == {
'provider': 'browser-use',
'model': 'bu-2-0',
'timeout': None,
}
def test_beta_agent_requests_openai_compatible_usage_for_cost_calculation(monkeypatch):
from browser_use.beta import Agent
class LLM:
def __init__(self, provider, model):
self.provider = provider
self.model = model
self.api_key = 'test-key'
monkeypatch.delenv('LLM_BROWSER_OPENAI_COMPAT_INCLUDE_USAGE', raising=False)
monkeypatch.delenv('BU_USE_CALCULATE_COST', raising=False)
openrouter_env = Agent(
task='Track OpenRouter usage.',
llm=LLM('openrouter', 'openrouter/model'),
calculate_cost=True,
)._run_env()
deepseek_env = Agent(
task='Track DeepSeek usage.',
llm=LLM('deepseek', 'deepseek-chat'),
calculate_cost=True,
)._run_env()
default_env = Agent(
task='Default OpenRouter usage.',
llm=LLM('openrouter', 'openrouter/model'),
)._run_env()
openai_env = Agent(
task='OpenAI usage.',
llm=LLM('openai', 'gpt-test'),
calculate_cost=True,
)._run_env()
assert openrouter_env['BU_USE_CALCULATE_COST'] == 'true'
assert openrouter_env['LLM_BROWSER_OPENAI_COMPAT_INCLUDE_USAGE'] == 'true'
assert deepseek_env['BU_USE_CALCULATE_COST'] == 'true'
assert deepseek_env['LLM_BROWSER_OPENAI_COMPAT_INCLUDE_USAGE'] == 'true'
assert 'LLM_BROWSER_OPENAI_COMPAT_INCLUDE_USAGE' not in default_env
assert 'LLM_BROWSER_OPENAI_COMPAT_INCLUDE_USAGE' not in openai_env
def test_beta_agent_translates_browser_profile_cdp_url(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
cdp_url = 'http://127.0.0.1:9222'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', llm=type('LLM', (), {'model': 'gpt-test'})(), browser_profile=BrowserProfile())
env = agent._run_env()
assert agent._sdk_run_params(max_steps=4, task=agent.task)['browser_mode'] == 'remote-cdp'
assert env['BU_CDP_URL'] == 'http://127.0.0.1:9222'
assert env['LLM_BROWSER_BROWSER_MODE'] == 'remote-cdp'
def test_beta_agent_translates_browser_profile_cdp_headers(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
cdp_url = 'http://127.0.0.1:9222'
headers = {'Authorization': 'Bearer test-token', 'X-Browser-Use': 'rust', 'Retries': 3}
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['LLM_BROWSER_BROWSER_MODE'] == 'remote-cdp'
assert agent.cdp_headers == {
'Authorization': 'Bearer test-token',
'X-Browser-Use': 'rust',
'Retries': '3',
}
assert json.loads(env['BU_CDP_HEADERS']) == agent.cdp_headers
def test_beta_agent_translates_browser_profile_remote_user_agent(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
cdp_url = 'http://127.0.0.1:9222'
user_agent = 'BrowserUseRemote/2.0'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['LLM_BROWSER_BROWSER_MODE'] == 'remote-cdp'
assert agent.browser_user_agent == 'BrowserUseRemote/2.0'
assert env['BU_BROWSER_USER_AGENT'] == 'BrowserUseRemote/2.0'
def test_beta_agent_translates_browser_profile_highlights(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
highlight_elements = True
dom_highlight_elements = False
interaction_highlight_color = 'rgb(255, 127, 39)'
interaction_highlight_duration = 1.75
cdp_url = 'http://127.0.0.1:9222'
class DomHighlightProfile:
highlight_elements = True
dom_highlight_elements = True
cdp_url = 'http://127.0.0.1:9222'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['BROWSER_USE_TERMINAL_AUTO_HIGHLIGHT'] == 'true'
assert env['BROWSER_USE_TERMINAL_HIGHLIGHT_COLOR'] == 'rgb(255, 127, 39)'
assert env['BROWSER_USE_TERMINAL_HIGHLIGHT_DURATION_MS'] == '1750'
dom_agent = Agent(task='report title', browser_profile=DomHighlightProfile())
dom_env = dom_agent._run_env()
assert dom_agent.highlight_enabled is False
assert dom_env['BROWSER_USE_TERMINAL_AUTO_HIGHLIGHT'] == 'false'
def test_beta_agent_translates_browser_profile_wait_timings(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
minimum_wait_page_load_time = 0.25
wait_for_network_idle_page_load_time = 0.75
wait_between_actions = 0.125
cdp_url = 'http://127.0.0.1:9222'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert agent.wait_timing_env == {
'BU_BROWSER_MINIMUM_WAIT_PAGE_LOAD_MS': '250',
'BU_BROWSER_NETWORK_IDLE_PAGE_LOAD_MS': '750',
'BU_BROWSER_WAIT_BETWEEN_ACTIONS_MS': '125',
}
assert env['BU_BROWSER_MINIMUM_WAIT_PAGE_LOAD_MS'] == '250'
assert env['BU_BROWSER_NETWORK_IDLE_PAGE_LOAD_MS'] == '750'
assert env['BU_BROWSER_WAIT_BETWEEN_ACTIONS_MS'] == '125'
def test_beta_agent_translates_browser_profile_block_ip_addresses(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
block_ip_addresses = True
cdp_url = 'http://127.0.0.1:9222'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert agent.block_ip_addresses is True
assert env['BU_BROWSER_BLOCK_IP_ADDRESSES'] == 'true'
def test_beta_agent_translates_browser_profile_headless(monkeypatch):
from browser_use.beta import Agent
class HeadedProfile:
headless = False
class HeadlessProfile:
headless = True
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
headed = Agent(task='report title', llm=type('LLM', (), {'model': 'gpt-test'})(), browser_profile=HeadedProfile())
headless = Agent(task='report title', llm=type('LLM', (), {'model': 'gpt-test'})(), browser_profile=HeadlessProfile())
assert headed._sdk_run_params(max_steps=4, task=headed.task)['browser_mode'] == 'managed-headed'
assert headed._run_env()['LLM_BROWSER_BROWSER_MODE'] == 'managed-headed'
assert headless._sdk_run_params(max_steps=4, task=headless.task)['browser_mode'] == 'managed-headless'
assert headless._run_env()['LLM_BROWSER_BROWSER_MODE'] == 'managed-headless'
def test_beta_agent_browser_mode_env_overrides_profile_headless(monkeypatch):
from browser_use.beta import Agent
class HeadedProfile:
headless = False
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.setenv('BROWSER_USE_BROWSER_MODE', 'cloud')
agent = Agent(task='report title', llm=type('LLM', (), {'model': 'gpt-test'})(), browser_profile=HeadedProfile())
assert agent._sdk_run_params(max_steps=4, task=agent.task)['browser_mode'] == 'cloud'
assert agent._run_env()['LLM_BROWSER_BROWSER_MODE'] == 'cloud'
def test_beta_agent_translates_browser_profile_cloud(monkeypatch):
from browser_use.beta import Agent
class CloudProfile:
use_cloud = True
headless = False
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
agent = Agent(task='report title', llm=type('LLM', (), {'model': 'gpt-test'})(), browser_profile=CloudProfile())
assert agent._sdk_run_params(max_steps=4, task=agent.task)['browser_mode'] == 'cloud'
assert agent._run_env()['LLM_BROWSER_BROWSER_MODE'] == 'cloud'
def test_beta_agent_translates_browser_profile_managed_launch_args(monkeypatch):
from browser_use.beta import Agent
class Proxy:
server = 'http://proxy.example:8080'
bypass = 'localhost,127.0.0.1'
class BrowserProfile:
headless = True
args = ['--lang=en-US']
disable_security = True
proxy = Proxy()
window_size = {'width': 1440, 'height': 900}
user_agent = 'BrowserUseTest/1.0'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
launch_args = json.loads(env['BU_MANAGED_BROWSER_ARGS'])
assert env['LLM_BROWSER_BROWSER_MODE'] == 'managed-headless'
assert agent.managed_browser_args == launch_args
assert agent.browser_user_agent == 'BrowserUseTest/1.0'
assert env['BU_BROWSER_USER_AGENT'] == 'BrowserUseTest/1.0'
assert '--lang=en-US' in launch_args
assert '--window-size=1440,900' in launch_args
assert '--proxy-server=http://proxy.example:8080' in launch_args
assert '--proxy-bypass-list=localhost,127.0.0.1' in launch_args
assert '--disable-web-security' in launch_args
assert '--user-agent=BrowserUseTest/1.0' in launch_args
def test_beta_agent_translates_browser_profile_chromium_sandbox(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
headless = True
chromium_sandbox = False
args = ['--no-sandbox']
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
agent = Agent(task='report title', browser_profile=BrowserProfile())
launch_args = json.loads(agent._run_env()['BU_MANAGED_BROWSER_ARGS'])
assert launch_args.count('--no-sandbox') == 1
assert '--disable-gpu-sandbox' in launch_args
assert '--disable-setuid-sandbox' in launch_args
assert '--disable-dev-shm-usage' in launch_args
def test_beta_agent_translates_browser_profile_window_position(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
headless = False
window_position = {'width': 32, 'height': 64}
class TuplePositionProfile:
headless = False
window_position = (12, 24)
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
agent = Agent(task='report title', browser_profile=BrowserProfile())
tuple_agent = Agent(task='report title', browser_profile=TuplePositionProfile())
assert '--window-position=32,64' in json.loads(agent._run_env()['BU_MANAGED_BROWSER_ARGS'])
assert '--window-position=12,24' in json.loads(tuple_agent._run_env()['BU_MANAGED_BROWSER_ARGS'])
def test_beta_agent_translates_browser_profile_devtools(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
headless = False
devtools = True
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
launch_args = json.loads(env['BU_MANAGED_BROWSER_ARGS'])
assert env['LLM_BROWSER_BROWSER_MODE'] == 'managed-headed'
assert '--auto-open-devtools-for-tabs' in launch_args
def test_beta_agent_translates_browser_profile_profile_directory(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
headless = False
profile_directory = 'Profile 7'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
agent = Agent(task='report title', browser_profile=BrowserProfile())
launch_args = json.loads(agent._run_env()['BU_MANAGED_BROWSER_ARGS'])
assert '--profile-directory=Profile 7' in launch_args
def test_beta_agent_translates_browser_profile_permissions(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
permissions = ['clipboardReadWrite', 'notifications', 'clipboardReadWrite']
cdp_url = 'http://127.0.0.1:9222'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['LLM_BROWSER_BROWSER_MODE'] == 'remote-cdp'
assert agent.browser_permissions == ['clipboardReadWrite', 'notifications']
assert json.loads(env['BU_BROWSER_PERMISSIONS']) == ['clipboardReadWrite', 'notifications']
def test_beta_agent_translates_browser_profile_downloads(monkeypatch, tmp_path):
from browser_use.beta import Agent
profile_downloads_path = tmp_path / 'downloads'
class BrowserProfile:
accept_downloads = True
downloads_path = profile_downloads_path
cdp_url = 'http://127.0.0.1:9222'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['LLM_BROWSER_BROWSER_MODE'] == 'remote-cdp'
assert agent.browser_accept_downloads is True
assert agent.browser_downloads_path == str(profile_downloads_path)
assert env['BU_BROWSER_ACCEPT_DOWNLOADS'] == 'true'
assert env['BU_BROWSER_DOWNLOADS_PATH'] == str(profile_downloads_path)
def test_beta_agent_translates_browser_profile_viewport(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
viewport = {'width': 1024, 'height': 768}
screen = (1440, 900)
device_scale_factor = 2
no_viewport = False
cdp_url = 'http://127.0.0.1:9222'
class NoViewportProfile:
viewport = {'width': 1024, 'height': 768}
no_viewport = True
cdp_url = 'http://127.0.0.1:9222'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
viewport = json.loads(env['BU_BROWSER_VIEWPORT'])
assert env['LLM_BROWSER_BROWSER_MODE'] == 'remote-cdp'
assert env['BU_BROWSER_NO_VIEWPORT'] == 'false'
assert viewport == {
'width': 1024,
'height': 768,
'deviceScaleFactor': 2,
'screenWidth': 1440,
'screenHeight': 900,
}
no_viewport_agent = Agent(task='report title', browser_profile=NoViewportProfile())
no_viewport_env = no_viewport_agent._run_env()
assert no_viewport_env['BU_BROWSER_NO_VIEWPORT'] == 'true'
assert 'BU_BROWSER_VIEWPORT' not in no_viewport_env
def test_beta_agent_translates_browser_profile_storage_state(monkeypatch, tmp_path):
from browser_use.beta import Agent
profile_storage_state_path = tmp_path / 'storage_state.json'
storage_state = {
'cookies': [
{
'name': 'sid',
'value': 'secret',
'domain': '.example.com',
'path': '/',
}
],
'origins': [
{
'origin': 'https://example.com',
'localStorage': [{'name': 'theme', 'value': 'dark'}],
}
],
}
profile_storage_state_path.write_text(json.dumps(storage_state))
class BrowserProfile:
storage_state = profile_storage_state_path
cdp_url = 'http://127.0.0.1:9222'
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['LLM_BROWSER_BROWSER_MODE'] == 'remote-cdp'
assert agent.browser_storage_state == storage_state
assert json.loads(env['BU_BROWSER_STORAGE_STATE']) == storage_state
def test_beta_agent_translates_browser_profile_user_data_dir(monkeypatch, tmp_path):
from browser_use.beta import Agent
profile_dir = tmp_path / 'browser profile'
class BrowserProfile:
headless = False
user_data_dir = profile_dir
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
monkeypatch.delenv('BU_MANAGED_BROWSER_PROFILE', raising=False)
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['LLM_BROWSER_BROWSER_MODE'] == 'managed-headed'
assert agent.managed_browser_profile_dir == str(profile_dir)
assert env['BU_MANAGED_BROWSER_PROFILE'] == str(profile_dir)
def test_beta_agent_translates_browser_profile_executable_path(monkeypatch, tmp_path):
from browser_use.beta import Agent
chrome_path = tmp_path / 'custom chrome'
class BrowserProfile:
headless = True
executable_path = chrome_path
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.setenv('CHROME_PATH', '/usr/bin/existing-chrome')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['LLM_BROWSER_BROWSER_MODE'] == 'managed-headless'
assert agent.managed_browser_executable_path == str(chrome_path)
assert env['CHROME_PATH'] == str(chrome_path)
monkeypatch.setenv('BROWSER_USE_BROWSER_MODE', 'cloud')
cloud_agent = Agent(task='report title', browser_profile=BrowserProfile())
cloud_env = cloud_agent._run_env()
assert cloud_env['LLM_BROWSER_BROWSER_MODE'] == 'cloud'
assert cloud_env['CHROME_PATH'] == '/usr/bin/existing-chrome'
def test_beta_agent_translates_browser_profile_env(monkeypatch):
from browser_use.beta import Agent
class BrowserProfile:
headless = True
env = {
'BU_BROWSER_FLAG': 'enabled',
'BU_BROWSER_BOOL': True,
'BU_BROWSER_RETRIES': 3,
'IGNORED_NESTED': {'nested': 'value'},
}
monkeypatch.setenv('BROWSER_USE_TERMINAL_BINARY', '/tmp/browser-use-terminal')
monkeypatch.setenv('BU_BROWSER_FLAG', 'outer')
monkeypatch.delenv('BROWSER_USE_RUST_BROWSER_MODE', raising=False)
monkeypatch.delenv('BROWSER_USE_BROWSER_MODE', raising=False)
agent = Agent(task='report title', browser_profile=BrowserProfile())
env = agent._run_env()
assert env['LLM_BROWSER_BROWSER_MODE'] == 'managed-headless'
assert agent.managed_browser_env == {
'BU_BROWSER_FLAG': 'enabled',
'BU_BROWSER_BOOL': 'true',
'BU_BROWSER_RETRIES': '3',
}
assert env['BU_BROWSER_FLAG'] == 'enabled'
assert env['BU_BROWSER_BOOL'] == 'true'
assert env['BU_BROWSER_RETRIES'] == '3'
assert 'IGNORED_NESTED' not in env
monkeypatch.setenv('BROWSER_USE_BROWSER_MODE', 'cloud')
cloud_agent = Agent(task='report title', browser_profile=BrowserProfile())
cloud_env = cloud_agent._run_env()
assert cloud_env['LLM_BROWSER_BROWSER_MODE'] == 'cloud'
assert cloud_env['BU_BROWSER_FLAG'] == 'outer'
assert 'BU_BROWSER_BOOL' not in cloud_env
def test_beta_agent_adds_browser_profile_domain_constraints():
from browser_use.beta import Agent
class BrowserProfile:
allowed_domains = ['example.com', '*.browser-use.com']
prohibited_domains = {'ads.example.com'}
agent = Agent(task='Open the allowed site.', browser_profile=BrowserProfile())
env = agent._run_env()
assert agent.allowed_domains == ['example.com', '*.browser-use.com']
assert agent.prohibited_domains == ['ads.example.com']
assert json.loads(env['BU_BROWSER_ALLOWED_DOMAINS']) == ['example.com', '*.browser-use.com']
assert json.loads(env['BU_BROWSER_PROHIBITED_DOMAINS']) == ['ads.example.com']
assert 'Browser profile navigation constraints:' in agent.task
assert 'Allowed domains:' in agent.task
assert '- example.com' in agent.task
assert '- *.browser-use.com' in agent.task
assert 'Prohibited domains:' in agent.task
assert '- ads.example.com' in agent.task
def test_beta_agent_adds_sensitive_data_placeholders_without_values():
from browser_use.beta import Agent
agent = Agent(
task='Log in to the portal.',
sensitive_data={
'username': 'alice@example.com',
'https://example.com': {'password': 'super-secret-password'},
},
)
assert agent.sensitive_data_context == {
'global_placeholders': ['username'],
'domain_placeholders': {'https://example.com': ['password']},
}
assert 'Sensitive data placeholders are available.' in agent.task
assert '- username' in agent.task
assert '- https://example.com: password' in agent.task
assert '<secret>placeholder</secret>' in agent.task
assert 'alice@example.com' not in agent.task
assert 'super-secret-password' not in agent.task
def test_beta_agent_warns_about_sensitive_data_domain_constraints(monkeypatch):
from browser_use.beta import Agent
class RecordingLogger:
def __init__(self):
self.messages = []
def error(self, message, *args, **kwargs):
self.messages.append(('error', message))
def warning(self, message, *args, **kwargs):
self.messages.append(('warning', message))
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
Agent(
task='Use credentials safely.',
sensitive_data={'password': 'super-secret-password'},
directly_open_url=False,
)
messages = '\n'.join(message for _level, message in logger.messages)
assert 'not locked down' in messages
assert 'super-secret-password' not in messages
logger.messages.clear()
class BrowserProfile:
allowed_domains = ['*.example.com']
Agent(
task='Use domain-scoped credentials safely.',
browser_profile=BrowserProfile(),
sensitive_data={
'https://secure.example.com': {'password': 'covered-secret'},
'https://evil.test': {'token': 'uncovered-secret'},
},
directly_open_url=False,
)
messages = '\n'.join(message for _level, message in logger.messages)
assert 'Domain pattern "https://evil.test" in sensitive_data is not covered' in messages
assert 'https://secure.example.com' not in messages
assert 'covered-secret' not in messages
assert 'uncovered-secret' not in messages
def test_beta_agent_sensitive_data_warnings_match_browser_use(monkeypatch):
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
from browser_use.browser import BrowserProfile
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
class RecordingLogger:
def __init__(self):
self.messages = []
def info(self, message, *args, **kwargs):
pass
def debug(self, message, *args, **kwargs):
pass
def error(self, message, *args, **kwargs):
self.messages.append(('error', message))
def warning(self, message, *args, **kwargs):
self.messages.append(('warning', message))
for kwargs in (
{
'sensitive_data': {'password': 'unlocked-secret'},
},
{
'browser_profile': BrowserProfile(allowed_domains=['*.example.com']),
'sensitive_data': {'https://evil.test': {'password': 'uncovered-secret'}},
},
):
browser_use_logger = RecordingLogger()
beta_logger = RecordingLogger()
monkeypatch.setattr(BrowserUseAgent, 'logger', property(lambda self, logger=browser_use_logger: logger))
monkeypatch.setattr(BetaAgent, 'logger', property(lambda self, logger=beta_logger: logger))
BrowserUseAgent(task='Use credentials safely.', llm=LLM(), directly_open_url=False, **kwargs)
BetaAgent(task='Use credentials safely.', llm=LLM(), directly_open_url=False, **kwargs)
assert beta_logger.messages == browser_use_logger.messages
message_text = '\n'.join(message for _level, message in beta_logger.messages)
assert 'unlocked-secret' not in message_text
assert 'uncovered-secret' not in message_text
def test_beta_agent_mirrors_direct_url_startup():
from browser_use.beta import Agent
agent = Agent(task='Open example.com and report the title.')
assert agent.initial_url == 'https://example.com'
assert agent.initial_action_payloads == [{'navigate': {'url': 'https://example.com', 'new_tab': False}}]
assert agent.initial_actions[0].model_dump(exclude_unset=True) == {
'navigate': {'url': 'https://example.com', 'new_tab': False}
}
assert "First navigate to 'https://example.com'" in agent.task
def test_beta_agent_logs_direct_url_startup_like_browser_use(monkeypatch):
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
class RecordingLogger:
def __init__(self):
self.infos = []
def info(self, message, *args, **kwargs):
self.infos.append(message)
def debug(self, message, *args, **kwargs):
pass
def warning(self, message, *args, **kwargs):
pass
def error(self, message, *args, **kwargs):
pass
browser_use_logger = RecordingLogger()
beta_logger = RecordingLogger()
monkeypatch.setattr(BrowserUseAgent, 'logger', property(lambda self: browser_use_logger))
monkeypatch.setattr(BetaAgent, 'logger', property(lambda self: beta_logger))
BrowserUseAgent(task='Open example.com and report the title.', llm=LLM())
BetaAgent(task='Open example.com and report the title.', llm=LLM())
BrowserUseAgent(task='Use https://XXX.XX only as an example price placeholder.', llm=LLM())
BetaAgent(task='Use https://XXX.XX only as an example price placeholder.', llm=LLM())
expected = ['🔗 Found URL in task: https://example.com, adding as initial action...']
assert browser_use_logger.infos == expected
assert beta_logger.infos == expected
def test_beta_agent_exposes_task_helper_methods():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent
class Answer(BaseModel):
answer: str
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
agent = Agent(task='Open example.com and report the title.')
browser_use_agent = BrowserUseAgent(task='Inspect URL extraction.', llm=LLM(), directly_open_url=False)
enhanced = agent._enhance_task_with_schema('Return the answer.', Answer)
assert agent._enhance_task_with_schema('Return the answer.', None) == 'Return the answer.'
assert 'Expected output format: Answer' in enhanced
assert '"answer"' in enhanced
assert agent._extract_start_url('Open example.com and report the title.') == 'https://example.com'
assert agent._extract_start_url('Email support@example.com only.') is None
assert agent._extract_start_url('Open https://example.com/report.pdf and summarize it.') is None
assert agent._extract_start_url('Use https://XXX.XX as a placeholder in the table.') is None
assert browser_use_agent._extract_start_url('Use https://XXX.XX as a placeholder in the table.') is None
numbered_task = '1. Navigate to https://elibrary.ferc.gov/eLibrary/search.\\n2. Ensure "General Search" is selected.'
assert agent._extract_start_url(numbered_task) == 'https://elibrary.ferc.gov/eLibrary/search'
assert browser_use_agent._extract_start_url(numbered_task) == 'https://elibrary.ferc.gov/eLibrary/search'
def test_beta_agent_exposes_url_text_helper_methods():
from browser_use.beta import Agent
from browser_use.llm.messages import ContentPartTextParam, UserMessage
class Nested(BaseModel):
url: str
class Restored(BaseModel):
text: str
nested: Nested
items: list
pair: tuple[str, str]
agent = Agent(task='Shorten URLs.', _url_shortening_limit=8)
original_url = 'https://example.com/path?abcdefghijklmnopqrstuvwxyz#section'
text = f'Open {original_url} now.'
cleaned = agent._remove_think_tags('visible <think>hidden</think> answer')
shortened_text, replacements = agent._replace_urls_in_text(text)
shortened_url = next(iter(replacements))
message = UserMessage(content=[ContentPartTextParam(text=text)])
message_replacements = agent._process_messsages_and_replace_long_urls_shorter_ones([message])
model = Restored(
text=shortened_url,
nested=Nested(url=shortened_url),
items=[shortened_url, {'url': shortened_url}],
pair=(shortened_url, 'untouched'),
)
Agent._recursive_process_all_strings_inside_pydantic_model(model, replacements)
assert cleaned == 'visible answer'
assert original_url not in shortened_text
assert replacements[shortened_url] == original_url
assert Agent._replace_shortened_urls_in_string(shortened_text, replacements) == text
assert message_replacements
assert original_url not in message.content[0].text
assert model.text == original_url
assert model.nested.url == original_url
assert model.items == [original_url, {'url': original_url}]
assert model.pair == (original_url, 'untouched')
def test_beta_agent_preserves_ordered_initial_actions_context():
from browser_use.beta import Agent
agent = Agent(
task='Report what is visible after setup.',
initial_actions=[
{'navigate': {'url': 'https://example.com', 'new_tab': False}},
{'click_element_by_index': {'index': 3}},
],
)
converted = [action.model_dump(exclude_unset=True) for action in agent.initial_actions]
assert agent.initial_action_payloads == [
{'navigate': {'url': 'https://example.com', 'new_tab': False}},
{'click_element_by_index': {'index': 3}},
]
assert converted == [
{'navigate': {'url': 'https://example.com', 'new_tab': False}},
{'click': {'index': 3}},
]
assert 'Browser Use initial actions in order' in agent.task
assert '"navigate"' in agent.task
assert '"click_element_by_index"' in agent.task
assert 'Then complete the task.' in agent.task
def test_beta_agent_skips_ambiguous_or_excluded_direct_urls():
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
provider = 'test-provider'
ambiguous = Agent(task='Compare example.com and browser-use.com.', llm=cast(Any, LLM()))
document = Agent(task='Open https://example.com/report.pdf and summarize it.', llm=cast(Any, LLM()))
assert ambiguous.initial_url is None
assert 'First navigate to' not in ambiguous.task
assert document.initial_url is None
assert 'First navigate to' not in document.task
def test_beta_agent_exposes_browser_use_settings():
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
provider = 'test-provider'
agent = Agent(
task='Open example.com',
llm=cast(Any, LLM()),
use_vision=False,
max_actions_per_step=2,
directly_open_url=False,
available_file_paths=['/tmp/report.txt'],
file_system_path='/tmp/browser-use-files',
include_recent_events=True,
)
assert agent.initial_url is None
assert agent.settings.use_vision is True
assert agent._sdk_run_params(max_steps=2, task=agent.task)['use_vision'] is True
assert agent.settings.max_actions_per_step == 2
assert agent.directly_open_url is False
assert agent.available_file_paths == ['/tmp/report.txt']
assert agent.file_system_path == '/tmp/browser-use-files'
assert agent.include_recent_events is True
def test_beta_agent_defaults_page_extraction_llm_to_main_llm():
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
class ExtractionLLM:
model = 'extract-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
llm = LLM()
extraction_llm = ExtractionLLM()
default_agent = Agent(task='Extract with the main model.', llm=llm)
override_agent = Agent(
task='Extract with a dedicated model.',
llm=llm,
page_extraction_llm=extraction_llm,
)
assert default_agent.settings.page_extraction_llm is llm
assert override_agent.settings.page_extraction_llm is extraction_llm
assert str(id(llm)) in default_agent.token_cost_service.registered_llms
assert str(id(extraction_llm)) in override_agent.token_cost_service.registered_llms
def test_beta_agent_default_llm_matches_browser_use_default(monkeypatch):
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
monkeypatch.setenv('ANTHROPIC_API_KEY', 'test-anthropic-key')
browser_use_agent = BrowserUseAgent(task='Use the default model.', directly_open_url=False)
beta_agent = BetaAgent(task='Use the default model.', directly_open_url=False)
assert browser_use_agent.llm.provider == 'browser-use'
assert beta_agent.llm.provider == browser_use_agent.llm.provider
assert beta_agent.model == browser_use_agent.llm.model
def test_beta_agent_default_llm_respects_default_llm_env(monkeypatch):
from browser_use.beta import Agent
monkeypatch.setenv('OPENAI_API_KEY', 'test-openai-key')
monkeypatch.setenv('DEFAULT_LLM', 'openai_gpt_4_1_mini')
agent = Agent(task='Use configured default model.', directly_open_url=False)
assert agent.llm.provider == 'openai'
assert agent.llm.model == 'gpt-4.1-mini'
def test_beta_agent_explicit_llm_ignores_default_llm_env(monkeypatch):
from browser_use.beta import Agent
monkeypatch.setenv('DEFAULT_LLM', 'openai_gpt_4_1_mini')
class LLM:
model = 'gpt-test'
provider = 'test-provider'
agent = Agent(task='Use configured default model.', llm=cast(Any, LLM()), directly_open_url=False)
assert agent.llm.provider == 'test-provider'
assert agent.llm.model == 'gpt-test'
def test_beta_agent_enables_flash_mode_for_browser_use_llm_provider():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class BrowserUseLLM:
model = 'browser-use-test'
provider = 'browser-use'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
class OtherLLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
browser_use_llm = BrowserUseLLM()
browser_use_agent = BrowserUseAgent(task='Use Browser Use model.', llm=browser_use_llm, directly_open_url=False)
beta_browser_use_agent = BetaAgent(task='Use Browser Use model.', llm=browser_use_llm, directly_open_url=False)
beta_other_agent = BetaAgent(task='Use another model.', llm=OtherLLM(), directly_open_url=False)
assert browser_use_agent.settings.flash_mode is True
assert beta_browser_use_agent.settings.flash_mode is True
assert beta_other_agent.settings.flash_mode is False
assert beta_browser_use_agent.AgentOutput.__name__ == browser_use_agent.AgentOutput.__name__
def test_beta_agent_llm_timeout_defaults_match_browser_use_model_families():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class LLM:
provider = 'test'
def __init__(self, model):
self.model = model
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
for model in ['gpt-test', 'gemini-2.5-pro', 'groq-llama', 'o3-mini', 'claude-sonnet', 'deepseek-chat']:
browser_use_agent = BrowserUseAgent(task='Inspect timeout.', llm=LLM(model), directly_open_url=False)
beta_agent = BetaAgent(task='Inspect timeout.', llm=LLM(model), directly_open_url=False)
assert beta_agent.settings.llm_timeout == browser_use_agent.settings.llm_timeout
override_agent = BetaAgent(task='Override timeout.', llm=LLM('gemini-2.5-pro'), llm_timeout=12)
assert override_agent.settings.llm_timeout == 12
def test_beta_agent_forces_vision_for_all_model_families():
from browser_use.beta import Agent as BetaAgent
class LLM:
provider = 'test'
def __init__(self, model):
self.model = model
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
for model in ['deepseek-chat', 'grok-3', 'grok-code']:
beta_agent = BetaAgent(task='Inspect vision.', llm=LLM(model), directly_open_url=False, use_vision=False)
assert beta_agent.settings.use_vision is True
normal_agent = BetaAgent(task='Inspect vision.', llm=LLM('gpt-test'), directly_open_url=False, use_vision=False)
assert normal_agent.settings.use_vision is True
def test_beta_agent_does_not_warn_when_forcing_vision(monkeypatch):
from browser_use.beta import Agent as BetaAgent
class LLM:
provider = 'test'
def __init__(self, model):
self.model = model
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
class RecordingLogger:
def __init__(self):
self.warnings = []
def info(self, message, *args, **kwargs):
pass
def debug(self, message, *args, **kwargs):
pass
def warning(self, message, *args, **kwargs):
self.warnings.append(message)
def error(self, message, *args, **kwargs):
pass
for model in ['deepseek-chat', 'grok-2']:
beta_logger = RecordingLogger()
monkeypatch.setattr(BetaAgent, 'logger', property(lambda self, logger=beta_logger: logger))
BetaAgent(task='Inspect vision warning.', llm=LLM(model), directly_open_url=False, use_vision=False)
assert beta_logger.warnings == []
def test_beta_agent_constructor_debug_summary_matches_browser_use(monkeypatch):
import browser_use.agent.service as agent_service
import browser_use.beta.service as beta_service
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
class RecordingModuleLogger:
def __init__(self):
self.debugs = []
def debug(self, message, *args, **kwargs):
self.debugs.append(message)
browser_use_logger = RecordingModuleLogger()
beta_logger = RecordingModuleLogger()
monkeypatch.setattr(agent_service, 'logger', browser_use_logger)
monkeypatch.setattr(beta_service, 'logger', beta_logger)
BrowserUseAgent(task='Inspect constructor setup.', llm=LLM(), directly_open_url=False)
BetaAgent(task='Inspect constructor setup.', llm=LLM(), directly_open_url=False)
expected = ' +vision extraction_model=gpt-test +file_system'
assert browser_use_logger.debugs[-1] == expected
assert beta_logger.debugs[-1] == expected
def test_beta_agent_state_id_defaults_like_browser_use():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.agent.views import AgentState
from browser_use.beta import Agent as BetaAgent
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
llm = LLM()
browser_use_task_id = 'browser_use_state'
beta_task_id = 'rust_state_id'
browser_use_agent = BrowserUseAgent(
task='Inspect state.',
llm=llm,
task_id=browser_use_task_id,
directly_open_url=False,
)
beta_agent = BetaAgent(task='Inspect state.', llm=llm, task_id=beta_task_id, directly_open_url=False)
assert browser_use_agent.task_id == browser_use_task_id
assert beta_agent.task_id == beta_task_id
assert browser_use_agent.state.agent_id != browser_use_task_id
assert beta_agent.state.agent_id != beta_task_id
assert beta_agent.state.n_steps == browser_use_agent.state.n_steps == 1
injected_state = AgentState(agent_id='restored-state-id')
restored_agent = BetaAgent(task='Restore state.', injected_agent_state=injected_state)
assert restored_agent.state is injected_state
assert restored_agent.state.agent_id == 'restored-state-id'
def test_beta_agent_eventbus_name_matches_browser_use_suffix_prefix():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
browser_use_agent = BrowserUseAgent(task='Inspect event bus.', llm=LLM(), task_id='browseruseabcd', directly_open_url=False)
beta_agent = BetaAgent(task='Inspect event bus.', llm=LLM(), task_id='rustagentwxyz', directly_open_url=False)
assert browser_use_agent.eventbus.name == 'Agent_abcd'
assert beta_agent.eventbus.name == 'Agent_wxyz'
beta_agent_with_hyphen = BetaAgent(task='Inspect event bus.', llm=LLM(), task_id='task-1', directly_open_url=False)
initial_name = beta_agent_with_hyphen.eventbus.name
beta_agent_with_hyphen.add_new_task('Follow up.')
assert initial_name == 'Agent_sk_1'
assert beta_agent_with_hyphen.eventbus.name.startswith(f'{initial_name}_')
assert beta_agent_with_hyphen.eventbus.name.isidentifier()
def test_beta_agent_constructor_aliases_match_browser_use():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
from browser_use.tools.service import Tools
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
tools = Tools()
controller = Tools()
browser_use_agent = BrowserUseAgent(
task='Resolve aliases.',
llm=LLM(),
tools=tools,
controller=controller,
directly_open_url=False,
)
beta_agent = BetaAgent(
task='Resolve aliases.',
llm=LLM(),
tools=tools,
controller=controller,
directly_open_url=False,
)
assert browser_use_agent.tools is tools
assert beta_agent.tools is tools
expected_error = 'Cannot specify both "browser" and "browser_session" parameters. Use "browser" for the cleaner API.'
for agent_class in (BrowserUseAgent, BetaAgent):
with pytest.raises(ValueError) as exc_info:
agent_class(
task='Conflicting browser aliases.',
llm=LLM(),
browser=object(),
browser_session=object(),
directly_open_url=False,
)
assert str(exc_info.value) == expected_error
def test_beta_agent_initializes_tools_and_action_models():
from browser_use.beta import Agent
from browser_use.tools.service import Tools
class Answer(BaseModel):
answer: str
agent = Agent(
task='Return structured answer.',
output_model_schema=Answer,
use_vision=False,
)
action_names = set(agent.tools.registry.registry.actions)
done_schema = agent.DoneActionModel.model_json_schema()
assert isinstance(agent.tools, Tools)
assert 'done' in action_names
assert agent.settings.use_vision is True
assert 'screenshot' in action_names
assert agent.ActionModel is not None
assert agent.DoneActionModel is not None
assert agent.AgentOutput is not None
assert 'done' in str(done_schema)
def test_beta_agent_setup_action_models_signature_matches_browser_use():
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
browser_use_signature = inspect.signature(BrowserUseAgent._setup_action_models)
beta_signature = inspect.signature(BetaAgent._setup_action_models)
assert list(beta_signature.parameters) == list(browser_use_signature.parameters) == ['self']
agent = BetaAgent(task='Rebuild action models.')
agent.ActionModel = None
agent.DoneActionModel = None
agent.AgentOutput = None
agent.DoneAgentOutput = None
agent._setup_action_models()
assert agent.ActionModel is not None
assert agent.DoneActionModel is not None
assert agent.AgentOutput is not None
assert agent.DoneAgentOutput is not None
async def test_beta_agent_updates_action_models_for_page(monkeypatch):
from browser_use.beta import Agent
agent = Agent(task='Use page-specific tools.')
registry = agent.tools.registry
original_create_action_model = registry.create_action_model
seen = []
def recording_create_action_model(*args, **kwargs):
seen.append(kwargs)
return original_create_action_model(*args, **kwargs)
monkeypatch.setattr(registry, 'create_action_model', recording_create_action_model)
await agent._update_action_models_for_page('https://example.com/settings')
assert seen == [
{'page_url': 'https://example.com/settings'},
{'include_actions': ['done'], 'page_url': 'https://example.com/settings'},
]
assert agent.ActionModel is not None
assert agent.DoneActionModel is not None
assert agent.AgentOutput is not None
assert agent.DoneAgentOutput is not None
def test_beta_agent_initializes_runtime_metadata_and_observability(monkeypatch):
from browser_use.beta import Agent
from browser_use.tokens.service import TokenCost
monkeypatch.delenv('BU_USE_CALCULATE_COST', raising=False)
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
llm = LLM()
agent = Agent(
task='Inspect metadata.',
llm=llm,
task_id='task-1234',
source='ci',
calculate_cost=True,
use_thinking=False,
)
assert agent.version
assert agent.source == 'ci'
assert agent.logger.name.endswith('1234 ⇢ 🅑 1234 🅣 --')
assert agent.eventbus is not None
assert callable(agent.telemetry.capture)
assert callable(agent.telemetry.flush)
assert isinstance(agent.token_cost_service, TokenCost)
assert agent.token_cost_service.include_cost is True
assert str(id(llm)) in agent.token_cost_service.registered_llms
assert agent.DoneAgentOutput is not None
assert agent._run_env()['BU_USE_CALCULATE_COST'] == 'true'
default_agent = Agent(task='Inspect default metadata.', llm=LLM())
assert 'BU_USE_CALCULATE_COST' not in default_agent._run_env()
async def test_beta_agent_logger_name_matches_browser_use(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
from browser_use.browser import BrowserProfile, BrowserSession
class TestEventBus:
def __init__(self, name):
self.name = name
def stop(self, *args, **kwargs):
return None
monkeypatch.setattr(beta_service, 'EventBus', TestEventBus)
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
async def stop_eventbus(eventbus):
try:
result = eventbus.stop(timeout=0.1, clear=True)
except TypeError:
result = eventbus.stop(timeout=0.1)
if inspect.isawaitable(result):
await result
browser_use_agent = BrowserUseAgent(
task='Inspect logger name.',
llm=LLM(),
browser_session=BrowserSession(browser_profile=BrowserProfile(), id='browser-abcd'),
task_id='task-1234',
directly_open_url=False,
)
browser_use_logger_name = browser_use_agent.logger.name
await stop_eventbus(browser_use_agent.eventbus)
beta_agent = BetaAgent(
task='Inspect logger name.',
llm=LLM(),
browser_session=BrowserSession(browser_profile=BrowserProfile(), id='browser-abcd'),
task_id='task-1234',
directly_open_url=False,
)
assert beta_agent.logger.name == browser_use_logger_name
assert beta_agent.logger.name == 'browser_use.Agent🅰 1234 ⇢ 🅑 abcd 🅣 --'
await stop_eventbus(beta_agent.eventbus)
async def test_beta_agent_exposes_logging_helper_methods(monkeypatch):
import time
import browser_use.beta.service as beta_service
from browser_use.agent.views import ActionResult
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
provider = 'test-provider'
class BrowserProfile:
downloads_path = None
class BrowserSession:
cdp_url = 'wss://browser.example/devtools/browser/1'
browser_profile = BrowserProfile()
class FakeAction:
def model_dump(self, **kwargs):
return {'navigate': {'url': 'https://example.com', 'new_tab': False}}
class Parsed:
action = [FakeAction()]
class DomState:
selector_map = {1: object(), 2: object()}
class BrowserStateSummary:
url = 'https://example.com/a/very/long/path/that/will/be/shortened/in/logging'
dom_state = DomState()
async def no_latest_version():
return None
captured_events = []
class Telemetry:
def capture(self, event):
captured_events.append(event)
monkeypatch.setattr(beta_service, 'check_latest_browser_use_version', no_latest_version)
agent = Agent(task='Log helper parity.', llm=LLM(), browser_session=BrowserSession())
agent.telemetry = Telemetry()
await agent._log_agent_run()
agent._log_first_step_startup()
agent._log_step_context(BrowserStateSummary())
agent._log_next_action_summary(Parsed())
agent._log_step_completion_summary(time.time() - 1, [ActionResult(extracted_content='ok')])
agent._log_action(FakeAction(), 'navigate', 1, 2)
agent.history = beta_service._history_from_events(
[
{'event_type': 'browser.state', 'payload': {'url': 'https://example.com', 'title': 'Example'}},
{
'event_type': 'model.usage',
'payload': {'input_tokens': 11, 'cached_input_tokens': 3, 'output_tokens': 7, 'cost_usd': 0.12},
},
{'event_type': 'session.done', 'payload': {'result': 'done'}},
],
model='gpt-test',
started=1.0,
finished=2.5,
output_model_schema=None,
process_error=None,
)
agent._sync_state_from_history()
agent.state.n_steps = 99
agent._log_final_outcome_messages()
agent._log_agent_event(max_steps=9, agent_run_error='manual-error')
assert captured_events
assert captured_events[0].model == 'gpt-test'
assert captured_events[0].model_provider == 'test-provider'
assert captured_events[0].agent_type == 'rust_core'
assert captured_events[0].cdp_url == 'browser.example'
assert captured_events[0].action_history == [[{'done': {'text': 'done', 'success': True}}]]
assert captured_events[0].urls_visited == ['https://example.com']
assert captured_events[0].steps == 1
assert captured_events[0].total_input_tokens == 11
assert captured_events[0].total_output_tokens == 7
assert captured_events[0].prompt_cached_tokens == 3
assert captured_events[0].final_result_response == '"done"'
assert captured_events[0].error_message == 'manual-error'
def test_beta_agent_telemetry_filters_empty_reconstructed_urls():
import browser_use.beta.service as beta_service
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
provider = 'test-provider'
captured_events = []
class Telemetry:
def capture(self, event):
captured_events.append(event)
agent = Agent(task='Log telemetry URLs.', llm=cast(Any, LLM()), directly_open_url=False)
agent.telemetry = Telemetry()
agent.history = beta_service._history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{
'event_type': 'tool.output',
'payload': {'name': 'browser', 'text': 'Connected to browser at http://127.0.0.1:41335'},
},
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'browser.state', 'payload': {'url': 'https://example.com', 'title': 'Example'}},
{'event_type': 'session.done', 'payload': {'result': 'done'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent._log_agent_event(max_steps=3)
assert captured_events[0].agent_type == 'rust_core'
assert captured_events[0].urls_visited == ['https://example.com']
def test_beta_agent_telemetry_records_cloud_cdp_hostname():
import browser_use.beta.service as beta_service
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
provider = 'test-provider'
class BrowserProfile:
downloads_path = None
class BrowserSession:
cdp_url = 'wss://cloud-browser.example/devtools/browser/session'
browser_profile = BrowserProfile()
captured_events = []
class Telemetry:
def capture(self, event):
captured_events.append(event)
agent = Agent(task='Log cloud CDP telemetry.', llm=cast(Any, LLM()), browser_session=cast(Any, BrowserSession()))
agent.telemetry = Telemetry()
agent.history = beta_service._history_from_events(
[
{'event_type': 'browser.state', 'payload': {'url': 'https://example.com', 'title': 'Example'}},
{'event_type': 'session.done', 'payload': {'result': 'done'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent._log_agent_event(max_steps=3)
assert captured_events[0].name == 'agent_event'
assert captured_events[0].agent_type == 'rust_core'
assert captured_events[0].cdp_url == 'cloud-browser.example'
assert captured_events[0].urls_visited == ['https://example.com']
async def test_beta_agent_run_metadata_logs_match_browser_use(monkeypatch):
import browser_use.agent.service as agent_service
import browser_use.beta.service as beta_service
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
class RecordingLogger:
def __init__(self):
self.infos = []
self.debugs = []
def info(self, message, *args, **kwargs):
self.infos.append(message)
def debug(self, message, *args, **kwargs):
self.debugs.append(message)
async def latest_version():
return '99.99.99'
browser_use_logger = RecordingLogger()
beta_logger = RecordingLogger()
monkeypatch.setattr(agent_service, 'check_latest_browser_use_version', latest_version)
monkeypatch.setattr(beta_service, 'check_latest_browser_use_version', latest_version)
monkeypatch.setattr(BrowserUseAgent, 'logger', property(lambda self: browser_use_logger))
monkeypatch.setattr(BetaAgent, 'logger', property(lambda self: beta_logger))
browser_use_agent = BrowserUseAgent(
task='Log run metadata parity.',
llm=LLM(),
task_id='runmetadatabu',
directly_open_url=False,
)
beta_agent = BetaAgent(
task='Log run metadata parity.',
llm=LLM(),
task_id='runmetadatars',
directly_open_url=False,
)
browser_use_logger.infos.clear()
browser_use_logger.debugs.clear()
beta_logger.infos.clear()
beta_logger.debugs.clear()
await browser_use_agent._log_agent_run()
await beta_agent._log_agent_run()
assert beta_logger.infos == browser_use_logger.infos
assert beta_logger.debugs == browser_use_logger.debugs
assert beta_logger.infos[0] == '\033[34m🎯 Task: Log run metadata parity.\033[0m'
assert beta_logger.debugs[0].startswith('🤖 Browser-Use Library Version')
assert beta_logger.infos[1].startswith('📦 Newer version available: 99.99.99')
async def test_beta_agent_exposes_step_finalization_helper_methods(tmp_path):
import base64
import time
from browser_use.agent.views import ActionResult, AgentStepInfo
from browser_use.beta import Agent
class DomState:
selector_map = {}
class BrowserStateSummary:
url = 'https://example.com/final'
title = 'Final'
tabs = []
screenshot = base64.b64encode(b'png-bytes').decode('utf-8')
dom_state = DomState()
class EventBus:
def __init__(self):
self.dispatched = []
def dispatch(self, event):
self.dispatched.append(event)
return event
agent = Agent(task='Finalize helper parity.', file_system_path=str(tmp_path / 'agent-files'))
agent.eventbus = EventBus()
await agent._handle_step_error(ValueError('bad step'))
assert agent.state.consecutive_failures == 1
assert agent.state.last_result is not None
assert agent.state.last_result[-1].error is not None
assert agent.state.last_result[-1].error.startswith('bad step')
await agent._post_process()
assert agent.state.consecutive_failures == 2
agent.state.last_result = [
ActionResult(is_done=True, success=True, extracted_content='final answer', attachments=['file:///tmp/report.txt'])
]
await agent._post_process()
assert agent.state.consecutive_failures == 0
agent.state.last_model_output = agent.AgentOutput(
evaluation_previous_goal='Need answer',
memory='Have answer',
next_goal='Finish',
action=[agent.ActionModel(done={'text': 'final answer', 'success': True})],
)
agent.step_start_time = time.time() - 0.1
await agent._finalize(BrowserStateSummary())
assert agent.state.n_steps == 2
assert agent.state.file_system_state is not None
assert agent.history.final_result() == 'final answer'
assert agent.history.urls() == ['https://example.com/final']
assert Path(agent.history.history[0].state.screenshot_path).read_bytes() == b'png-bytes'
assert [type(event).__name__ for event in agent.eventbus.dispatched] == ['CreateAgentStepEvent']
assert agent.eventbus.dispatched[0].agent_task_id == str(agent.task_id)
assert agent.eventbus.dispatched[0].actions == [{'done': {'text': 'final answer', 'success': True, 'files_to_display': []}}]
assert agent.eventbus.dispatched[0].url == 'https://example.com/final'
assert agent.eventbus.dispatched[0].screenshot_url.startswith('data:image/')
assert ';base64,' in agent.eventbus.dispatched[0].screenshot_url
await agent._force_done_after_last_step(AgentStepInfo(step_number=2, max_steps=3))
assert agent.AgentOutput is agent.DoneAgentOutput
agent.state.consecutive_failures = agent.settings.max_failures
agent.AgentOutput = None
await agent._force_done_after_failure()
assert agent.AgentOutput is agent.DoneAgentOutput
async def test_beta_agent_finalize_logs_step_completion_without_browser_state(monkeypatch, tmp_path):
import time
from browser_use.agent.views import ActionResult
from browser_use.beta import Agent
class RecordingLogger:
def __init__(self):
self.debugs = []
def debug(self, message, *args, **kwargs):
self.debugs.append(message)
class EventBus:
def __init__(self):
self.dispatched = []
def dispatch(self, event):
self.dispatched.append(event)
return event
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
agent = Agent(task='Finalize without browser state.', file_system_path=str(tmp_path / 'agent-files'))
agent.eventbus = EventBus()
agent.state.last_result = [ActionResult(extracted_content='ok')]
agent.step_start_time = time.time() - 0.1
starting_steps = agent.state.n_steps
await agent._finalize(None)
assert agent.state.n_steps == starting_steps + 1
assert agent.state.file_system_state is not None
assert agent.history.history == []
assert agent.eventbus.dispatched == []
assert any('Ran 1 action' in message and 'success=1' in message for message in logger.debugs)
async def test_beta_agent_finalize_orders_summary_save_before_step_event(monkeypatch, tmp_path):
import base64
import time
from browser_use.agent.views import ActionResult
from browser_use.beta import Agent
calls = []
class RecordingLogger:
def debug(self, message, *args, **kwargs):
if 'Ran 1 action' in message:
calls.append('summary')
class DomState:
selector_map = {}
class BrowserStateSummary:
url = 'https://example.com/finalize-order'
title = 'Finalize Order'
tabs = []
screenshot = base64.b64encode(b'png-bytes').decode('utf-8')
dom_state = DomState()
class EventBus:
def __init__(self):
self.dispatched = []
def dispatch(self, event):
calls.append('dispatch')
self.dispatched.append(event)
return event
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
agent = Agent(task='Finalize order parity.', file_system_path=str(tmp_path / 'agent-files'))
agent.eventbus = EventBus()
agent.state.last_result = [ActionResult(is_done=True, success=True, extracted_content='final answer')]
agent.state.last_model_output = agent.AgentOutput(
evaluation_previous_goal='Need answer',
memory='Have answer',
next_goal='Finish',
action=[agent.ActionModel(done={'text': 'final answer', 'success': True})],
)
agent.step_start_time = time.time() - 0.1
monkeypatch.setattr(agent, 'save_file_system_state', lambda: calls.append('save'))
starting_steps = agent.state.n_steps
await agent._finalize(BrowserStateSummary())
assert calls == ['summary', 'save', 'dispatch']
assert agent.state.n_steps == starting_steps + 1
assert [type(event).__name__ for event in agent.eventbus.dispatched] == ['CreateAgentStepEvent']
async def test_beta_agent_done_only_guidance_matches_browser_use(monkeypatch):
from browser_use.agent.views import AgentStepInfo
from browser_use.beta import Agent
class RecordingLogger:
def __init__(self):
self.debugs = []
def debug(self, message, *args, **kwargs):
self.debugs.append(message)
messages = []
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
agent = Agent(task='Done-only guidance parity.')
monkeypatch.setattr(agent._message_manager, '_add_context_message', lambda message: messages.append(message.content))
await agent._force_done_after_last_step(AgentStepInfo(step_number=2, max_steps=3))
assert agent.AgentOutput is agent.DoneAgentOutput
assert logger.debugs == ['Last step finishing up']
assert len(messages) == 1
assert 'All other tools which you see in history or examples are not available.' in messages[-1]
assert 'set success in "done" to false! E.g. if not all steps are fully completed.' in messages[-1]
agent.AgentOutput = None
agent.state.consecutive_failures = agent.settings.max_failures
await agent._force_done_after_failure()
assert agent.AgentOutput is agent.DoneAgentOutput
assert logger.debugs[-1] == 'Force done action, because we reached max_failures.'
assert len(messages) == 2
assert messages[-1].startswith(f'You failed {agent.settings.max_failures} times. Therefore we terminate the agent.')
assert 'All other tools which you see in history or examples are not available.' in messages[-1]
assert 'set success in "done" to false! E.g. if not all steps are fully completed.' in messages[-1]
async def test_beta_agent_post_process_logs_browser_use_result_messages(monkeypatch):
from browser_use.agent.views import ActionResult
from browser_use.beta import Agent
class RecordingLogger:
def __init__(self):
self.debugs = []
self.infos = []
def debug(self, message, *args, **kwargs):
self.debugs.append(message)
def info(self, message, *args, **kwargs):
self.infos.append(message)
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
agent = Agent(task='Post-process log parity.')
download_checks = []
async def fake_check_downloads(label):
download_checks.append(label)
agent._check_and_update_downloads = fake_check_downloads
agent.state.last_result = [ActionResult(error='failed action')]
await agent._post_process()
assert agent.state.consecutive_failures == 1
assert logger.debugs == [f'🔄 Step {agent.state.n_steps}: Consecutive failures: 1']
agent.state.last_result = [
ActionResult(
is_done=True, success=True, extracted_content='final answer', attachments=['file:///tmp/a.txt', 'file:///tmp/b.txt']
)
]
await agent._post_process()
assert agent.state.consecutive_failures == 0
assert logger.debugs[-1] == f'🔄 Step {agent.state.n_steps}: Consecutive failures reset to: 0'
assert logger.infos[-3:] == [
'\n📄 \033[32m Final Result:\033[0m \nfinal answer\n\n',
'👉 Attachment 1: file:///tmp/a.txt',
'👉 Attachment 2: file:///tmp/b.txt',
]
logger.infos.clear()
agent.state.last_result = [ActionResult(is_done=True, success=False, extracted_content='not done')]
await agent._post_process()
assert logger.infos == ['\n📄 \033[31m Final Result:\033[0m \nnot done\n\n']
assert download_checks == ['after executing actions', 'after executing actions', 'after executing actions']
async def test_beta_agent_handle_step_error_logs_browser_use_failure_prefix(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.beta import Agent
class RecordingLogger:
def __init__(self):
self.errors = []
def error(self, message, *args, **kwargs):
self.errors.append(message)
def debug(self, message, *args, **kwargs):
pass
def info(self, message, *args, **kwargs):
pass
def warning(self, message, *args, **kwargs):
pass
def isEnabledFor(self, level):
return False
class LLM:
model = 'gpt-test'
agent_logger = RecordingLogger()
module_logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: agent_logger))
monkeypatch.setattr(beta_service, 'logger', module_logger)
agent = Agent(task='Handle normal step error.', llm=LLM())
await agent._handle_step_error(ValueError('bad step'))
assert agent.state.consecutive_failures == 1
assert agent.state.last_result is not None
assert agent.state.last_result[-1].error is not None
assert agent.state.last_result[-1].error.startswith('bad step')
max_total_failures = agent.settings.max_failures + int(agent.settings.final_response_after_failure)
assert len(agent_logger.errors) == 1
assert agent_logger.errors[0].startswith(f'❌ Result failed 1/{max_total_failures} times:\n bad step')
agent_logger.errors.clear()
agent.state.last_result = None
await agent._handle_step_error(InterruptedError('paused'))
assert agent_logger.errors == ['The agent was interrupted mid-step - paused']
assert agent.state.last_result is None
parse_agent = Agent(task='Handle parse step error.', llm=LLM())
agent_logger.errors.clear()
module_logger.errors.clear()
await parse_agent._handle_step_error(ValueError('Could not parse response: missing action'))
assert parse_agent.state.consecutive_failures == 1
assert parse_agent.state.last_result[-1].error is not None
assert parse_agent.state.last_result[-1].error.startswith('Could not parse response: missing action')
assert agent_logger.errors == []
parse_max_total_failures = parse_agent.settings.max_failures + int(parse_agent.settings.final_response_after_failure)
assert module_logger.errors[0] == 'Model: gpt-test failed'
assert module_logger.errors[1].startswith(
f'❌ Result failed 1/{parse_max_total_failures} times:\n Could not parse response: missing action'
)
def test_beta_agent_initializes_message_manager_and_followup_state():
from browser_use.agent.message_manager.service import MessageManager
from browser_use.beta import Agent
agent = Agent(
task='Initial task.',
task_id='task-with-hyphen',
include_recent_events=True,
)
initial_eventbus_name = agent.eventbus.name
agent.add_new_task('Follow up task.')
assert isinstance(agent.message_manager, MessageManager)
assert agent.message_manager is agent._message_manager
assert agent.message_manager.include_recent_events is True
assert '<initial_user_request>' in agent.message_manager.task
assert '<follow_up_user_request> Follow up task. </follow_up_user_request>' in agent.message_manager.task
assert agent.state.follow_up_task is True
assert agent.state.stopped is False
assert agent.state.paused is False
assert agent.eventbus.name.startswith(initial_eventbus_name)
assert agent.eventbus.name.isidentifier()
def test_beta_agent_initializes_browser_use_session_and_file_system(tmp_path):
from browser_use.beta import Agent
from browser_use.browser import BrowserProfile, BrowserSession
file_system_path = tmp_path / 'agent-files'
downloads_path = tmp_path / 'downloads'
agent = Agent(
task='Open example.com',
task_id='task-1234',
browser_profile=BrowserProfile(headless=True, downloads_path=downloads_path),
file_system_path=str(file_system_path),
)
assert isinstance(agent.browser_session, BrowserSession)
assert agent.browser_session.id.endswith('1234')
assert agent.browser_profile is agent.browser_session.browser_profile
assert agent.browser_profile.downloads_path == downloads_path
assert agent.has_downloads_path is True
assert agent.file_system_path == str(file_system_path)
assert agent.file_system.base_dir == file_system_path
assert agent.state.file_system_state is not None
def test_beta_agent_exposes_setup_helper_methods(tmp_path):
from browser_use.beta import Agent
from browser_use.screenshots.service import ScreenshotService
class LLM:
_verified_api_keys = True
initial_file_system_path = tmp_path / 'initial-files'
new_file_system_path = tmp_path / 'new-files'
agent = Agent(
task='Set up helpers.',
llm=LLM(),
file_system_path=str(initial_file_system_path),
source='constructor-source',
)
agent.file_system = None
agent.file_system_path = None
agent._set_file_system()
agent._set_screenshot_service()
agent._set_browser_use_version_and_source('helper-source')
assert agent.file_system_path == str(initial_file_system_path)
assert agent.file_system.base_dir == initial_file_system_path
assert isinstance(agent.screenshot_service, ScreenshotService)
assert agent.screenshot_service.agent_directory == agent.agent_directory
assert agent.source == 'helper-source'
assert agent.version
assert agent._verify_and_setup_llm() is True
with pytest.raises(ValueError, match='Cannot provide both file_system_state'):
agent._set_file_system(str(new_file_system_path))
agent.state.file_system_state = None
agent._set_file_system(str(new_file_system_path))
assert agent.file_system_path == str(new_file_system_path)
assert agent.file_system.base_dir == new_file_system_path
def test_beta_agent_constructor_invokes_llm_verification(monkeypatch):
from browser_use.beta import Agent
class LLM:
model = 'gpt-test'
provider = 'test-provider'
_verified_api_keys = False
monkeypatch.setenv('SKIP_LLM_API_KEY_VERIFICATION', 'true')
llm = LLM()
agent = Agent(task='Verify LLM during construction.', llm=llm, directly_open_url=False)
assert agent.llm is llm
assert llm._verified_api_keys is True
def test_beta_agent_browser_profile_property_tracks_session_profile():
from browser_use.beta import Agent
class FirstProfile:
downloads_path = None
class SecondProfile:
downloads_path = '/tmp/changed-downloads'
class BrowserSession:
browser_profile = FirstProfile()
session = BrowserSession()
agent = Agent(task='Use the current browser profile.', browser_session=session)
assert agent.browser_profile is session.browser_profile
session.browser_profile = SecondProfile()
assert agent.browser_profile is session.browser_profile
assert agent.browser_profile.downloads_path == '/tmp/changed-downloads'
async def test_beta_agent_tracks_downloaded_files_and_saves_file_system_state(tmp_path):
from browser_use.beta import Agent
class BrowserProfile:
downloads_path = tmp_path / 'downloads'
class BrowserSession:
browser_profile = BrowserProfile()
downloaded_files = ['/tmp/report.csv', '/tmp/report.csv']
agent = Agent(
task='Use downloaded files.',
browser_session=BrowserSession(),
available_file_paths=['/tmp/input.txt'],
file_system_path=str(tmp_path / 'agent-files'),
)
await agent._check_and_update_downloads('test')
BrowserSession.downloaded_files = ['/tmp/report.csv', '/tmp/summary.pdf']
await agent._check_and_update_downloads('test')
agent.state.file_system_state = None
agent.save_file_system_state()
assert agent.available_file_paths == ['/tmp/input.txt', '/tmp/report.csv', '/tmp/summary.pdf']
assert agent._last_known_downloads == ['/tmp/report.csv', '/tmp/summary.pdf']
assert agent.state.file_system_state is not None
assert agent.state.file_system_state.base_dir == str(tmp_path / 'agent-files')
async def test_beta_agent_initializes_screenshot_service(tmp_path):
import base64
from browser_use.beta import Agent
from browser_use.screenshots.service import ScreenshotService
agent = Agent(task='Capture a screenshot.', file_system_path=str(tmp_path / 'agent-files'))
assert isinstance(agent.screenshot_service, ScreenshotService)
assert agent.screenshot_service.agent_directory == agent.agent_directory
assert agent.screenshot_service.screenshots_dir == agent.agent_directory / 'screenshots'
assert agent.screenshot_service.screenshots_dir.exists()
screenshot_b64 = base64.b64encode(b'png-bytes').decode('utf-8')
screenshot_path = await agent.screenshot_service.store_screenshot(screenshot_b64, step_number=3)
assert Path(screenshot_path).name == 'step_3.png'
assert Path(screenshot_path).read_bytes() == b'png-bytes'
assert await agent.screenshot_service.get_screenshot(screenshot_path) == screenshot_b64
def test_beta_agent_adds_available_files_to_task_context():
from browser_use.beta import Agent
agent = Agent(
task='Summarize the provided report.',
available_file_paths=['/tmp/report.txt', '~/notes.md'],
)
assert agent.available_file_paths == ['/tmp/report.txt', '~/notes.md']
assert 'Available local files:' in agent.task
assert '- /tmp/report.txt' in agent.task
assert '- ' in agent.task and 'notes.md' in agent.task
def test_beta_agent_default_model_uses_browser_use_default_llm(monkeypatch):
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent
monkeypatch.setenv('BROWSER_USE_RUST_MODEL', 'legacy-codex-default')
agent = Agent(task='report title')
browser_use_agent = BrowserUseAgent(task='report title')
assert agent.llm.provider == browser_use_agent.llm.provider == 'browser-use'
assert agent.model == browser_use_agent.llm.model
async def test_beta_agent_log_completion_matches_browser_use(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class RecordingLogger:
def __init__(self):
self.infos = []
def debug(self, message, *args, **kwargs):
pass
def info(self, message, *args, **kwargs):
self.infos.append(message)
def warning(self, message, *args, **kwargs):
pass
def error(self, message, *args, **kwargs):
pass
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
def make_history(success: bool):
events = [{'event_type': 'session.done', 'payload': {'result': 'done'}}] if success else []
return beta_service._history_from_events(
events,
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None if success else 'failed',
)
for index, success in enumerate((True, False)):
browser_use_logger = RecordingLogger()
beta_logger = RecordingLogger()
monkeypatch.setattr(BrowserUseAgent, 'logger', property(lambda self, logger=browser_use_logger: logger))
monkeypatch.setattr(BetaAgent, 'logger', property(lambda self, logger=beta_logger: logger))
browser_use_agent = BrowserUseAgent(
task='Log completion parity.',
llm=LLM(),
task_id=f'browsercompletionbu{index}',
directly_open_url=False,
)
beta_agent = BetaAgent(
task='Log completion parity.',
llm=LLM(),
task_id=f'rustcompletionrs{index}',
directly_open_url=False,
)
browser_use_agent.history = make_history(success)
beta_agent.history = make_history(success)
await browser_use_agent.log_completion()
await beta_agent.log_completion()
assert beta_logger.infos == browser_use_logger.infos
def test_beta_agent_run_sync_delegates_to_async_run(monkeypatch):
from browser_use.beta import Agent
agent = Agent(task='start')
async def fake_run(max_steps=100, on_step_start=None, on_step_end=None):
agent.kwargs['seen_max_steps'] = max_steps
return agent.history
agent.run = fake_run
assert agent.run_sync(max_steps=7) is agent.history
assert agent.kwargs['seen_max_steps'] == 7
def test_beta_agent_lifecycle_state_and_save_history(tmp_path):
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
agent = Agent(task='start')
agent.pause()
assert agent.state.paused is True
agent.resume()
assert agent.state.paused is False
agent.stop()
assert agent.state.stopped is True
agent.history = _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'saved answer'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
history_file = tmp_path / 'history.json'
agent.save_history(history_file)
assert 'saved answer' in history_file.read_text(encoding='utf-8')
def test_beta_agent_control_methods_match_browser_use_user_feedback(monkeypatch, capsys):
from browser_use.beta import Agent
class RecordingLogger:
def __init__(self):
self.infos = []
def info(self, message, *args, **kwargs):
self.infos.append(message)
def debug(self, message, *args, **kwargs):
pass
def warning(self, message, *args, **kwargs):
pass
def error(self, message, *args, **kwargs):
pass
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
agent = Agent(task='Control lifecycle feedback.', directly_open_url=False)
agent.pause()
pause_output = capsys.readouterr().out
assert agent.state.paused is True
assert not agent._external_pause_event.is_set()
assert 'Paused the agent and left the browser open.' in pause_output
assert 'Press [Enter] to resume or [Ctrl+C] again to quit.' in pause_output
agent.resume()
resume_output = capsys.readouterr().out
assert agent.state.paused is False
assert agent._external_pause_event.is_set()
assert '----------------------------------------------------------------------' in resume_output
assert 'Resuming agent execution where it left off...' in resume_output
agent.stop()
assert agent.state.stopped is True
assert agent._external_pause_event.is_set()
assert logger.infos == ['⏹️ Agent stopping']
def test_beta_agent_sync_state_counts_terminal_histories_monotonically():
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
def history_from_result(result: str):
return _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': result}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent = Agent(task='Sync terminal history steps.', llm=type('LLM', (), {'model': 'gpt-test'})())
starting_steps = agent.state.n_steps
agent.history = history_from_result('first answer')
agent._sync_state_from_history()
assert agent.state.n_steps == starting_steps + 1
assert agent.state.last_result is not None
assert agent.state.last_result[-1].extracted_content == 'first answer'
agent._sync_state_from_history()
assert agent.state.n_steps == starting_steps + 1
agent.history = history_from_result('follow-up answer')
agent._sync_state_from_history()
assert agent.state.n_steps == starting_steps + 2
assert agent.state.last_result is not None
assert agent.state.last_result[-1].extracted_content == 'follow-up answer'
async def test_beta_agent_close_kills_non_keep_alive_browser_session():
from browser_use.beta import Agent
class FakeSdk:
def __init__(self):
self.calls = []
self.close_calls = 0
async def call(self, method, params=None):
self.calls.append((method, params or {}))
return {'ok': True}
async def close(self):
self.close_calls += 1
class BrowserProfile:
keep_alive = False
class BrowserSession:
browser_profile = BrowserProfile()
def __init__(self):
self.kill_calls = 0
async def kill(self):
self.kill_calls += 1
session = BrowserSession()
agent = Agent(task='close session', browser_session=session, directly_open_url=False)
fake_sdk = FakeSdk()
agent._sdk_client = fake_sdk
agent._sdk_agent_id = 'agent-1'
agent._sdk_browser_id = 'browser-1'
agent.terminal_session_id = 'session-1'
await agent.close()
assert fake_sdk.calls == [
('agent.close', {'agent_id': 'agent-1'}),
('browser.close', {'browser_id': 'browser-1'}),
]
assert fake_sdk.close_calls == 1
assert agent._sdk_client is None
assert agent._sdk_agent_id is None
assert agent._sdk_browser_id is None
assert agent.terminal_session_id is None
assert session.kill_calls == 1
class KeepAliveProfile:
keep_alive = True
keep_alive_session = BrowserSession()
keep_alive_session.browser_profile = KeepAliveProfile()
keep_alive_agent = Agent(task='keep session', browser_session=keep_alive_session, directly_open_url=False)
keep_alive_sdk = FakeSdk()
keep_alive_agent._sdk_client = keep_alive_sdk
keep_alive_agent._sdk_agent_id = 'agent-keep'
keep_alive_agent._sdk_browser_id = 'browser-keep'
keep_alive_agent.terminal_session_id = 'session-keep'
await keep_alive_agent.close()
assert keep_alive_sdk.calls == []
assert keep_alive_sdk.close_calls == 0
assert keep_alive_agent._sdk_client is keep_alive_sdk
assert keep_alive_agent._sdk_agent_id == 'agent-keep'
assert keep_alive_agent._sdk_browser_id == 'browser-keep'
assert keep_alive_agent.terminal_session_id == 'session-keep'
assert keep_alive_session.kill_calls == 0
async def test_beta_agent_close_logs_cleanup_errors_without_raising(monkeypatch):
from browser_use.beta import Agent
class RecordingLogger:
def __init__(self):
self.errors = []
def error(self, message, *args, **kwargs):
self.errors.append(message)
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
class BrowserProfile:
keep_alive = False
class BrowserSession:
browser_profile = BrowserProfile()
async def kill(self):
raise RuntimeError('cleanup failed')
agent = Agent(task='close failed session', browser_session=BrowserSession(), directly_open_url=False)
assert await agent.close() is None
assert logger.errors == ['Error during cleanup: cleanup failed']
async def test_beta_agent_finalize_run_cleanup_closes_sdk_resources():
from browser_use.beta import Agent
class FakeSdk:
def __init__(self):
self.calls = []
self.close_calls = 0
async def call(self, method, params=None):
self.calls.append((method, params or {}))
return {'ok': True}
async def close(self):
self.close_calls += 1
class BrowserProfile:
keep_alive = False
class BrowserSession:
browser_profile = BrowserProfile()
async def kill(self):
return None
agent = Agent(task='finalize session', browser_session=BrowserSession(), directly_open_url=False)
fake_sdk = FakeSdk()
agent._sdk_client = fake_sdk
agent._sdk_agent_id = 'agent-finalize'
agent._sdk_browser_id = 'browser-finalize'
agent.terminal_session_id = 'session-finalize'
await agent._finalize_run_cleanup()
assert fake_sdk.calls == [
('agent.close', {'agent_id': 'agent-finalize'}),
('browser.close', {'browser_id': 'browser-finalize'}),
]
assert fake_sdk.close_calls == 1
assert agent._sdk_client is None
assert agent._sdk_agent_id is None
assert agent._sdk_browser_id is None
assert agent.terminal_session_id is None
async def test_beta_agent_check_stop_or_pause_matches_browser_use_lifecycle():
from browser_use.beta import Agent
seen = []
async def should_stop():
seen.append('should_stop')
return True
agent = Agent(task='check controls', register_should_stop_callback=should_stop)
with pytest.raises(InterruptedError):
await agent._check_stop_or_pause()
assert seen == ['should_stop']
assert agent.state.stopped is True
paused_agent = Agent(task='check pause')
paused_agent.pause()
with pytest.raises(InterruptedError):
await paused_agent._check_stop_or_pause()
assert paused_agent.state.paused is True
async def test_beta_agent_rerun_history_delegates_to_rust_run():
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
agent = Agent(task='rerun start', max_steps=8)
previous = _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'previous answer'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
seen = []
async def fake_run(max_steps=100, on_step_start=None, on_step_end=None):
seen.append(max_steps)
return _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'rerun answer'}}],
model='gpt-test',
started=3.0,
finished=4.0,
output_model_schema=None,
process_error=None,
)
agent.run = fake_run
results = await agent.rerun_history(previous)
assert seen == [8]
assert len(results) == 1
assert results[0].extracted_content == 'rerun answer'
async def test_beta_agent_rerun_history_honors_retry_and_skip_controls(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.agent.views import ActionResult
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
previous = _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'previous answer'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
success = _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'retry success'}}],
model='gpt-test',
started=3.0,
finished=4.0,
output_model_schema=None,
process_error=None,
)
sleeps = []
async def fake_sleep(delay):
sleeps.append(delay)
monkeypatch.setattr(beta_service.asyncio, 'sleep', fake_sleep)
class ErrorHistory:
def action_results(self):
return [ActionResult(error='history failed')]
def errors(self):
return ['history failed']
agent = Agent(task='rerun retry', max_steps=5)
attempts = []
async def fake_run(max_steps=100, on_step_start=None, on_step_end=None):
attempts.append(max_steps)
if len(attempts) == 1:
raise RuntimeError('process failed')
if len(attempts) == 2:
return ErrorHistory()
return success
agent.run = fake_run
results = await agent.rerun_history(previous, max_retries=3, delay_between_actions=0.1)
assert attempts == [5, 5, 5]
assert sleeps == [0.1, 0.1]
assert results[0].extracted_content == 'retry success'
failing_agent = Agent(task='rerun strict failure', max_steps=2)
strict_attempts = []
sleeps.clear()
async def always_fail(max_steps=100, on_step_start=None, on_step_end=None):
strict_attempts.append(max_steps)
raise RuntimeError('still broken')
failing_agent.run = always_fail
with pytest.raises(RuntimeError, match='Rerun failed after 2 attempts: still broken'):
await failing_agent.rerun_history(previous, max_retries=2, skip_failures=False, delay_between_actions=0.25)
assert strict_attempts == [2, 2]
assert sleeps == [0.25]
skip_agent = Agent(task='rerun skipped failure')
skip_agent.run = always_fail
results = await skip_agent.rerun_history(previous, max_retries=1, skip_failures=True)
assert results[-1].error == 'Rerun failed after 1 attempts: still broken'
async def test_beta_agent_load_and_rerun_loads_saved_rust_history(tmp_path):
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
agent = Agent(task='rerun saved')
history = _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'loaded answer'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
history_file = tmp_path / 'history.json'
history.save_to_file(history_file)
seen = []
async def fake_rerun_history(loaded_history, **kwargs):
seen.append((loaded_history.final_result(), kwargs))
return loaded_history.action_results()
agent.rerun_history = fake_rerun_history
results = await agent.load_and_rerun(history_file, max_retries=2)
assert seen == [('loaded answer', {'max_retries': 2})]
assert results[0].extracted_content == 'loaded answer'
async def test_beta_agent_take_step_runs_one_terminal_turn():
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
agent = Agent(task='step once')
seen = []
async def fake_run(max_steps=100, on_step_start=None, on_step_end=None):
seen.append(max_steps)
return _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'single step answer'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent.run = fake_run
is_done, is_valid = await agent.take_step()
assert seen == [1]
assert is_done is True
assert is_valid is True
async def test_beta_agent_take_step_matches_browser_use_non_final_status():
from browser_use.beta import Agent
agent = Agent(task='step once without finishing')
seen = []
class NonFinalHistory:
def is_done(self):
return False
def has_errors(self):
return False
async def fake_run(max_steps=100, on_step_start=None, on_step_end=None):
seen.append(max_steps)
return NonFinalHistory()
agent.run = fake_run
is_done, is_valid = await agent.take_step()
assert seen == [1]
assert is_done is False
assert is_valid is False
async def test_beta_agent_take_step_executes_initial_actions_on_first_step():
from browser_use.agent.views import AgentStepInfo
from browser_use.beta import Agent
agent = Agent(
task='step once after initial action',
initial_actions=[{'navigate': {'url': 'https://example.com', 'new_tab': False}}],
)
seen = []
class DoneHistory:
def is_done(self):
return True
async def fake_execute_initial_actions():
seen.append('initial_actions')
async def fake_run(max_steps=100, on_step_start=None, on_step_end=None):
seen.append(('run', max_steps))
return DoneHistory()
agent._execute_initial_actions = fake_execute_initial_actions
agent.run = fake_run
is_done, is_valid = await agent.take_step(AgentStepInfo(step_number=0, max_steps=3))
assert seen == ['initial_actions', ('run', 1)]
assert is_done is True
assert is_valid is True
seen.clear()
async def fake_interrupted_initial_actions():
seen.append('interrupted_initial_actions')
raise InterruptedError
agent._execute_initial_actions = fake_interrupted_initial_actions
is_done, is_valid = await agent.take_step(AgentStepInfo(step_number=0, max_steps=3))
assert seen == ['interrupted_initial_actions', ('run', 1)]
assert is_done is True
assert is_valid is True
async def test_beta_agent_run_executes_initial_actions_before_sdk():
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
agent = Agent(
task='start on example',
initial_actions=[{'navigate': {'url': 'https://example.com', 'new_tab': False}}],
)
seen = []
async def fake_execute_initial_actions(*, allow_terminal_run=True):
seen.append(('initial_actions', allow_terminal_run))
async def fake_run_sdk_agent(**kwargs):
seen.append(('sdk', kwargs['max_steps']))
return _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'done'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent._execute_initial_actions = fake_execute_initial_actions
agent._run_sdk_agent = fake_run_sdk_agent
agent._initialize_run_lifecycle_state = lambda: None
history = await agent.run(max_steps=3)
assert seen == [('initial_actions', False), ('sdk', 3)]
assert history.final_result() == 'done'
async def test_beta_agent_run_pre_navigates_cdp_session_before_sdk_by_default():
from types import SimpleNamespace
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
class FakeBrowserSession:
id = 'browser-cdp'
cdp_url = 'wss://cloud-browser.example/devtools/browser/session'
browser_profile = SimpleNamespace(cdp_url=cdp_url)
def __init__(self):
self.calls = []
async def navigate_to(self, url, new_tab=False):
self.calls.append((url, new_tab))
async def get_browser_state_summary(self, include_screenshot=False):
assert include_screenshot is False
return SimpleNamespace(url='https://example.com/start', title='Example Start', tabs=[])
browser_session = FakeBrowserSession()
agent = Agent(
task='read the page',
browser_session=browser_session,
initial_actions=[{'navigate': {'url': 'https://example.com', 'new_tab': False}}],
)
seen = []
async def fake_run_sdk_agent(**kwargs):
seen.append(kwargs['task'])
return _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'done'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent._run_sdk_agent = fake_run_sdk_agent
agent._initialize_run_lifecycle_state = lambda: None
history = await agent.run(max_steps=3)
assert history.final_result() == 'done'
assert browser_session.calls == [('https://example.com', False)]
assert agent._completed_initial_navigation_urls == ['https://example.com']
assert seen[0].startswith("The browser session is already open at 'https://example.com'")
assert "current_url='https://example.com/start'" in seen[0]
assert "title='Example Start'" in seen[0]
assert 'first browser step should inspect or extract from the current page' in seen[0]
assert "First navigate to 'https://example.com'" not in seen[0]
async def test_beta_agent_run_keeps_initial_navigation_when_direct_state_mismatches():
from types import SimpleNamespace
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
class FakeBrowserSession:
id = 'browser-cdp'
cdp_url = 'wss://cloud-browser.example/devtools/browser/session'
browser_profile = SimpleNamespace(cdp_url=cdp_url)
def __init__(self):
self.calls = []
async def navigate_to(self, url, new_tab=False):
self.calls.append((url, new_tab))
async def get_browser_state_summary(self, include_screenshot=False):
assert include_screenshot is False
return SimpleNamespace(url='about:blank', title='', tabs=[])
browser_session = FakeBrowserSession()
agent = Agent(
task='read the page',
browser_session=browser_session,
initial_actions=[{'navigate': {'url': 'https://example.com/start', 'new_tab': False}}],
)
seen = []
async def fake_run_sdk_agent(**kwargs):
seen.append(kwargs['task'])
return _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'done'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent._run_sdk_agent = fake_run_sdk_agent
agent._initialize_run_lifecycle_state = lambda: None
history = await agent.run(max_steps=3)
assert history.final_result() == 'done'
assert browser_session.calls == [('https://example.com/start', False)]
assert agent._completed_initial_navigation_urls == []
assert seen[0].startswith("First navigate to 'https://example.com/start'")
assert 'The browser session is already open at' not in seen[0]
def test_rust_history_uses_browser_script_lifecycle_outputs_as_result():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {}},
{
'event_type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': 'emit_output(page_info(), label="page_info")'},
},
},
{
'event_type': 'browser_script.completed',
'payload': {
'name': 'browser_script',
'ok': True,
'status': 'finished',
'run_id': 'bs-test',
'outputs': [
{
'label': 'page_info',
'value': {'url': 'https://example.com', 'title': 'Example'},
'summary': {'kind': 'observed', 'output_label': 'page_info'},
}
],
},
},
{'event_type': 'model.turn.request', 'payload': {}},
{'event_type': 'session.done', 'payload': {'result': 'done'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
result = history.history[0].result[0]
assert result.extracted_content is not None
assert json.loads(result.extracted_content) == [
{
'label': 'page_info',
'value': {'url': 'https://example.com', 'title': 'Example'},
'summary': {'kind': 'observed', 'output_label': 'page_info'},
}
]
assert history.history[0].state.url == 'https://example.com'
def test_rust_history_uses_printed_browser_script_page_info_as_state():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {}},
{
'event_type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': 'info = page_info()\nprint(info)'},
},
},
{
'event_type': 'browser_script.completed',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'ok': True,
'status': 'finished',
'text': (
"{'url': 'https://example.com/', 'title': 'Example Domain', "
"'target': {'targetId': 'target-1', 'url': 'https://example.com/', 'title': 'Example Domain'}}"
),
},
},
{'event_type': 'model.turn.request', 'payload': {}},
{'event_type': 'session.done', 'payload': {'result': 'done'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
state = history.history[0].state
assert state.url == 'https://example.com/'
assert state.title == 'Example Domain'
assert state.tabs[0].target_id == 'target-1'
async def test_beta_agent_multi_act_preserves_done_action():
from browser_use.beta import Agent
agent = Agent(task='finish manually')
results = await agent.multi_act([{'done': {'text': 'manual answer', 'success': False, 'files_to_display': ['report.txt']}}])
assert len(results) == 1
assert results[0].is_done is True
assert results[0].success is False
assert results[0].extracted_content == 'manual answer'
assert results[0].attachments == ['report.txt']
async def test_beta_agent_multi_act_routes_actions_to_followup():
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
agent = Agent(task='act on current page')
agent.terminal_session_id = '12345678-1234-1234-1234-123456789abc'
seen = []
async def fake_follow_up(task, max_steps=None):
seen.append((task, max_steps))
return _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'actions applied'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent.follow_up = fake_follow_up
results = await agent.multi_act(
[
{'click_element': {'index': 2}},
{'input_text': {'index': 3, 'text': 'hello'}},
]
)
assert seen[0][1] == 2
assert 'Browser Use action models' in seen[0][0]
assert '"click_element"' in seen[0][0]
assert '"input_text"' in seen[0][0]
assert results[0].extracted_content == 'actions applied'
async def test_beta_agent_multi_act_ignores_later_done_actions():
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
agent = Agent(task='act without premature done')
agent.terminal_session_id = '12345678-1234-1234-1234-123456789abc'
seen = []
async def fake_follow_up(task, max_steps=None):
seen.append((task, max_steps))
return _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'clicked only'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
agent.follow_up = fake_follow_up
results = await agent.multi_act(
[
{'click_element': {'index': 2}},
{'done': {'text': 'should not execute', 'success': True}},
{'input_text': {'index': 3, 'text': 'should not execute'}},
]
)
assert seen[0][1] == 1
assert '"click_element"' in seen[0][0]
assert '"done"' not in seen[0][0]
assert 'should not execute' not in seen[0][0]
assert '"input_text"' not in seen[0][0]
assert results[0].extracted_content == 'clicked only'
async def test_beta_agent_initial_actions_can_pre_navigate_existing_cdp_session(monkeypatch):
from types import SimpleNamespace
from browser_use.beta import Agent
class FakeBrowserSession:
id = 'browser-cdp'
cdp_url = 'wss://cloud-browser.example/devtools/browser/session'
browser_profile = SimpleNamespace(cdp_url=cdp_url)
def __init__(self):
self.calls = []
async def navigate_to(self, url, new_tab=False):
self.calls.append((url, new_tab))
async def get_browser_state_summary(self, include_screenshot=False):
assert include_screenshot is False
return SimpleNamespace(url='https://example.com', title='Example Domain', tabs=[])
browser_session = FakeBrowserSession()
agent = Agent(
task='read the page',
browser_session=browser_session,
initial_actions=[{'navigate': {'url': 'https://example.com', 'new_tab': False}}],
)
monkeypatch.setenv('BROWSER_USE_RUST_DIRECT_INITIAL_NAVIGATION', '1')
await agent._execute_initial_actions(allow_terminal_run=False)
assert browser_session.calls == [('https://example.com', False)]
assert agent._initial_actions_executed is True
assert agent._completed_initial_navigation_urls == ['https://example.com']
assert (
agent.state.last_result[0].extracted_content
== 'Navigated to https://example.com. Current page: https://example.com (Example Domain)'
)
assert agent.history.history[0].metadata.step_number == 0
assert agent.history.history[0].model_output.action == agent.initial_actions
async def test_beta_agent_direct_initial_navigation_defaults_on_for_cdp(monkeypatch):
from types import SimpleNamespace
from browser_use.beta import Agent
class FakeBrowserSession:
id = 'browser-cdp'
cdp_url = 'wss://cloud-browser.example/devtools/browser/session'
browser_profile = SimpleNamespace(cdp_url=cdp_url)
def __init__(self):
self.calls = []
async def navigate_to(self, url, new_tab=False):
self.calls.append((url, new_tab))
async def get_browser_state_summary(self, include_screenshot=False):
assert include_screenshot is False
return SimpleNamespace(url='https://example.com', title='Example Domain', tabs=[])
browser_session = FakeBrowserSession()
agent = Agent(
task='read the page',
browser_session=browser_session,
initial_actions=[{'navigate': {'url': 'https://example.com', 'new_tab': False}}],
)
monkeypatch.delenv('BROWSER_USE_RUST_DIRECT_INITIAL_NAVIGATION', raising=False)
await agent._execute_initial_actions(allow_terminal_run=False)
assert browser_session.calls == [('https://example.com', False)]
assert agent._initial_actions_executed is True
assert agent._completed_initial_navigation_urls == ['https://example.com']
assert (
agent.state.last_result[0].extracted_content
== 'Navigated to https://example.com. Current page: https://example.com (Example Domain)'
)
async def test_beta_agent_direct_initial_navigation_can_be_disabled(monkeypatch):
from types import SimpleNamespace
from browser_use.beta import Agent
class FakeBrowserSession:
id = 'browser-cdp'
cdp_url = 'wss://cloud-browser.example/devtools/browser/session'
browser_profile = SimpleNamespace(cdp_url=cdp_url)
def __init__(self):
self.calls = []
async def navigate_to(self, url, new_tab=False):
self.calls.append((url, new_tab))
browser_session = FakeBrowserSession()
agent = Agent(
task='read the page',
browser_session=browser_session,
initial_actions=[{'navigate': {'url': 'https://example.com', 'new_tab': False}}],
)
monkeypatch.setenv('BROWSER_USE_RUST_DIRECT_INITIAL_NAVIGATION', '0')
await agent._execute_initial_actions(allow_terminal_run=False)
assert browser_session.calls == []
assert agent._initial_actions_executed is False
assert agent.history.history == []
async def test_beta_agent_records_initial_actions_in_history(monkeypatch):
from browser_use.agent.views import ActionResult
from browser_use.beta import Agent
class RecordingLogger:
def __init__(self):
self.debugs = []
def debug(self, message, *args, **kwargs):
self.debugs.append(message)
def info(self, message, *args, **kwargs):
pass
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
class BrowserProfile:
downloads_path = None
class BrowserSession:
browser_profile = BrowserProfile()
agent = Agent(
task='Replay actions.',
browser_session=BrowserSession(),
initial_actions=[{'navigate': {'url': 'https://example.com', 'new_tab': False}}],
)
agent.initial_url = 'https://example.com'
initial_calls = []
async def fake_initial_multi_act(actions):
initial_calls.append(actions)
return [ActionResult(long_term_memory='loaded')]
agent.multi_act = fake_initial_multi_act
await agent._execute_initial_actions()
assert initial_calls == [agent.initial_actions]
assert agent.state.last_result[0].long_term_memory == 'Found initial url and automatically loaded it. loaded'
assert agent.history.history[0].metadata.step_number == 0
assert agent.history.history[0].state.url == 'https://example.com'
assert agent.history.history[0].model_output.action == agent.initial_actions
assert logger.debugs == [
'⚡ Executing 1 initial actions...',
'📝 Saved initial actions to history as step 0',
'Initial actions completed',
]
async def test_beta_agent_exposes_model_output_helper_methods(monkeypatch, tmp_path):
from types import SimpleNamespace
from browser_use.agent.views import ActionResult
from browser_use.beta import Agent
from browser_use.llm.messages import UserMessage
class RecordingLogger:
def __init__(self):
self.debugs = []
self.warnings = []
def debug(self, message, *args, **kwargs):
self.debugs.append(message)
def warning(self, message, *args, **kwargs):
self.warnings.append(message)
def info(self, message, *args, **kwargs):
pass
def isEnabledFor(self, level):
return True
logger = RecordingLogger()
monkeypatch.setattr(Agent, 'logger', property(lambda self: logger))
original_url = 'https://example.com/path?abcdefghijklmnopqrstuvwxyz#section'
class LLM:
model = 'gpt-test'
provider = 'test-provider'
def __init__(self):
self.agent = None
self.calls = []
async def ainvoke(self, messages, output_format=None, **kwargs):
self.calls.append((messages, output_format))
shortened_text = messages[0].content
return SimpleNamespace(
usage=None,
completion=self.agent.AgentOutput(
evaluation_previous_goal='ok',
memory='remember',
next_goal='finish',
action=[
self.agent.ActionModel(done={'text': shortened_text, 'success': True}),
self.agent.ActionModel(done={'text': 'extra action', 'success': True}),
],
),
)
seen_callbacks = []
async def new_step_callback(browser_state, model_output, step_number):
seen_callbacks.append((browser_state, model_output, step_number))
llm = LLM()
agent = Agent(
task='Use LLM helpers.',
llm=llm,
max_actions_per_step=1,
_url_shortening_limit=8,
save_conversation_path=tmp_path / 'conversations',
register_new_step_callback=new_step_callback,
)
llm.agent = agent
input_messages = [UserMessage(content=f'Open {original_url} now.')]
browser_state = SimpleNamespace(url='https://example.com')
agent._message_manager.get_messages = lambda: input_messages
await agent._get_next_action(browser_state)
assert llm.calls[0][1] is agent.AgentOutput
assert original_url not in llm.calls[0][0][0].content
assert len(agent.state.last_model_output.action) == 1
assert agent.state.last_model_output.action[0].model_dump(exclude_unset=True)['done']['text'] == f'Open {original_url} now.'
assert seen_callbacks == [(browser_state, agent.state.last_model_output, agent.state.n_steps)]
assert list((tmp_path / 'conversations').glob(f'conversation_{agent.id}_{agent.state.n_steps}.txt'))
seen_actions = []
async def fake_multi_act(actions):
seen_actions.append(actions)
return [ActionResult(extracted_content='executed')]
agent.multi_act = fake_multi_act
await agent._execute_actions()
assert seen_actions == [agent.state.last_model_output.action]
assert agent.state.last_result[0].extracted_content == 'executed'
class RetryLLM(LLM):
async def ainvoke(self, messages, output_format=None, **kwargs):
self.calls.append((messages, output_format))
if len(self.calls) == 1:
return SimpleNamespace(
usage=None,
completion=self.agent.AgentOutput(
evaluation_previous_goal='empty',
memory='none',
next_goal='retry',
action=[],
),
)
return SimpleNamespace(
usage=None,
completion=self.agent.AgentOutput(
evaluation_previous_goal='ok',
memory='remember',
next_goal='finish',
action=[self.agent.ActionModel(done={'text': 'retried action', 'success': True})],
),
)
retry_llm = RetryLLM()
retry_agent = Agent(task='Retry LLM helpers.', llm=retry_llm)
retry_llm.agent = retry_agent
logger.debugs.clear()
logger.warnings.clear()
retry_output = await retry_agent._get_model_output_with_retry([UserMessage(content='Retry with an action.')])
assert len(retry_llm.calls) == 2
assert retry_output.action
assert logger.debugs[0] == '✅ Step 1: Got LLM response with 0 actions'
assert any('Next actions: done' in message for message in logger.debugs)
assert logger.warnings == ['Model returned empty action. Retrying...']
class EmptyRetryLLM(LLM):
async def ainvoke(self, messages, output_format=None, **kwargs):
self.calls.append((messages, output_format))
return SimpleNamespace(
usage=None,
completion=self.agent.AgentOutput(
evaluation_previous_goal='empty',
memory='none',
next_goal='retry',
action=[],
),
)
empty_retry_llm = EmptyRetryLLM()
empty_retry_agent = Agent(task='Retry empty LLM helpers.', llm=empty_retry_llm)
empty_retry_llm.agent = empty_retry_agent
logger.debugs.clear()
logger.warnings.clear()
empty_retry_output = await empty_retry_agent._get_model_output_with_retry([UserMessage(content='Retry empty action.')])
assert len(empty_retry_llm.calls) == 2
assert empty_retry_output.action[0].model_dump(exclude_unset=True)['done']['text'] == 'No next action returned by LLM!'
assert logger.debugs == ['✅ Step 1: Got LLM response with 0 actions']
assert logger.warnings == [
'Model returned empty action. Retrying...',
'Model still returned empty after retry. Inserting safe noop action.',
]
async def test_beta_agent_get_model_output_logs_response_like_browser_use(monkeypatch):
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
from browser_use.llm.messages import UserMessage
class RecordingLogger:
def __init__(self):
self.debugs = []
self.infos = []
def debug(self, message, *args, **kwargs):
self.debugs.append(message)
def info(self, message, *args, **kwargs):
self.infos.append(message)
def warning(self, message, *args, **kwargs):
pass
def isEnabledFor(self, level):
return False
class LLM:
model = 'gpt-test'
provider = 'test-provider'
def __init__(self):
self.agent = None
async def ainvoke(self, messages, output_format=None, **kwargs):
return type(
'Result',
(),
{
'usage': None,
'completion': self.agent.AgentOutput(
evaluation_previous_goal='neutral',
memory='remember response logging',
next_goal='finish response logging',
action=[self.agent.ActionModel(done={'text': 'logged', 'success': True})],
),
},
)()
browser_use_logger = RecordingLogger()
beta_logger = RecordingLogger()
monkeypatch.setattr(BrowserUseAgent, 'logger', property(lambda self: browser_use_logger))
monkeypatch.setattr(BetaAgent, 'logger', property(lambda self: beta_logger))
browser_use_llm = LLM()
browser_use_agent = BrowserUseAgent(task='Log the model output.', llm=browser_use_llm, directly_open_url=False)
browser_use_llm.agent = browser_use_agent
await browser_use_agent.get_model_output([UserMessage(content='log response')])
beta_llm = LLM()
beta_agent = BetaAgent(task='Log the model output.', llm=beta_llm, directly_open_url=False)
beta_llm.agent = beta_agent
await beta_agent.get_model_output([UserMessage(content='log response')])
assert beta_logger.infos == browser_use_logger.infos
assert any('Eval: neutral' in message for message in beta_logger.infos)
assert any('Memory: remember response logging' in message for message in beta_logger.infos)
assert any('Next goal: finish response logging' in message for message in beta_logger.infos)
async def test_beta_agent_exposes_prepare_context_helper_method(monkeypatch):
from types import SimpleNamespace
from browser_use.agent.views import AgentStepInfo
from browser_use.beta import Agent
state_requests = []
browser_state = SimpleNamespace(
url='https://example.com/settings',
title='Settings',
screenshot='base64-screen',
dom_state=SimpleNamespace(selector_map={1: object(), 2: object()}),
)
class BrowserProfile:
downloads_path = None
class BrowserSession:
browser_profile = BrowserProfile()
async def get_browser_state_summary(self, include_screenshot=False, include_recent_events=False):
state_requests.append((include_screenshot, include_recent_events))
return browser_state
agent = Agent(
task='Prepare context.',
browser_session=BrowserSession(),
include_recent_events=True,
use_vision=False,
available_file_paths=['/tmp/input.txt'],
sensitive_data={'api_key': 'secret-value'},
)
download_contexts = []
updated_pages = []
prompt_urls = []
created_messages = []
async def fake_check_downloads(context=''):
download_contexts.append(context)
async def fake_update_action_models(page_url):
updated_pages.append(page_url)
def fake_prompt_description(page_url):
prompt_urls.append(page_url)
return 'filtered action prompt'
def fake_create_state_messages(**kwargs):
created_messages.append(kwargs)
monkeypatch.setattr(agent, '_check_and_update_downloads', fake_check_downloads)
monkeypatch.setattr(agent, '_update_action_models_for_page', fake_update_action_models)
monkeypatch.setattr(agent.tools.registry, 'get_prompt_description', fake_prompt_description)
monkeypatch.setattr(agent._message_manager, 'create_state_messages', fake_create_state_messages)
step_info = AgentStepInfo(step_number=2, max_steps=3)
result = await agent._prepare_context(step_info)
assert result is browser_state
assert state_requests == [(True, True)]
assert download_contexts == ['Step 1: after getting browser state']
assert updated_pages == ['https://example.com/settings']
assert prompt_urls == ['https://example.com/settings']
assert created_messages == [
{
'browser_state_summary': browser_state,
'model_output': agent.state.last_model_output,
'result': agent.state.last_result,
'step_info': step_info,
'use_vision': True,
'page_filtered_actions': 'filtered action prompt',
'sensitive_data': {'api_key': 'secret-value'},
'available_file_paths': ['/tmp/input.txt'],
}
]
assert agent.AgentOutput is agent.DoneAgentOutput
async def test_beta_agent_trace_and_cloud_auth_helpers():
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
agent = Agent(task='Open example.com', llm=type('LLM', (), {'model': 'gpt-test'})(), task_id='task-1')
agent.history = _history_from_events(
[
{'event_type': 'browser.state', 'payload': {'url': 'https://example.com', 'title': 'Example'}},
{'event_type': 'session.done', 'payload': {'result': 'trace answer'}},
],
model='gpt-test',
started=1.0,
finished=3.0,
output_model_schema=None,
process_error=None,
)
trace_object = agent.get_trace_object()
assert await agent.authenticate_cloud_sync() is False
assert set(trace_object) == {'trace', 'trace_details'}
assert trace_object['trace']['model'] == 'gpt-test'
assert trace_object['trace']['task_id'] == 'task-1'
assert trace_object['trace']['final_result_response_truncated'] == 'trace answer'
assert trace_object['trace']['self_report_completed'] == 1
assert trace_object['trace_details']['final_result_response'] == 'trace answer'
assert 'trace answer' in trace_object['trace_details']['complete_history']
async def test_beta_agent_run_exposes_laminar_trace_id_for_eval_links(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.beta import Agent
class FakeLaminar:
@staticmethod
def is_initialized():
return True
@staticmethod
def get_trace_id():
return '01234567-89ab-cdef-0123-456789abcdef'
async def fake_run_terminal(self, max_steps, on_step_start, on_step_end):
return self.history
monkeypatch.setattr(beta_service, 'Laminar', FakeLaminar)
monkeypatch.setattr(beta_service.Agent, '_run_terminal', fake_run_terminal)
agent = Agent(task='Trace link task.', llm=type('LLM', (), {'model': 'claude-sonnet-4-6'})())
await agent.run(max_steps=3)
assert agent.laminar_trace_id == '01234567-89ab-cdef-0123-456789abcdef'
def test_rust_laminar_replay_flush_avoids_context_reset(monkeypatch):
import browser_use.beta.service as beta_service
class FakeLaminar:
flushes = 0
force_flushes = 0
@staticmethod
def is_initialized():
return True
@classmethod
def flush(cls):
cls.flushes += 1
@classmethod
def force_flush(cls):
cls.force_flushes += 1
monkeypatch.setattr(beta_service, 'Laminar', FakeLaminar)
beta_service._laminar_force_flush()
assert FakeLaminar.flushes == 1
assert FakeLaminar.force_flushes == 0
def test_beta_agent_laminar_run_summary_populates_current_span(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
class FakeLaminar:
attributes = []
outputs = []
events = []
spans = []
flushes = 0
current_span = None
@staticmethod
def is_initialized():
return True
@classmethod
def set_span_attributes(cls, attributes):
if cls.current_span is not None:
cls.current_span['attributes'].append(attributes)
else:
cls.attributes.append(attributes)
@classmethod
def set_span_output(cls, output):
if cls.current_span is not None:
cls.current_span['outputs'].append(output)
else:
cls.outputs.append(output)
@classmethod
def event(cls, name, attributes=None):
cls.events.append((name, attributes))
@classmethod
def flush(cls):
cls.flushes += 1
@classmethod
def start_as_current_span(cls, name, input=None, span_type='DEFAULT'):
record = {'name': name, 'input': input, 'span_type': span_type, 'attributes': [], 'outputs': []}
class Span:
def __enter__(self):
cls.current_span = record
cls.spans.append(record)
return self
def __exit__(self, exc_type, exc, tb):
cls.current_span = None
return False
return Span()
monkeypatch.setattr(beta_service, 'Laminar', FakeLaminar)
agent = Agent(task='Trace terminal observability.', llm=type('LLM', (), {'model': 'claude-sonnet-4-6'})())
agent.terminal_session_id = 'terminal-session-1'
agent.last_events = [
{
'event_type': 'model.turn.request',
'ts_ms': 1000,
'payload': {
'model': 'claude-sonnet-4-6',
'provider': 'test-provider',
'turn_idx': 0,
'composition': {
'system_prompt_tokens': 100,
'tools': [{'name': 'browser'}, {'name': 'done'}],
},
'llm_input': {
'system': [{'text': 'System prompt'}],
'tools': [
{
'name': 'browser_script',
'description': 'Run browser Python. Use click_at_xy(x, y) to click visible page coordinates.',
'input_schema': {
'type': 'object',
'properties': {'code': {'type': 'string'}},
'required': ['code'],
},
}
],
'messages': [
{
'role': 'user',
'content': [
{'type': 'text', 'text': 'Find the title on example.com'},
{
'type': 'media',
'mime_type': 'image/png',
'data': 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB',
'detail': 'low',
},
],
}
],
'message_count': 1,
'omitted_earlier_messages': 0,
},
},
},
{
'event_type': 'tool.started',
'ts_ms': 1200,
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': 'emit_output(page_info(), label="page_info")'},
},
},
{
'event_type': 'tool.output',
'ts_ms': 1400,
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'ok': True,
'text': '',
'summary': [{'kind': 'page', 'title': 'Example Domain', 'url': 'https://example.com'}],
'outputs': [{'label': 'page_info', 'value': {'title': 'Example Domain'}}],
'content': [
{'type': 'input_text', 'text': 'Example Domain page info'},
{'type': 'input_image', 'image_url': 'data:image/png;base64,iVBORw0KGgo=', 'detail': 'low'},
],
},
},
{'event_type': 'model.stream_delta', 'ts_ms': 1500, 'payload': {'text': 'laminar answer'}},
{
'event_type': 'token_count',
'ts_ms': 2000,
'payload': {
'info': {
'last_token_usage': {
'cached_input_tokens': 5,
'input_cache_creation_tokens': 13,
'input_tokens': 100,
'output_tokens': 20,
'total_tokens': 133,
},
'total_token_usage': {
'cached_input_tokens': 5,
'input_cache_creation_tokens': 13,
'input_tokens': 100,
'output_tokens': 20,
'total_tokens': 133,
},
},
},
},
{'event_type': 'session.done', 'payload': {'result': 'laminar answer'}},
]
agent.history = _history_from_events(
agent.last_events,
model='claude-sonnet-4-6',
started=1.0,
finished=4.0,
output_model_schema=None,
process_error=None,
)
agent._record_laminar_run_observability(max_steps=7, duration_seconds=3.0)
assert FakeLaminar.attributes[-1]['runtime'] == 'browser_use.beta'
assert FakeLaminar.attributes[-1]['terminal_session_id'] == 'terminal-session-1'
assert FakeLaminar.outputs[-1]['final_result_preview'] == 'laminar answer'
assert FakeLaminar.outputs[-1]['steps'] == agent.history.number_of_steps()
assert FakeLaminar.outputs[-1]['terminal_events_count'] == 6
assert FakeLaminar.events[-1][0] == 'agent.run.terminal_summary'
assert FakeLaminar.spans[0]['name'] == 'rust_core.llm'
assert FakeLaminar.spans[0]['span_type'] == 'LLM'
llm_input = FakeLaminar.spans[0]['input']
assert isinstance(llm_input, list)
assert llm_input[0]['role'] == 'system'
assert llm_input[0]['content'][0]['text'] == 'System prompt'
assert llm_input[1]['content'][0]['text'] == 'Find the title on example.com'
assert llm_input[1]['content'][1] == {
'type': 'image_url',
'image_url': {
'url': 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB',
'detail': 'low',
},
}
span_attrs = {}
for attributes in FakeLaminar.spans[0]['attributes']:
span_attrs.update(attributes)
assert span_attrs['tools_count'] == 1
assert 'browser_script' in span_attrs['tool_names']
assert span_attrs['input_tokens'] == 100
assert span_attrs['cached_input_tokens'] == 5
assert span_attrs['cache_creation_input_tokens'] == 13
assert span_attrs['output_tokens'] == 20
assert span_attrs['total_tokens'] == 133
assert span_attrs['input_cached_cost_usd'] == 5 * (0.30 / 1_000_000)
assert span_attrs['input_cache_creation_cost_usd'] == 13 * (3.75 / 1_000_000)
assert span_attrs['gen_ai.operation.name'] == 'chat'
assert span_attrs['gen_ai.request.model'] == 'claude-sonnet-4-6'
assert span_attrs['gen_ai.prompt.0.role'] == 'system'
assert span_attrs['gen_ai.prompt.0.content'] == 'System prompt'
assert span_attrs['gen_ai.prompt.1.role'] == 'user'
assert span_attrs['gen_ai.prompt.1.content'] == 'Find the title on example.com\n[image]'
assert span_attrs['gen_ai.completion.0.role'] == 'assistant'
assert span_attrs['gen_ai.completion.0.content'] == 'laminar answer'
assert span_attrs['gen_ai.usage.input_tokens'] == 100
assert span_attrs['gen_ai.usage.input_cached_tokens'] == 5
assert span_attrs['gen_ai.usage.cache_creation_input_tokens'] == 13
assert span_attrs['gen_ai.usage.output_tokens'] == 20
assert span_attrs['gen_ai.usage.total_tokens'] == 133
assert span_attrs['llm.request.functions.0.name'] == 'browser_script'
assert 'click_at_xy' in span_attrs['llm.request.functions.0.description']
assert '"code"' in span_attrs['llm.request.functions.0.input_schema']
assert span_attrs['gen_ai.request.tools.0.name'] == 'browser_script'
assert 'click_at_xy' in span_attrs['gen_ai.request.tools.0.description']
assert '"browser_script"' in span_attrs['gen_ai.tool.definitions']
assert 'click_at_xy' in span_attrs['gen_ai.tool.definitions']
assert '"browser_script"' in span_attrs['gen_ai.request.tools']
assert '"System prompt"' in span_attrs['gen_ai.input.messages']
assert '[image in span input]' in span_attrs['gen_ai.input.messages']
assert 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB' not in span_attrs['gen_ai.input.messages']
assert '"laminar answer"' in span_attrs['gen_ai.output.messages']
assert span_attrs['assistant_output_preview'] == 'laminar answer'
assert FakeLaminar.spans[0]['outputs'][-1][0]['content'][0]['text'] == 'laminar answer'
assert FakeLaminar.spans[1]['name'] == 'rust_core.tool.browser_script'
assert FakeLaminar.spans[1]['span_type'] == 'TOOL'
tool_attrs = {}
for attributes in FakeLaminar.spans[1]['attributes']:
tool_attrs.update(attributes)
assert tool_attrs['tool_index'] == 1
assert FakeLaminar.spans[1]['input'][0]['tool_calls'][0]['arguments']['code'] == 'emit_output(page_info(), label="page_info")'
assert FakeLaminar.spans[1]['outputs'][-1][0]['content'][0] == {'type': 'text', 'text': 'Example Domain page info'}
assert FakeLaminar.spans[1]['outputs'][-1][0]['content'][1] == {
'type': 'image_url',
'image_url': {'url': 'data:image/png;base64,iVBORw0KGgo=', 'detail': 'low'},
}
assert FakeLaminar.flushes == 2
def test_beta_agent_laminar_tool_span_preserves_image_only_outputs(tmp_path):
import browser_use.beta.service as beta_service
image_path = tmp_path / 'tool.png'
image_path.write_bytes(b'\x89PNG\r\n\x1a\n')
events = [
{
'event_type': 'tool.started',
'payload': {'name': 'browser_script', 'tool_call_id': 'call-1', 'arguments': {'code': 'observe()'}},
},
{
'event_type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-1',
'ok': True,
'text': '',
'images': [{'path': str(image_path), 'mime_type': 'image/png'}],
},
},
]
class FakeLaminar:
spans = []
flushes = 0
current_span = None
@staticmethod
def is_initialized():
return True
@classmethod
def set_span_attributes(cls, attributes):
if cls.current_span is not None:
cls.current_span['attributes'].append(attributes)
@classmethod
def set_span_output(cls, output):
if cls.current_span is not None:
cls.current_span['outputs'].append(output)
@classmethod
def event(cls, name, attributes=None):
return None
@classmethod
def flush(cls):
cls.flushes += 1
@classmethod
def start_as_current_span(cls, name, input=None, span_type='DEFAULT'):
record = {'name': name, 'input': input, 'span_type': span_type, 'attributes': [], 'outputs': []}
class Span:
def __enter__(self):
cls.current_span = record
cls.spans.append(record)
return self
def __exit__(self, exc_type, exc, tb):
cls.current_span = None
return False
return Span()
original = beta_service.Laminar
try:
beta_service.Laminar = FakeLaminar
beta_service._record_laminar_terminal_tool_spans(events, max_spans=10)
finally:
beta_service.Laminar = original
output = FakeLaminar.spans[0]['outputs'][-1]
assert output[0]['content'] == [{'type': 'image_url', 'image_url': {'url': 'data:image/png;base64,iVBORw0KGgo='}}]
assert FakeLaminar.flushes == 1
async def test_beta_agent_authenticate_cloud_sync_logs_browser_use_warning(monkeypatch):
from browser_use.agent.service import _PythonAgent as BrowserUseAgent
from browser_use.beta import Agent as BetaAgent
class LLM:
model = 'gpt-test'
provider = 'test'
async def ainvoke(self, messages, output_format=None, **kwargs):
return type('Result', (), {'usage': None})()
class RecordingLogger:
def __init__(self):
self.warnings = []
def debug(self, message, *args, **kwargs):
pass
def info(self, message, *args, **kwargs):
pass
def warning(self, message, *args, **kwargs):
self.warnings.append(message)
def error(self, message, *args, **kwargs):
pass
browser_use_logger = RecordingLogger()
beta_logger = RecordingLogger()
monkeypatch.setattr(BrowserUseAgent, 'logger', property(lambda self: browser_use_logger))
monkeypatch.setattr(BetaAgent, 'logger', property(lambda self: beta_logger))
browser_use_agent = BrowserUseAgent(task='Cloud sync warning parity.', llm=LLM(), directly_open_url=False)
beta_agent = BetaAgent(task='Cloud sync warning parity.', llm=LLM(), directly_open_url=False)
assert await browser_use_agent.authenticate_cloud_sync(show_instructions=False) is False
assert await beta_agent.authenticate_cloud_sync(show_instructions=False) is False
assert beta_logger.warnings == browser_use_logger.warnings
assert beta_logger.warnings == ['Cloud sync has been removed and is no longer available']
def test_beta_agent_trace_metadata_matches_browser_use_helpers(monkeypatch):
import browser_use.beta.service as beta_service
from browser_use.beta import Agent
from browser_use.beta.service import _history_from_events
monkeypatch.setattr(beta_service, 'get_browser_use_version', lambda: '9.9.9-test')
monkeypatch.setattr(beta_service, 'get_git_info', lambda: {'branch': 'trace-branch', 'commit_hash': 'abc123'})
agent = Agent(task='Trace metadata.', llm=type('LLM', (), {'model': 'gpt-test'})())
agent.history = _history_from_events(
[{'event_type': 'session.done', 'payload': {'result': 'trace metadata answer'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
trace = agent.get_trace_object()['trace']
assert trace['browser_use_version'] == '9.9.9-test'
assert json.loads(trace['git_info']) == {'branch': 'trace-branch', 'commit_hash': 'abc123'}
def test_beta_agent_initializes_action_models_without_conversation_path():
from browser_use.beta import Agent
agent = Agent(
task='Initialize action models without saving conversations.',
llm=type('LLM', (), {'model': 'gpt-test'})(),
directly_open_url=False,
)
assert agent.settings.save_conversation_path is None
assert agent.AgentOutput is not None
assert agent.DoneAgentOutput is not None
def test_rust_history_marks_process_failure_not_done():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error='terminal failed',
)
assert history.final_result() is None
assert history.is_done() is False
assert history.is_successful() is None
assert history.errors() == ['terminal failed']
def test_rust_history_process_failure_ignores_empty_stream_text():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'model.stream_delta', 'payload': {'text': None}},
{'event_type': 'token_count', 'payload': {'info': {'last_token_usage': {'input_tokens': 1}}}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error='terminal failed after empty stream delta',
)
assert history.final_result() is None
assert history.is_done() is False
assert history.errors() == ['terminal failed after empty stream delta']
def test_rust_history_marks_missing_terminal_result_as_error():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}}],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.final_result() is None
assert history.is_done() is False
assert history.errors() == ['Rust terminal session did not produce a final result.']
def test_rust_history_surfaces_terminal_stream_error_message():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'stream_error', 'payload': {'message': 'provider error'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.final_result() is None
assert history.is_done() is False
assert history.errors() == ['provider error']
def test_rust_history_surfaces_terminal_operational_failure_events():
from browser_use.beta.service import _history_from_events
failed = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'browser.cloud_shutdown_failed', 'payload': {'error': 'cloud browser shutdown failed'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
final_not_ready = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'session.final_answer_not_ready_at_max_turns', 'payload': {}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
successful_with_cleanup_warning = _history_from_events(
[
{'event_type': 'session.done', 'payload': {'result': 'final answer'}},
{'event_type': 'browser.cleanup_failed', 'payload': {'error': 'cleanup panicked'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert failed.final_result() is None
assert failed.is_done() is False
assert failed.errors() == ['browser cloud shutdown failed: cloud browser shutdown failed']
assert failed.action_results()[-1].error == 'browser cloud shutdown failed: cloud browser shutdown failed'
assert final_not_ready.final_result() is None
assert final_not_ready.is_done() is False
assert final_not_ready.errors() == ['final answer artifact is not ready']
assert successful_with_cleanup_warning.final_result() == 'final answer'
assert successful_with_cleanup_warning.is_done() is True
assert successful_with_cleanup_warning.errors() == [None]
def test_rust_history_surfaces_terminal_subagent_failure_events():
from browser_use.beta.service import _history_from_events
failed = _history_from_events(
[
{
'event_type': 'agent.failed',
'payload': {
'child_session_id': 'child-1',
'status': 'failed',
'payload': {
'child_session_id': 'child-1',
'status': 'failed',
'failure': 'child task failed',
},
},
}
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
cancelled = _history_from_events(
[
{
'event_type': 'agent.cancelled',
'payload': {
'child_session_id': 'child-2',
'status': 'cancelled',
'payload': {
'child_session_id': 'child-2',
'status': 'cancelled',
},
},
}
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert failed.final_result() is None
assert failed.is_done() is False
assert failed.errors() == ['child task failed']
assert failed.action_results()[-1].error == 'child task failed'
assert cancelled.final_result() is None
assert cancelled.is_done() is False
assert cancelled.errors() == ['Subagent was cancelled.']
assert cancelled.action_results()[-1].error == 'Subagent was cancelled.'
def test_rust_history_surfaces_terminal_tool_failure_message():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{
'type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': "goto_url('https://example.invalid')"},
},
},
{
'type': 'tool.failed',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'error': 'RuntimeError: navigation failed',
},
},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.final_result() is None
assert history.is_done() is False
assert history.errors() == ['browser_script failed: RuntimeError: navigation failed']
assert history.action_results()[-1].error == 'browser_script failed: RuntimeError: navigation failed'
def test_rust_history_surfaces_running_browser_script_observe_instruction():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{
'event_type': 'tool.started',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'arguments': {'code': "goto_url('https://example.com')\nwait_for_load(30)"},
},
},
{
'event_type': 'tool.output',
'payload': {
'name': 'browser_script',
'tool_call_id': 'call-browser',
'ok': True,
'status': 'running',
'run_id': 'bs-123',
'next_observe_ms': 7000,
'text': '',
},
},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
tool_result = history.action_results()[0]
assert tool_result.error is None
assert tool_result.extracted_content is not None
assert 'browser_script is still running.' in tool_result.extracted_content
assert 'run_id: bs-123' in tool_result.extracted_content
assert 'action="observe"' in tool_result.extracted_content
assert 'observe_timeout_ms=7000' in tool_result.extracted_content
def test_rust_history_surfaces_terminal_cancellation_and_tool_abort_messages():
from browser_use.beta.service import _history_from_events
cancelled = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'session.cancelled', 'payload': {'reason': 'user requested stop'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
aborted = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{
'type': 'tool.started',
'payload': {
'name': 'browser',
'tool_call_id': 'call-browser',
'arguments': {'cmd': 'connect managed --headless'},
},
},
{
'type': 'tool.aborted',
'payload': {
'name': 'browser',
'tool_call_id': 'call-browser',
'error': 'aborted by user',
},
},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert cancelled.final_result() is None
assert cancelled.is_done() is False
assert cancelled.errors() == ['Rust terminal session was cancelled: user requested stop']
assert cancelled.action_results()[-1].error == 'Rust terminal session was cancelled: user requested stop'
assert aborted.final_result() is None
assert aborted.is_done() is False
assert aborted.errors() == ['browser aborted: aborted by user']
assert aborted.action_results()[0].error == 'browser aborted: aborted by user'
assert aborted.action_results()[-1].error == 'browser aborted: aborted by user'
def test_rust_history_preserves_terminal_tool_abort_when_failed_event_follows():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{
'type': 'tool.started',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'arguments': {'cmd': 'sleep 60'},
},
},
{
'type': 'tool.aborted',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'error': 'aborted by user after 1.0s',
},
},
{
'type': 'tool.failed',
'payload': {
'name': 'exec_command',
'tool_call_id': 'call-exec',
'error': 'aborted by user after 1.0s',
},
},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.final_result() is None
assert history.is_done() is False
assert history.errors() == ['exec_command aborted: aborted by user after 1.0s']
assert history.action_results()[0].error == 'exec_command aborted: aborted by user after 1.0s'
assert history.action_results()[-1].error == 'exec_command aborted: aborted by user after 1.0s'
def test_rust_history_surfaces_terminal_session_interrupted_message():
from browser_use.beta.service import _history_from_events
history = _history_from_events(
[
{'event_type': 'model.turn.request', 'payload': {'model': 'gpt-test'}},
{'event_type': 'session.interrupted', 'payload': {'reason': 'interrupted by send_input'}},
],
model='gpt-test',
started=1.0,
finished=2.0,
output_model_schema=None,
process_error=None,
)
assert history.final_result() is None
assert history.is_done() is False
assert history.errors() == ['Rust terminal session was interrupted: interrupted by send_input']
assert history.action_results()[-1].error == 'Rust terminal session was interrupted: interrupted by send_input'