vibesurf 0.1.36__py3-none-any.whl → 0.1.37__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
vibe_surf/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.36'
32
- __version_tuple__ = version_tuple = (0, 1, 36)
31
+ __version__ = version = '0.1.37'
32
+ __version_tuple__ = version_tuple = (0, 1, 37)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -72,7 +72,7 @@ from browser_use.utils import (
72
72
  time_execution_async,
73
73
  time_execution_sync,
74
74
  )
75
-
75
+ from browser_use.llm.messages import BaseMessage, ContentPartImageParam, ContentPartTextParam, UserMessage
76
76
  from browser_use.agent.service import Agent, AgentHookFunc
77
77
  from vibe_surf.tools.file_system import CustomFileSystem
78
78
  from vibe_surf.telemetry.service import ProductTelemetry
@@ -107,6 +107,7 @@ class BrowserUseAgent(Agent):
107
107
  | None
108
108
  ) = None,
109
109
  register_external_agent_status_raise_error_callback: Callable[[], Awaitable[bool]] | None = None,
110
+ register_should_stop_callback: Callable[[], Awaitable[bool]] | None = None,
110
111
  # Agent settings
111
112
  output_model_schema: type[AgentStructuredOutput] | None = None,
112
113
  use_vision: bool = True,
@@ -127,7 +128,6 @@ class BrowserUseAgent(Agent):
127
128
  source: str | None = None,
128
129
  file_system_path: str | None = None,
129
130
  task_id: str | None = None,
130
- cloud_sync: CloudSync | None = None,
131
131
  calculate_cost: bool = False,
132
132
  display_files_in_done_text: bool = True,
133
133
  include_tool_call_examples: bool = False,
@@ -136,7 +136,9 @@ class BrowserUseAgent(Agent):
136
136
  step_timeout: int = 120,
137
137
  directly_open_url: bool = False,
138
138
  include_recent_events: bool = False,
139
- allow_parallel_action_types: list[str] = ["extract_structured_data", "extract_content_from_file"],
139
+ sample_images: list[ContentPartTextParam | ContentPartImageParam] | None = None,
140
+ final_response_after_failure: bool = True,
141
+ allow_parallel_action_types: list[str] = ["extract", "extract_content_from_file"],
140
142
  _url_shortening_limit: int = 25,
141
143
  token_cost_service: Optional[TokenCost] = None,
142
144
  **kwargs,
@@ -151,7 +153,7 @@ class BrowserUseAgent(Agent):
151
153
  self.session_id: str = uuid7str()
152
154
  self.allow_parallel_action_types = allow_parallel_action_types
153
155
  self._url_shortening_limit = _url_shortening_limit
154
-
156
+ self.sample_images = sample_images
155
157
  browser_profile = browser_profile or DEFAULT_BROWSER_PROFILE
156
158
 
157
159
  # Handle browser vs browser_session parameter (browser takes precedence)
@@ -206,6 +208,7 @@ class BrowserUseAgent(Agent):
206
208
  include_tool_call_examples=include_tool_call_examples,
207
209
  llm_timeout=llm_timeout,
208
210
  step_timeout=step_timeout,
211
+ final_response_after_failure=final_response_after_failure,
209
212
  )
210
213
 
211
214
  # Token cost service
@@ -289,7 +292,6 @@ class BrowserUseAgent(Agent):
289
292
  self._message_manager = MessageManager(
290
293
  task=task,
291
294
  system_message=SystemPrompt(
292
- action_description=self.unfiltered_actions,
293
295
  max_actions_per_step=self.settings.max_actions_per_step,
294
296
  override_system_message=override_system_message,
295
297
  extend_system_message=extend_system_message,
@@ -306,6 +308,7 @@ class BrowserUseAgent(Agent):
306
308
  vision_detail_level=self.settings.vision_detail_level,
307
309
  include_tool_call_examples=self.settings.include_tool_call_examples,
308
310
  include_recent_events=self.include_recent_events,
311
+ sample_images=self.sample_images,
309
312
  )
310
313
 
311
314
  if self.sensitive_data:
@@ -375,6 +378,7 @@ class BrowserUseAgent(Agent):
375
378
  # Callbacks
376
379
  self.register_new_step_callback = register_new_step_callback
377
380
  self.register_done_callback = register_done_callback
381
+ self.register_should_stop_callback = register_should_stop_callback
378
382
  self.register_external_agent_status_raise_error_callback = register_external_agent_status_raise_error_callback
379
383
 
380
384
  # Telemetry
@@ -463,7 +467,7 @@ class BrowserUseAgent(Agent):
463
467
 
464
468
  # Use _make_history_item like main branch
465
469
  await self._make_history_item(self.state.last_model_output, browser_state_summary, self.state.last_result,
466
- metadata)
470
+ metadata, state_message=self._message_manager.last_state_message_text,)
467
471
 
