Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
26a00eb
feat: add @mention routing for direct single-agent conversation
nitpicker55555 Feb 27, 2026
9a3e95e
Merge branch 'main' into feat/ask_with_single_agent
nitpicker55555 Feb 27, 2026
53cc86c
fix: disable tool call pruning for persistent @mention agents
nitpicker55555 Feb 27, 2026
b5e2bc2
fix: disable tool call pruning at browser agent factory level
nitpicker55555 Feb 27, 2026
c4b3e13
inital_commit for multi agent running
nitpicker55555 Feb 27, 2026
9e15e11
fix bug multi agent running
nitpicker55555 Feb 27, 2026
c2ffc41
fix bug multi agent running
nitpicker55555 Feb 27, 2026
7caaf80
remove typewriter effect
nitpicker55555 Feb 27, 2026
2790eff
worker summary card
nitpicker55555 Feb 27, 2026
ee75f83
fix worker summary card
nitpicker55555 Feb 27, 2026
586c657
fix agentMessages update task
nitpicker55555 Feb 27, 2026
63b723e
merge main into feat/ask_with_single_agent
nitpicker55555 Mar 3, 2026
896b744
merge latest main into feat/ask_with_single_agent
nitpicker55555 Mar 3, 2026
eec2b14
remove accidentally committed untracked files from merge
nitpicker55555 Mar 3, 2026
fb416f7
fix: ruff lint and format for standard_env.py
nitpicker55555 Mar 3, 2026
027f4de
Merge branch 'main' into feat/ask_with_single_agent
nitpicker55555 Mar 4, 2026
25bdee6
feat: add extension proxy integration with multi-tab support
nitpicker55555 Mar 4, 2026
b0123d3
update plug
nitpicker55555 Mar 4, 2026
b30e73b
update ui and optimize
nitpicker55555 Mar 4, 2026
2f32d19
feat: enable LLM streaming output for extension chat
nitpicker55555 Mar 4, 2026
c742f3e
feat: add Chrome browser extension for CAMEL browser agent
nitpicker55555 Mar 4, 2026
549442e
rename extension to extensions
nitpicker55555 Mar 4, 2026
7bc8d44
move extension files into extensions/chrome_extension/
nitpicker55555 Mar 4, 2026
1b02df4
update ui
nitpicker55555 Mar 4, 2026
39edbfa
merge main into feat/browser_extension
nitpicker55555 Mar 26, 2026
d835f44
Update index.tsx
fengju0213 Mar 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
376 changes: 298 additions & 78 deletions backend/app/agent/factory/browser.py

Large diffs are not rendered by default.

9 changes: 6 additions & 3 deletions backend/app/agent/listen_chat_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,15 +709,18 @@ def clone(self, with_memory: bool = False) -> ChatAgent:
if has_cdp and hasattr(self, "_cdp_options"):
options = self._cdp_options
cdp_browsers = getattr(options, "cdp_browsers", [])
if cdp_browsers and hasattr(self, "_browser_toolkit"):
cdp_only_browsers = [
b for b in cdp_browsers if not b.get("isExtensionProxy", False)
]
if cdp_only_browsers and hasattr(self, "_browser_toolkit"):
need_cdp_clone = True
import uuid as _uuid

from app.agent.factory.browser import _cdp_pool_manager

new_cdp_session = str(_uuid.uuid4())[:8]
selected = _cdp_pool_manager.acquire_browser(
cdp_browsers,
cdp_only_browsers,
new_cdp_session,
getattr(self, "_cdp_task_id", None),
)
Expand All @@ -726,7 +729,7 @@ def clone(self, with_memory: bool = False) -> ChatAgent:
if selected:
new_cdp_port = _get_browser_port(selected)
else:
new_cdp_port = _get_browser_port(cdp_browsers[0])
new_cdp_port = _get_browser_port(cdp_only_browsers[0])

