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,
)
# Inject a GCU browser profile for this subagent.
# Use just agent_id (not subagent_instance) so the profile is stable
# across multiple calls to the same subagent type. This allows
# cookies/auth to persist between runs.
_profile_token = None
_subagent_profile = agent_id # Stable profile per agent type
try:
from gcu.browser.session import set_active_profile as _set_gcu_profile
# Each subagent instance gets its own unique browser profile so concurrent
# subagents don't share tab groups. The profile is injected into every
# browser_* tool call by wrapping the tool executor.
_gcu_profile = f"{agent_id}:{subagent_instance}"
_original_tool_executor = None
_profile_token = _set_gcu_profile(_subagent_profile)
except ImportError:
pass # GCU tools not installed; no-op
if tool_executor is not None:
_original_tool_executor = tool_executor
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:
logger.info("🚀 Starting subagent '%s' execution...", agent_id)
@@ -356,14 +366,16 @@ async def execute_subagent(
is_error=True,
)
finally:
# Restore the GCU profile context
if _profile_token is not None:
from gcu.browser.session import _active_profile as _gcu_profile_var
_gcu_profile_var.reset(_profile_token)
# NOTE: We intentionally do NOT call browser_stop() here.
# Keeping the browser session alive allows cookies and auth state
# to persist across multiple subagent calls. The browser will be
# cleaned up when the parent agent stops or when explicitly
# requested via browser_stop().
# Close the tab group this subagent created, if any.
if _original_tool_executor is not None:
try:
stop_call = ToolUse(
id="__subagent_cleanup__",
name="browser_stop",
input={"profile": _gcu_profile},
)
result = _original_tool_executor(stop_call)
if asyncio.isfuture(result) or asyncio.iscoroutine(result):
await result
except Exception:
pass
+1
View File
@@ -98,6 +98,7 @@ class BeelineBridge:
"127.0.0.1",
port,
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)
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:
"""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.
Note: Sessions are managed via bridge extension.
"""
pass
await shutdown_all_contexts()
class BrowserSession:
+18
View File
@@ -38,6 +38,24 @@ _EXTENSION_PATH = (
).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:
"""Register browser lifecycle management tools."""