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
7908 lines
241 KiB
Python
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'
|