if need_cdp_clone:
# Temporarily override the browser toolkit's CDP URL.
Expand Down
90 changes: 86 additions & 4 deletions backend/app/agent/toolkit/hybrid_browser_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,9 @@ def __init__(
cdp_url: str | None = "http://localhost:9222",
cdp_keep_current_page: bool = False,
full_visual_mode: bool = False,
extension_proxy_mode: bool = False,
extension_proxy_host: str = "localhost",
extension_proxy_port: int = 8765,
) -> None:
logger.info(
f"[HybridBrowserToolkit] Initializing with api_task_id: {api_task_id}"
Expand Down Expand Up @@ -489,6 +492,13 @@ def __init__(
logger.debug(
f"[HybridBrowserToolkit] Calling super().__init__ with session_id: {session_id}"
)

if extension_proxy_mode:
# In extension proxy mode, we don't connect via CDP
# Set cdp_url to None to prevent CDP connection attempts
cdp_url = None
connect_over_cdp = False

super().__init__(
headless=headless,
user_data_dir=user_data_dir,
Expand All @@ -510,16 +520,59 @@ def __init__(
cdp_url=cdp_url,
cdp_keep_current_page=cdp_keep_current_page,
full_visual_mode=full_visual_mode,
extension_proxy_mode=extension_proxy_mode,
extension_proxy_host=extension_proxy_host,
extension_proxy_port=extension_proxy_port,
)
logger.info(
f"[HybridBrowserToolkit] Initialization complete for api_task_id: {self.api_task_id}"
f"{' (extension proxy mode)' if extension_proxy_mode else ''}"
)

async def _ensure_ws_wrapper(self):
"""Ensure WebSocket wrapper is initialized using connection pool."""
logger.debug(
f"[HybridBrowserToolkit] _ensure_ws_wrapper called for api_task_id: {getattr(self, 'api_task_id', 'NOT SET')}"
)

# Extension proxy mode: use ExtensionProxyWrapper
if self._extension_proxy_mode:
if self._extension_proxy_wrapper is not None:
# Already have a shared wrapper (from clone or explicit set)
self._ws_wrapper = self._extension_proxy_wrapper
logger.info(
"[HybridBrowserToolkit] Using shared ExtensionProxyWrapper"
)
return

from camel.toolkits.hybrid_browser_toolkit.extension_proxy_wrapper import (
ExtensionProxyWrapper,
)

session_id = self._ws_config.get("session_id", "default")
wrapper = ExtensionProxyWrapper(
config=self._ws_config,
host=self._extension_proxy_host,
port=self._extension_proxy_port,
)
await wrapper.start()
logger.info(
f"[HybridBrowserToolkit] Extension proxy started on "
f"ws://{self._extension_proxy_host}:{self._extension_proxy_port}"
)

# Wait for Chrome extension to connect
connected = await wrapper.wait_for_connection(timeout=120.0)
if not connected:
raise RuntimeError(
"Timed out waiting for Chrome extension to connect"
)

self._extension_proxy_wrapper = wrapper
self._ws_wrapper = wrapper
return

# Standard CDP mode
global websocket_connection_pool

# Get session ID from config or use default
Expand Down Expand Up @@ -561,17 +614,46 @@ def clone_for_new_session(
new_session_id = str(uuid.uuid4())[:8]

# For cloned sessions, use the same user_data_dir to share login state
# This allows multiple agents to use the same browser profile without conflicts
logger.info(
f"Cloning session {new_session_id} with shared user_data_dir: {self._user_data_dir}"
)

# Use the same session_id to share the same browser instance
# This ensures all clones use the same WebSocket connection and browser
if self._extension_proxy_mode:
# Extension proxy mode: share the same wrapper connection
cloned = HybridBrowserToolkit(
self.api_task_id,
headless=self._headless,
user_data_dir=self._user_data_dir,
stealth=self._stealth,
cache_dir=f"{self._cache_dir.rstrip('/')}/_clone_{new_session_id}/",
enabled_tools=self.enabled_tools.copy(),
browser_log_to_file=self._browser_log_to_file,
log_dir=self.config_loader.get_toolkit_config().log_dir,
session_id=new_session_id,
default_start_url=self._default_start_url,
default_timeout=self._default_timeout,
short_timeout=self._short_timeout,
navigation_timeout=self._navigation_timeout,
network_idle_timeout=self._network_idle_timeout,
screenshot_timeout=self._screenshot_timeout,
page_stability_timeout=self._page_stability_timeout,
dom_content_loaded_timeout=self._dom_content_loaded_timeout,
viewport_limit=self._viewport_limit,
full_visual_mode=self._full_visual_mode,
extension_proxy_mode=True,
extension_proxy_host=self._extension_proxy_host,
extension_proxy_port=self._extension_proxy_port,
)
# Share the existing ExtensionProxyWrapper connection
if self._extension_proxy_wrapper is not None:
cloned._extension_proxy_wrapper = self._extension_proxy_wrapper
return cloned

# Standard CDP mode
return HybridBrowserToolkit(
self.api_task_id,
headless=self._headless,
user_data_dir=self._user_data_dir, # Use the same user_data_dir
user_data_dir=self._user_data_dir,
stealth=self._stealth,
cache_dir=f"{self._cache_dir.rstrip('/')}/_clone_{new_session_id}/",
enabled_tools=self.enabled_tools.copy(),
Expand Down
3 changes: 2 additions & 1 deletion backend/app/controller/chat_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,14 +342,15 @@ def improve(id: str, data: SupplementChat):
data=ImprovePayload(
question=data.question,
attaches=data.attaches or [],
target=data.target,
),
new_task_id=data.task_id,
)
)
)
chat_logger.info(
"Improvement request queued with preserved context",
extra={"project_id": id},
extra={"project_id": id, "target": data.target},
)
return Response(status_code=201)

Expand Down
81 changes: 81 additions & 0 deletions backend/app/controller/extension_proxy_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========

import logging

from fastapi import APIRouter
from pydantic import BaseModel

from app.service.extension_proxy_service import (
get_status,
start_extension_proxy,
stop_extension_proxy,
)

logger = logging.getLogger("extension_proxy_controller")
router = APIRouter()


class StartProxyRequest(BaseModel):
host: str = "localhost"
port: int = 8765
model_platform: str | None = None
model_type: str | None = None
api_key: str | None = None
api_url: str | None = None
extra_params: dict | None = None


@router.post("/extension-proxy/start", name="start extension proxy")
async def start_proxy(req: StartProxyRequest = None):
if req is None:
req = StartProxyRequest()
# Build model config if provided
model_config = None
if req.model_platform and req.model_type and req.api_key:
model_config = {
"model_platform": req.model_platform,
"model_type": req.model_type,
"api_key": req.api_key,
"api_url": req.api_url,
"extra_params": req.extra_params or {},
}

result = await start_extension_proxy(
host=req.host,
port=req.port,
model_config=model_config,
)
return {"success": True, **result}


@router.post("/extension-proxy/stop", name="stop extension proxy")
async def stop_proxy():
result = await stop_extension_proxy()
return {"success": True, **result}


@router.get("/extension-proxy/status", name="extension proxy status")
async def proxy_status():
return {"status": get_status()}


@router.post(
"/extension-proxy/chat/clear", name="clear extension chat context"
)
async def clear_chat():
from app.service.extension_chat_service import clear_chat_context

await clear_chat_context()
return {"success": True}
4 changes: 4 additions & 0 deletions backend/app/model/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ class Chat(BaseModel):
search_config: dict[str, str] | None = None
# User identifier for user-specific skill configurations
user_id: str | None = None
# Target agent for @mention routing: "browser", "dev", "doc",
# "media", "workforce", or None (default behavior)
target: str | None = None

@field_validator("model_type")
@classmethod
Expand Down Expand Up @@ -141,6 +144,7 @@ class SupplementChat(BaseModel):
question: str
task_id: str | None = None
attaches: list[str] = []
target: str | None = None


class HumanReply(BaseModel):
Expand Down
6 changes: 6 additions & 0 deletions backend/app/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

from app.controller import (
chat_controller,
extension_proxy_controller,
health_controller,
model_controller,
task_controller,
Expand Down Expand Up @@ -71,6 +72,11 @@ def register_routers(app: FastAPI, prefix: str = "") -> None:
"tags": ["tool"],
"description": "Tool installation and management",
},
{
"router": extension_proxy_controller.router,
"tags": ["extension-proxy"],
"description": "Extension proxy WebSocket server lifecycle",
},
]

app.include_router(health_controller.router, tags=["Health"])
Expand Down
Loading
Loading