fix: browser quickstart

This commit is contained in:
Timothy
2026-04-03 17:40:53 -07:00
parent 59e90d3168
commit 9193336fd3
4 changed files with 56 additions and 27 deletions
@@ -273,18 +273,28 @@ async def execute_subagent(
conversation_store=subagent_conv_store, conversation_store=subagent_conv_store,
) )
# Inject a GCU browser profile for this subagent. # Each subagent instance gets its own unique browser profile so concurrent
# Use just agent_id (not subagent_instance) so the profile is stable # subagents don't share tab groups. The profile is injected into every
# across multiple calls to the same subagent type. This allows # browser_* tool call by wrapping the tool executor.
# cookies/auth to persist between runs. _gcu_profile = f"{agent_id}:{subagent_instance}"
_profile_token = None _original_tool_executor = None
_subagent_profile = agent_id # Stable profile per agent type
try:
from gcu.browser.session import set_active_profile as _set_gcu_profile
_profile_token = _set_gcu_profile(_subagent_profile) if tool_executor is not None:
except ImportError: _original_tool_executor = tool_executor
pass # GCU tools not installed; no-op
async def _gcu_profile_injecting_executor(
tool_use: ToolUse,
) -> ToolResult | Awaitable[ToolResult]:
if tool_use.name.startswith("browser_") and "profile" not in (tool_use.input or {}):
from dataclasses import replace
tool_use = replace(tool_use, input={**(tool_use.input or {}), "profile": _gcu_profile})
result = _original_tool_executor(tool_use)
if asyncio.isfuture(result) or asyncio.iscoroutine(result):
return await result
return result
tool_executor = _gcu_profile_injecting_executor
try: try:
logger.info("🚀 Starting subagent '%s' execution...", agent_id) logger.info("🚀 Starting subagent '%s' execution...", agent_id)
@@ -356,14 +366,16 @@ async def execute_subagent(
is_error=True, is_error=True,
) )
finally: finally:
# Restore the GCU profile context # Close the tab group this subagent created, if any.
if _profile_token is not None: if _original_tool_executor is not None:
from gcu.browser.session import _active_profile as _gcu_profile_var try:
stop_call = ToolUse(
_gcu_profile_var.reset(_profile_token) id="__subagent_cleanup__",
name="browser_stop",
# NOTE: We intentionally do NOT call browser_stop() here. input={"profile": _gcu_profile},
# Keeping the browser session alive allows cookies and auth state )
# to persist across multiple subagent calls. The browser will be result = _original_tool_executor(stop_call)
# cleaned up when the parent agent stops or when explicitly if asyncio.isfuture(result) or asyncio.iscoroutine(result):
# requested via browser_stop(). await result
except Exception:
pass
+1
View File
@@ -98,6 +98,7 @@ class BeelineBridge:
"127.0.0.1", "127.0.0.1",
port, port,
logger=null_logger, logger=null_logger,
max_size=50 * 1024 * 1024, # 50 MB — CDP responses (AX tree, screenshots) can be large
) )
logger.info("Beeline bridge listening on ws://127.0.0.1:%d", port) logger.info("Beeline bridge listening on ws://127.0.0.1:%d", port)
except OSError as e: except OSError as e:
+3 -5
View File
@@ -45,12 +45,10 @@ def get_all_sessions() -> dict[str, Any]:
async def shutdown_all_browsers() -> None: async def shutdown_all_browsers() -> None:
"""Stop all browser sessions. """Stop all browser sessions. Called at server shutdown to clean up."""
from gcu.browser.tools.lifecycle import shutdown_all_contexts
Called at server shutdown to clean up. await shutdown_all_contexts()
Note: Sessions are managed via bridge extension.
"""
pass
class BrowserSession: class BrowserSession:
+18
View File
@@ -38,6 +38,24 @@ _EXTENSION_PATH = (
).resolve() ).resolve()
async def shutdown_all_contexts() -> None:
"""Close all active browser contexts. Called at GCU server shutdown."""
if not _contexts:
return
bridge = get_bridge()
for profile_name, ctx in list(_contexts.items()):
group_id = ctx.get("groupId")
if group_id is not None and bridge and bridge.is_connected:
try:
await bridge.destroy_context(group_id)
logger.info(
"Shutdown: closed browser context '%s' (groupId=%s)", profile_name, group_id
)
except Exception as e:
logger.warning("Shutdown: failed to close context '%s': %s", profile_name, e)
_contexts.clear()
def register_lifecycle_tools(mcp: FastMCP) -> None: def register_lifecycle_tools(mcp: FastMCP) -> None:
"""Register browser lifecycle management tools.""" """Register browser lifecycle management tools."""