468
472
  # Log step completion summary
469
473
  self._log_step_completion_summary(self.step_start_time, self.state.last_result)
@@ -490,6 +494,9 @@ class BrowserUseAgent(Agent):
490
494
  self.task = new_task
491
495
  self._message_manager.add_new_task(new_task)
492
496
 
497
+ # Mark as follow-up task and recreate eventbus (gets shut down after each run)
498
+ self.state.follow_up_task = True
499
+
493
500
  @observe(name='agent.run', metadata={'task': '{{task}}', 'debug': '{{debug}}'})
494
501
  @time_execution_async('--run')
495
502
  async def run(
@@ -749,273 +756,4 @@ class BrowserUseAgent(Agent):
749
756
  # Add the last group
750
757
  groups.append(current_group)
751
758
 
752
- return groups
753
-
754
- @observe_debug(ignore_input=True, ignore_output=True)
755
- @time_execution_async('--multi_act')
756
- async def multi_act(
757
- self,
758
- actions: list[ActionModel],
759
- check_for_new_elements: bool = True,
760
- ) -> list[ActionResult]:
761
- """Execute multiple actions, with parallel execution for allowed action types"""
762
- results: list[ActionResult] = []
763
- time_elapsed = 0
764
- total_actions = len(actions)
765
-
766
- assert self.browser_session is not None, 'BrowserSession is not set up'
767
- try:
768
- if (
769
- self.browser_session._cached_browser_state_summary is not None
770
- and self.browser_session._cached_browser_state_summary.dom_state is not None
771
- ):
772
- cached_selector_map = dict(self.browser_session._cached_browser_state_summary.dom_state.selector_map)
773
- cached_element_hashes = {e.parent_branch_hash() for e in cached_selector_map.values()}
774
- else:
775
- cached_selector_map = {}
776
- cached_element_hashes = set()
777
- except Exception as e:
778
- self.logger.error(f'Error getting cached selector map: {e}')
779
- cached_selector_map = {}
780
- cached_element_hashes = set()
781
-
782
- # Group actions for potential parallel execution
783
- action_groups = self._group_actions_for_parallel_execution(actions)
784
-
785
- # Track global action index for logging and DOM checks
786
- global_action_index = 0
787
-
788
- for group_index, action_group in enumerate(action_groups):
789
- group_size = len(action_group)
790
-
791
- # Check if this group can be executed in parallel
792
- can_execute_in_parallel = (
793
- group_size > 1 and
794
- all(self._is_action_parallel_allowed(action) for action in action_group)
795
- )
796
-
797
- if can_execute_in_parallel:
798
- self.logger.info(
799
- f'🚀 Executing {group_size} actions in parallel: group {group_index + 1}/{len(action_groups)}')
800
- # Execute actions in parallel using asyncio.gather
801
- parallel_results = await self._execute_actions_in_parallel(
802
- action_group, global_action_index, total_actions,
803
- cached_selector_map, cached_element_hashes, check_for_new_elements
804
- )
805
- results.extend(parallel_results)
806
- global_action_index += group_size
807
-
808
- # Check if any result indicates completion or error
809
- if any(result.is_done or result.error for result in parallel_results):
810
- break
811
- else:
812
- # Execute actions sequentially
813
- for local_index, action in enumerate(action_group):
814
- i = global_action_index + local_index
815
-
816
- # Original sequential execution logic continues here...
817
- # if i > 0:
818
- # # ONLY ALLOW TO CALL `done` IF IT IS A SINGLE ACTION
819
- # if action.model_dump(exclude_unset=True).get('done') is not None:
820
- # msg = f'Done action is allowed only as a single action - stopped after action {i} / {total_actions}.'
821
- # self.logger.debug(msg)
822
- # break
823
-
824
- # DOM synchronization check - verify element indexes are still valid AFTER first action
825
- if action.get_index() is not None and i != 0:
826
- result = await self._check_dom_synchronization(
827
- action, i, total_actions, cached_selector_map, cached_element_hashes,
828
- check_for_new_elements, actions
829
- )
830
- if result:
831
- results.append(result)
832
- break
833
-
834
- # wait between actions (only after first action)
835
- if i > 0:
836
- await asyncio.sleep(self.browser_profile.wait_between_actions)
837
-
838
- # Execute single action
839
- try:
840
- action_result = await self._execute_single_action(action, i, total_actions)
841
- results.append(action_result)
842
-
843
- if action_result.is_done or action_result.error or i == total_actions - 1:
844
- break
845
-
846
- except Exception as e:
847
- self.logger.error(f'❌ Executing action {i + 1} failed: {type(e).__name__}: {e}')
848
- raise e
849
-
850
- global_action_index += len(action_group)
851
-
852
- return results
853
-
854
- async def _execute_actions_in_parallel(
855
- self,
856
- actions: list[ActionModel],
857
- start_index: int,
858
- total_actions: int,
859
- cached_selector_map: dict,
860
- cached_element_hashes: set,
861
- check_for_new_elements: bool
862
- ) -> list[ActionResult]:
863
- """Execute a group of actions in parallel using asyncio.gather"""
864
-
865
- async def execute_single_parallel_action(action: ActionModel, action_index: int) -> ActionResult:
866
- """Execute a single action for parallel execution"""
867
- await self._raise_if_stopped_or_paused()
868
-
869
- # Get action info for logging
870
- action_data = action.model_dump(exclude_unset=True)
871
- action_name = next(iter(action_data.keys())) if action_data else 'unknown'
872
- action_params = getattr(action, action_name, '') or str(action.model_dump(mode='json'))[:140].replace(
873
- '"', ''
874
- ).replace('{', '').replace('}', '').replace("'", '').strip().strip(',')
875
- action_params = str(action_params)
876
- action_params = f'{action_params[:122]}...' if len(action_params) > 128 else action_params
877
-
878
- time_start = time.time()
879
- blue = '\033[34m'
880
- reset = '\033[0m'
881
- self.logger.info(f' 🦾 {blue}[PARALLEL ACTION {action_index + 1}/{total_actions}]{reset} {action_params}')
882
-
883
- # Execute the action
884
- result = await self.tools.act(
885
- action=action,
886
- browser_session=self.browser_session,
887
- file_system=self.file_system,
888
- page_extraction_llm=self.settings.page_extraction_llm,
889
- sensitive_data=self.sensitive_data,
890
- available_file_paths=self.available_file_paths,
891
- )
892
-
893
- time_end = time.time()
894
- time_elapsed = time_end - time_start
895
-
896
- green = '\033[92m'
897
- self.logger.debug(
898
- f'☑️ Parallel action {action_index + 1}/{total_actions}: {green}{action_params}{reset} in {time_elapsed:.2f}s'
899
- )
900
-
901
- return result
902
-
903
- # Create tasks for parallel execution
904
- tasks = [
905
- execute_single_parallel_action(action, start_index + i)
906
- for i, action in enumerate(actions)
907
- ]
908
-
909
- # Execute all tasks in parallel
910
- parallel_results = await asyncio.gather(*tasks, return_exceptions=True)
911
-
912
- # Process results and handle any exceptions
913
- processed_results = []
914
- for i, result in enumerate(parallel_results):
915
- if isinstance(result, Exception):
916
- action_index = start_index + i
917
- self.logger.error(f'❌ Parallel action {action_index + 1} failed: {type(result).__name__}: {result}')
918
- raise result
919
- else:
920
- processed_results.append(result)
921
-
922
- return processed_results
923
-
924
- async def _check_dom_synchronization(
925
- self,
926
- action: ActionModel,
927
- action_index: int,
928
- total_actions: int,
929
- cached_selector_map: dict,
930
- cached_element_hashes: set,
931
- check_for_new_elements: bool,
932
- all_actions: list[ActionModel]
933
- ) -> ActionResult | None:
934
- """Check DOM synchronization and return result if page changed"""
935
- new_browser_state_summary = await self.browser_session.get_browser_state_summary(
936
- cache_clickable_elements_hashes=False,
937
- include_screenshot=False,
938
- )
939
- new_selector_map = new_browser_state_summary.dom_state.selector_map
940
-
941
- # Detect index change after previous action
942
- orig_target = cached_selector_map.get(action.get_index())
943
- orig_target_hash = orig_target.parent_branch_hash() if orig_target else None
944
-
945
- new_target = new_selector_map.get(action.get_index()) # type: ignore
946
- new_target_hash = new_target.parent_branch_hash() if new_target else None
947
-
948
- def get_remaining_actions_str(actions: list[ActionModel], index: int) -> str:
949
- remaining_actions = []
950
- for remaining_action in actions[index:]:
951
- action_data = remaining_action.model_dump(exclude_unset=True)
952
- action_name = next(iter(action_data.keys())) if action_data else 'unknown'
953
- remaining_actions.append(action_name)
954
- return ', '.join(remaining_actions)
955
-
956
- if orig_target_hash != new_target_hash:
957
- # Get names of remaining actions that won't be executed
958
- remaining_actions_str = get_remaining_actions_str(all_actions, action_index)
959
- msg = f'Page changed after action {action_index} / {total_actions}: actions {remaining_actions_str} were not executed'
960
- self.logger.info(msg)
961
- return ActionResult(
962
- extracted_content=msg,
963
- include_in_memory=True,
964
- long_term_memory=msg,
965
- )
966
-
967
- # Check for new elements that appeared
968
- new_element_hashes = {e.parent_branch_hash() for e in new_selector_map.values()}
969
- if check_for_new_elements and not new_element_hashes.issubset(cached_element_hashes):
970
- # next action requires index but there are new elements on the page
971
- remaining_actions_str = get_remaining_actions_str(all_actions, action_index)
972
- msg = f'Something new appeared after action {action_index} / {total_actions}: actions {remaining_actions_str} were not executed'
973
- self.logger.info(msg)
974
- return ActionResult(
975
- extracted_content=msg,
976
- include_in_memory=True,
977
- long_term_memory=msg,
978
- )
979
-
980
- return None
981
-
982
- async def _execute_single_action(self, action: ActionModel, action_index: int, total_actions: int) -> ActionResult:
983
- """Execute a single action in sequential mode"""
984
- await self._raise_if_stopped_or_paused()
985
-
986
- # Get action name from the action model
987
- action_data = action.model_dump(exclude_unset=True)
988
- action_name = next(iter(action_data.keys())) if action_data else 'unknown'
989
- action_params = getattr(action, action_name, '') or str(action.model_dump(mode='json'))[:140].replace(
990
- '"', ''
991
- ).replace('{', '').replace('}', '').replace("'", '').strip().strip(',')
992
- # Ensure action_params is always a string before checking length
993
- action_params = str(action_params)
994
- action_params = f'{action_params[:122]}...' if len(action_params) > 128 else action_params
995
-
996
- time_start = time.time()
997
-
998
- red = '\033[91m'
999
- green = '\033[92m'
1000
- blue = '\033[34m'
1001
- reset = '\033[0m'
1002
-
1003
- self.logger.info(f' 🦾 {blue}[ACTION {action_index + 1}/{total_actions}]{reset} {action_params}')
1004
-
1005
- result = await self.tools.act(
1006
- action=action,
1007
- browser_session=self.browser_session,
1008
- file_system=self.file_system,
1009
- page_extraction_llm=self.settings.page_extraction_llm,
1010
- sensitive_data=self.sensitive_data,
1011
- available_file_paths=self.available_file_paths,
1012
- )
1013
-
1014
- time_end = time.time()
1015
- time_elapsed = time_end - time_start
1016
-
1017
- self.logger.debug(
1018
- f'☑️ Executed action {action_index + 1}/{total_actions}: {green}{action_params}{reset} in {time_elapsed:.2f}s'
1019
- )
1020
-
1021
- return result
759
+ return groups
@@ -157,7 +157,27 @@ class ReportWriterAgent:
157
157
 
158
158
  # Add system message with unified prompt only if message history is empty
159
159
  if not self.message_history:
160
- self.message_history.append(SystemMessage(content=REPORT_WRITER_PROMPT))
160
+ report_system_prompt = REPORT_WRITER_PROMPT
161
+ if self.use_thinking:
162
+ report_system_prompt += """
163
+ You must ALWAYS respond with a valid JSON in this exact format:
164
+ {{
165
+ "thinking": "A structured <think>-style reasoning.",
166
+ "action":[{{"task_done": {{ }}, // ... more actions in sequence]
167
+ }}
168
+
169
+ Action list should NEVER be empty.
170
+ """
171
+ else:
172
+ report_system_prompt += """
173
+ You must ALWAYS respond with a valid JSON in this exact format:
174
+ {{
175
+ "action":[{{"task_done": {{ }}, // ... more actions in sequence]
176
+ }}
177
+
178
+ Action list should NEVER be empty.
179
+ """
180
+ self.message_history.append(SystemMessage(content=report_system_prompt))
161
181
 
162
182
  # Add initial user message with task details
163
183
  user_message = f"""Please generate a report within MAX {max_iterations} steps based on the following:
@@ -41,7 +41,11 @@ from vibe_surf.tools.file_system import CustomFileSystem
41
41
  from vibe_surf.agents.views import VibeSurfAgentSettings
42
42
 
43
43
  from vibe_surf.telemetry.service import ProductTelemetry
44
- from vibe_surf.telemetry.views import VibeSurfAgentTelemetryEvent
44
+ from vibe_surf.telemetry.views import (
45
+ VibeSurfAgentTelemetryEvent,
46
+ VibeSurfAgentParsedOutputEvent,
47
+ VibeSurfAgentExceptionEvent
48
+ )
45
49
 
46
50
  from vibe_surf.logger import get_logger
47
51
 
@@ -400,6 +404,25 @@ async def _vibesurf_agent_node_impl(state: VibeSurfState) -> VibeSurfState:
400
404
  AssistantMessage(content=json.dumps(response.completion.model_dump(exclude_none=True, exclude_unset=True),
401
405
  ensure_ascii=False)))
402
406
 
407
+ # Capture telemetry for parsed output
408
+ import vibe_surf
409
+ action_types = []
410
+ for action in actions:
411
+ action_data = action.model_dump(exclude_unset=True)
412
+ action_name = next(iter(action_data.keys())) if action_data else 'unknown'
413
+ action_types.append(action_name)
414
+
415
+ parsed_output_event = VibeSurfAgentParsedOutputEvent(
416
+ version=vibe_surf.__version__,
417
+ parsed_output=json.dumps(parsed.model_dump(exclude_none=True, exclude_unset=True), ensure_ascii=False), # Limit size
418
+ action_count=len(actions),
419
+ action_types=action_types,
420
+ model=getattr(vibesurf_agent.llm, 'model_name', None),
421
+ session_id=state.session_id,
422
+ )
423
+ vibesurf_agent.telemetry.capture(parsed_output_event)
424
+ vibesurf_agent.telemetry.flush()
425
+
403
426
  # Log thinking if present
404
427
  if hasattr(parsed, 'thinking') and parsed.thinking:
405
428
  await log_agent_activity(state, agent_name, "thinking", parsed.thinking)
@@ -505,8 +528,24 @@ async def _vibesurf_agent_node_impl(state: VibeSurfState) -> VibeSurfState:
505
528
 
506
529
  except Exception as e:
507
530
  import traceback
531
+ traceback_str = traceback.format_exc()
508
532
  traceback.print_exc()
509
533
  logger.error(f"❌ VibeSurf agent failed: {e}")
534
+
535
+ # Capture telemetry for exception
536
+ import vibe_surf
537
+ exception_event = VibeSurfAgentExceptionEvent(
538
+ version=vibe_surf.__version__,
539
+ error_message=str(e)[:500], # Limit error message length
540
+ error_type=type(e).__name__,
541
+ traceback=traceback_str[:1000], # Limit traceback length
542
+ model=getattr(vibesurf_agent.llm, 'model_name', None),
543
+ session_id=state.session_id,
544
+ function_name='_vibesurf_agent_node_impl'
545
+ )
546
+ vibesurf_agent.telemetry.capture(exception_event)
547
+ vibesurf_agent.telemetry.flush()
548
+
510
549
  state.final_response = f"Task execution failed: {str(e)}"
511
550
  state.is_complete = True
512
551
  await log_agent_activity(state, agent_name, "error", f"Agent failed: {str(e)}")
@@ -1606,7 +1645,27 @@ Please continue with your assigned work, incorporating this guidance only if it'
1606
1645
  upload_files = await self.process_upload_files(upload_files)
1607
1646
 
1608
1647
  if not self.message_history:
1609
- self.message_history.append(SystemMessage(content=VIBESURF_SYSTEM_PROMPT))
1648
+ vibesurf_system_prompt = VIBESURF_SYSTEM_PROMPT
1649
+ if self.settings.agent_mode == "thinking":
1650
+ vibesurf_system_prompt += """
1651
+ You must ALWAYS respond with a valid JSON in this exact format:
1652
+ {{
1653
+ "thinking": "A structured <think>-style reasoning.",
1654
+ "action":[{{"task_done": {{ }}, // ... more actions in sequence]
1655
+ }}
1656
+
1657
+ Action list should NEVER be empty.
1658
+ """
1659
+ else:
1660
+ vibesurf_system_prompt += """
1661
+ You must ALWAYS respond with a valid JSON in this exact format:
1662
+ {{
1663
+ "action":[{{"task_done": {{ }}, // ... more actions in sequence]
1664
+ }}
1665
+
1666
+ Action list should NEVER be empty.
1667
+ """
1668
+ self.message_history.append(SystemMessage(content=vibesurf_system_prompt))
1610
1669
 
1611
1670
  # Format processed upload files for prompt
1612
1671
  user_request = f"* User's New Request:\n{task}\n"
@@ -24,6 +24,17 @@ LLM_PROVIDERS = {
24
24
  "gemini-2.5-pro",
25
25
  "gemini-2.5-flash",
26
26
  ],
27
+ "kimi": [
28
+ "kimi-k2-0905-preview",
29
+ "kimi-k2-0711-preview",
30
+ "kimi-k2-turbo-preview"
31
+ ],
32
+ "qwen": [
33
+ "qwen-flash",
34
+ "qwen-plus",
35
+ "qwen3-vl-plus",
36
+ "qwen3-vl-flash"
37
+ ],
27
38
  "azure_openai": [
28
39
  "gpt-4o",
29
40
  "gpt-4o-mini",
@@ -152,6 +163,22 @@ PROVIDER_METADATA = {
152
163
  "supports_tools": True,
153
164
  "supports_vision": True,
154
165
  "default_model": ""
166
+ },
167
+ "qwen": {
168
+ "display_name": "Qwen",
169
+ "requires_api_key": True,
170
+ "requires_base_url": True,
171
+ "supports_tools": True,
172
+ "supports_vision": True,
173
+ "default_model": ""
174
+ },
175
+ "kimi": {
176
+ "display_name": "Kimi",
177
+ "requires_api_key": True,
178
+ "requires_base_url": True,
179
+ "supports_tools": True,
180
+ "supports_vision": True,
181
+ "default_model": ""
155
182
  }
156
183
  }
157
184
 
@@ -505,32 +505,6 @@ async def initialize_vibesurf_components():
505
505
  # Initialize LLM from default profile (if available) or fallback to environment variables
506
506
  llm = await _initialize_default_llm()
507
507
 
508
- # Initialize browser manager
509
- if browser_manager:
510
- main_browser_session = browser_manager.main_browser_session
511
- else:
512
- from screeninfo import get_monitors
513
- primary_monitor = get_monitors()[0]
514
- _update_extension_backend_url(envs["VIBESURF_EXTENSION"], backend_url)
515
-
516
- browser_profile = AgentBrowserProfile(
517
- executable_path=browser_execution_path,
518
- user_data_dir=browser_user_data,
519
- headless=False,
520
- keep_alive=True,
521
- auto_download_pdfs=False,
522
- highlight_elements=True,
523
- custom_extensions=[envs["VIBESURF_EXTENSION"]],
524
- window_size={"width": primary_monitor.width, "height": primary_monitor.height}
525
- )
526
-
527
- # Initialize components
528
- main_browser_session = AgentBrowserSession(browser_profile=browser_profile)
529
- await main_browser_session.start()
530
- browser_manager = BrowserManager(
531
- main_browser_session=main_browser_session
532
- )
533
-
534
508
  # Load active MCP servers from database
535
509
  mcp_server_config = await _load_active_mcp_servers()
536
510
 
@@ -564,6 +538,32 @@ async def initialize_vibesurf_components():
564
538
  )
565
539
  logger.info(f"✅ Registered Composio tools from {len(toolkit_tools_dict)} enabled toolkits")
566
540
 
541
+ # Initialize browser manager
542
+ if browser_manager:
543
+ main_browser_session = browser_manager.main_browser_session
544
+ else:
545
+ from screeninfo import get_monitors
546
+ primary_monitor = get_monitors()[0]
547
+ _update_extension_backend_url(envs["VIBESURF_EXTENSION"], backend_url)
548
+
549
+ browser_profile = AgentBrowserProfile(
550
+ executable_path=browser_execution_path,
551
+ user_data_dir=browser_user_data,
552
+ headless=False,
553
+ keep_alive=True,
554
+ auto_download_pdfs=False,
555
+ highlight_elements=True,
556
+ custom_extensions=[envs["VIBESURF_EXTENSION"]],
557
+ window_size={"width": primary_monitor.width, "height": primary_monitor.height}
558
+ )
559
+
560
+ # Initialize components
561
+ main_browser_session = AgentBrowserSession(browser_profile=browser_profile)
562
+ await main_browser_session.start()
563
+ browser_manager = BrowserManager(
564
+ main_browser_session=main_browser_session
565
+ )
566
+
567
567
  # Initialize VibeSurfAgent
568
568
  vibesurf_agent = VibeSurfAgent(
569
569
  llm=llm,
@@ -39,9 +39,38 @@ def derive_key(machine_id: str, salt: bytes = None) -> bytes:
39
39
  key = base64.urlsafe_b64encode(kdf.derive(password))
40
40
  return key
41
41
 
42
- def get_encryption_key() -> bytes:
42
+ def get_local_user_id():
43
+ _curr_user_id = "vibesurf_userid"
44
+ try:
45
+ from uuid_extensions import uuid7str
46
+ import os
47
+ WORKSPACE_DIR = os.getenv('VIBESURF_WORKSPACE', './vibesurf_workspace')
48
+ USER_ID_PATH = os.path.join(WORKSPACE_DIR, 'telemetry', 'userid')
49
+
50
+ if not os.path.exists(USER_ID_PATH):
51
+ os.makedirs(os.path.dirname(USER_ID_PATH), exist_ok=True)
52
+ with open(USER_ID_PATH, 'w') as f:
53
+ new_user_id = uuid7str()
54
+ f.write(new_user_id)
55
+ _curr_user_id = new_user_id
56
+ else:
57
+ with open(USER_ID_PATH) as f:
58
+ _curr_user_id = f.read()
59
+ return _curr_user_id
60
+ except Exception as e:
61
+ logger.error(e)
62
+ return _curr_user_id
63
+
64
+
65
+ def get_encryption_key(use_local_userid=False) -> bytes:
43
66
  """Get the encryption key for this machine."""
44
- machine_id1 = get_mac_address()
67
+ machine_id1 = ''
68
+ if not use_local_userid:
69
+ machine_id1 = get_mac_address()
70
+ if not machine_id1:
71
+ logger.info("Use local user id as encryption key.")
72
+ # fallback to get user id
73
+ machine_id1 = get_local_user_id()
45
74
  return derive_key(machine_id1)
46
75
 
47
76
  def encrypt_api_key(api_key: str) -> str:
@@ -86,8 +115,15 @@ def decrypt_api_key(encrypted_api_key: str) -> str:
86
115
  decrypted_data = fernet.decrypt(encrypted_data)
87
116
  return decrypted_data.decode('utf-8')
88
117
  except Exception as e:
89
- logger.error(f"Failed to decrypt API key: {e}")
90
- raise ValueError("Decryption failed")
118
+ try:
119
+ key = get_encryption_key(use_local_userid=True)
120
+ fernet = Fernet(key)
121
+ encrypted_data = base64.urlsafe_b64decode(encrypted_api_key.encode('utf-8'))
122
+ decrypted_data = fernet.decrypt(encrypted_data)
123
+ return decrypted_data.decode('utf-8')
124
+ except Exception as e:
125
+ logger.error(f"Failed to decrypt API key: {e}")
126
+ raise ValueError("Decryption failed")
91
127
 
92
128
  def is_encrypted(value: str) -> bool:
93
129
  """