superbrowser-sdk 2.0.0__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.
- super_browser/__init__.py +17 -0
- super_browser/agent/__init__.py +37 -0
- super_browser/agent/config.py +18 -0
- super_browser/agent/debug.py +201 -0
- super_browser/agent/delegator.py +145 -0
- super_browser/agent/facade.py +1216 -0
- super_browser/agent/llm/__init__.py +7 -0
- super_browser/agent/llm/anthropic_client.py +326 -0
- super_browser/agent/llm/browser_transport.py +352 -0
- super_browser/agent/llm/budget_aware.py +195 -0
- super_browser/agent/llm/factory.py +89 -0
- super_browser/agent/llm/openai_client.py +409 -0
- super_browser/agent/llm/protocol.py +73 -0
- super_browser/agent/loop.py +647 -0
- super_browser/agent/loop_detector.py +75 -0
- super_browser/agent/plugins.py +38 -0
- super_browser/agent/registry.py +218 -0
- super_browser/agent/router.py +99 -0
- super_browser/agent/structured_logging.py +76 -0
- super_browser/agent/types.py +184 -0
- super_browser/behavioral/__init__.py +36 -0
- super_browser/behavioral/bezier.py +64 -0
- super_browser/behavioral/dwell.py +162 -0
- super_browser/behavioral/fitts.py +35 -0
- super_browser/behavioral/gauss.py +100 -0
- super_browser/behavioral/keyboard.py +183 -0
- super_browser/behavioral/mouse.py +198 -0
- super_browser/behavioral/navigation.py +144 -0
- super_browser/behavioral/orchestrator.py +221 -0
- super_browser/behavioral/prng.py +27 -0
- super_browser/behavioral/qwerty.py +163 -0
- super_browser/behavioral/scroll.py +113 -0
- super_browser/behavioral/session_seed.py +85 -0
- super_browser/behavioral/types.py +62 -0
- super_browser/browser/__init__.py +15 -0
- super_browser/browser/backends/__init__.py +43 -0
- super_browser/browser/backends/cdp_backend.py +613 -0
- super_browser/browser/backends/patchright_backend.py +351 -0
- super_browser/browser/backends/playwright_backend.py +368 -0
- super_browser/browser/backends/selenium_backend.py +567 -0
- super_browser/browser/cdp.py +241 -0
- super_browser/browser/cloak_backend.py +162 -0
- super_browser/browser/cloud.py +265 -0
- super_browser/browser/config.py +48 -0
- super_browser/browser/discovery.py +101 -0
- super_browser/browser/engine.py +326 -0
- super_browser/browser/fetch.py +384 -0
- super_browser/browser/injectors/__init__.py +46 -0
- super_browser/browser/injectors/bidi_injector.py +39 -0
- super_browser/browser/injectors/cdp_injector.py +83 -0
- super_browser/browser/injectors/page_injector.py +71 -0
- super_browser/browser/page.py +96 -0
- super_browser/browser/session.py +295 -0
- super_browser/browser/shutdown.py +75 -0
- super_browser/browser/tabs.py +140 -0
- super_browser/budget/__init__.py +40 -0
- super_browser/budget/cascade.py +142 -0
- super_browser/budget/client.py +132 -0
- super_browser/budget/compressor.py +218 -0
- super_browser/budget/cost_estimator.py +67 -0
- super_browser/budget/credential_pool.py +282 -0
- super_browser/budget/governor.py +279 -0
- super_browser/budget/types.py +227 -0
- super_browser/cli/__init__.py +214 -0
- super_browser/cli/commands.py +235 -0
- super_browser/cli/interactive.py +85 -0
- super_browser/cli/script.py +266 -0
- super_browser/cli.py +279 -0
- super_browser/config.py +479 -0
- super_browser/events/__init__.py +31 -0
- super_browser/events/bus.py +110 -0
- super_browser/events/types.py +42 -0
- super_browser/interaction/__init__.py +32 -0
- super_browser/interaction/cache.py +180 -0
- super_browser/interaction/controller.py +648 -0
- super_browser/interaction/decorator.py +41 -0
- super_browser/interaction/presets.py +117 -0
- super_browser/interaction/recovery.py +48 -0
- super_browser/interaction/snapshot.py +153 -0
- super_browser/interaction/types.py +135 -0
- super_browser/interaction/vision.py +56 -0
- super_browser/memory/__init__.py +6 -0
- super_browser/memory/integration.py +85 -0
- super_browser/memory/store.py +241 -0
- super_browser/memory/types.py +57 -0
- super_browser/plugins/__init__.py +10 -0
- super_browser/plugins/decorators.py +33 -0
- super_browser/plugins/hooks.py +28 -0
- super_browser/py.typed +0 -0
- super_browser/recording/__init__.py +19 -0
- super_browser/recording/persistence.py +49 -0
- super_browser/recording/recorder.py +189 -0
- super_browser/recording/replayer.py +218 -0
- super_browser/recording/report.py +123 -0
- super_browser/recording/types.py +124 -0
- super_browser/recovery/__init__.py +67 -0
- super_browser/recovery/checkpoint.py +317 -0
- super_browser/recovery/classifier.py +235 -0
- super_browser/recovery/coordinator.py +240 -0
- super_browser/recovery/event_bus.py +67 -0
- super_browser/recovery/format_validator.py +172 -0
- super_browser/recovery/reflection.py +109 -0
- super_browser/recovery/retry_tracker.py +81 -0
- super_browser/recovery/session_recovery.py +248 -0
- super_browser/recovery/types.py +178 -0
- super_browser/recovery/watchdogs.py +251 -0
- super_browser/results/__init__.py +54 -0
- super_browser/results/output.py +154 -0
- super_browser/results/typed.py +165 -0
- super_browser/results/types.py +361 -0
- super_browser/results/validation.py +126 -0
- super_browser/security/__init__.py +75 -0
- super_browser/security/action_redaction.py +127 -0
- super_browser/security/approval.py +130 -0
- super_browser/security/credential_vault.py +203 -0
- super_browser/security/domain_filter.py +56 -0
- super_browser/security/gate.py +114 -0
- super_browser/security/injection.py +162 -0
- super_browser/security/manager.py +185 -0
- super_browser/security/policy.py +69 -0
- super_browser/security/redactor.py +151 -0
- super_browser/security/types.py +215 -0
- super_browser/session/__init__.py +1 -0
- super_browser/session/proxy.py +153 -0
- super_browser/skills/__init__.py +31 -0
- super_browser/skills/activation.py +38 -0
- super_browser/skills/markdown.py +109 -0
- super_browser/skills/registry.py +335 -0
- super_browser/skills/types.py +123 -0
- super_browser/stealth/__init__.py +94 -0
- super_browser/stealth/action_policy.py +88 -0
- super_browser/stealth/captcha.py +318 -0
- super_browser/stealth/challenges/__init__.py +41 -0
- super_browser/stealth/challenges/cache.py +293 -0
- super_browser/stealth/challenges/pow.py +242 -0
- super_browser/stealth/challenges/turnstile.py +259 -0
- super_browser/stealth/consistency/__init__.py +28 -0
- super_browser/stealth/consistency/dag.py +142 -0
- super_browser/stealth/consistency/derive.py +282 -0
- super_browser/stealth/consistency/errors.py +40 -0
- super_browser/stealth/consistency/inject.py +429 -0
- super_browser/stealth/consistency/inject_delivery.py +283 -0
- super_browser/stealth/consistency/matrix.py +106 -0
- super_browser/stealth/consistency/prng.py +115 -0
- super_browser/stealth/consistency/rule.py +64 -0
- super_browser/stealth/consistency/rules/__init__.py +40 -0
- super_browser/stealth/consistency/rules/audio.py +37 -0
- super_browser/stealth/consistency/rules/behavior.py +142 -0
- super_browser/stealth/consistency/rules/fonts.py +49 -0
- super_browser/stealth/consistency/rules/gpu.py +116 -0
- super_browser/stealth/consistency/rules/locale.py +66 -0
- super_browser/stealth/consistency/rules/navigator.py +70 -0
- super_browser/stealth/consistency/rules/screen.py +87 -0
- super_browser/stealth/consistency/rules/user_agent.py +121 -0
- super_browser/stealth/diagnostics.py +204 -0
- super_browser/stealth/ejecta/__init__.py +20 -0
- super_browser/stealth/ejecta/audio.py +182 -0
- super_browser/stealth/ejecta/browser_apis.py +225 -0
- super_browser/stealth/ejecta/canvas.py +210 -0
- super_browser/stealth/ejecta/config.py +48 -0
- super_browser/stealth/ejecta/registry.py +55 -0
- super_browser/stealth/ejecta/timing.py +159 -0
- super_browser/stealth/ejecta/types.py +30 -0
- super_browser/stealth/ejecta/webrtc.py +120 -0
- super_browser/stealth/fingerprint_scanner.py +187 -0
- super_browser/stealth/fingerprint_score.py +122 -0
- super_browser/stealth/headers.py +115 -0
- super_browser/stealth/human.py +419 -0
- super_browser/stealth/human_config.py +158 -0
- super_browser/stealth/ip_reputation.py +330 -0
- super_browser/stealth/manager.py +463 -0
- super_browser/stealth/profiles/__init__.py +145 -0
- super_browser/stealth/profiles/data/linux-chrome-stable.json +98 -0
- super_browser/stealth/profiles/data/macos-chrome-stable.json +130 -0
- super_browser/stealth/profiles/data/macos-m4-chrome-stable.json +132 -0
- super_browser/stealth/profiles/data/windows-chrome-stable.json +103 -0
- super_browser/stealth/profiles/host_detect.py +32 -0
- super_browser/stealth/profiles/schema.py +165 -0
- super_browser/stealth/proxy.py +86 -0
- super_browser/stealth/proxy_pool.py +490 -0
- super_browser/stealth/report.py +111 -0
- super_browser/stealth/scoring.py +54 -0
- super_browser/stealth/tls_baselines.json +36 -0
- super_browser/stealth/tls_fingerprint.py +494 -0
- super_browser/stealth/types.py +179 -0
- super_browser/stealth/user_agent_pool.py +148 -0
- super_browser/stealth/validation/__init__.py +13 -0
- super_browser/stealth/validation/checks.py +334 -0
- super_browser/stealth/validation/harness.py +161 -0
- super_browser/stealth/validation/report.py +31 -0
- super_browser/stealth/validation/suite.py +49 -0
- super_browser/testing.py +422 -0
- super_browser/tracing/__init__.py +29 -0
- super_browser/tracing/cost_analytics.py +49 -0
- super_browser/tracing/flow_logger.py +283 -0
- super_browser/tracing/middleware.py +42 -0
- super_browser/tracing/session_db.py +243 -0
- super_browser/tracing/sinks.py +166 -0
- super_browser/tracing/types.py +175 -0
- super_browser/verification/__init__.py +39 -0
- super_browser/verification/ax_diff.py +65 -0
- super_browser/verification/hasher.py +154 -0
- super_browser/verification/types.py +153 -0
- super_browser/verification/verifier.py +331 -0
- super_browser/vision/__init__.py +49 -0
- super_browser/vision/cache.py +178 -0
- super_browser/vision/controller.py +373 -0
- super_browser/vision/coords.py +57 -0
- super_browser/vision/factory.py +116 -0
- super_browser/vision/ocr.py +140 -0
- super_browser/vision/providers.py +348 -0
- super_browser/vision/types.py +110 -0
- superbrowser_sdk-2.0.0.dist-info/METADATA +562 -0
- superbrowser_sdk-2.0.0.dist-info/RECORD +217 -0
- superbrowser_sdk-2.0.0.dist-info/WHEEL +4 -0
- superbrowser_sdk-2.0.0.dist-info/entry_points.txt +2 -0
- superbrowser_sdk-2.0.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Super Browser — Comprehensive browser control for AI agents."""
|
|
2
|
+
|
|
3
|
+
from super_browser.agent.facade import SuperBrowser as SuperBrowser # noqa: F401
|
|
4
|
+
from super_browser.agent.llm import create_llm as create_llm # noqa: F401
|
|
5
|
+
from super_browser.agent.types import StreamEvent as StreamEvent # noqa: F401
|
|
6
|
+
from super_browser.config import Config as Config # noqa: F401
|
|
7
|
+
from super_browser.results.types import ActionResult as ActionResult # noqa: F401
|
|
8
|
+
|
|
9
|
+
__version__ = "2.0.0"
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"SuperBrowser",
|
|
13
|
+
"Config",
|
|
14
|
+
"ActionResult",
|
|
15
|
+
"create_llm",
|
|
16
|
+
"StreamEvent",
|
|
17
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""GAP-07: Agent Orchestration & Facade."""
|
|
2
|
+
|
|
3
|
+
from super_browser.agent.delegator import SubagentDelegator
|
|
4
|
+
from super_browser.agent.facade import SuperBrowser
|
|
5
|
+
from super_browser.agent.llm import LLMClient, create_llm
|
|
6
|
+
from super_browser.agent.loop import AgentLoop
|
|
7
|
+
from super_browser.agent.loop_detector import ActionLoopDetector
|
|
8
|
+
from super_browser.agent.plugins import PluginRegistry, PluginSlot
|
|
9
|
+
from super_browser.agent.registry import ToolDefinition, ToolParameter, ToolRegistry, Toolset
|
|
10
|
+
from super_browser.agent.types import (
|
|
11
|
+
ChildTask,
|
|
12
|
+
DelegationResult,
|
|
13
|
+
DelegationStatus,
|
|
14
|
+
LoopNudge,
|
|
15
|
+
LoopResult,
|
|
16
|
+
PlanItem,
|
|
17
|
+
PlanStatus,
|
|
18
|
+
PluginSlotKey,
|
|
19
|
+
StepEvent,
|
|
20
|
+
StepResult,
|
|
21
|
+
)
|
|
22
|
+
from super_browser.config import AgentConfig
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"AgentConfig",
|
|
26
|
+
"ChildTask", "DelegationResult", "DelegationStatus",
|
|
27
|
+
"LoopNudge", "LoopResult",
|
|
28
|
+
"PlanItem", "PlanStatus", "PluginSlotKey",
|
|
29
|
+
"StepEvent", "StepResult",
|
|
30
|
+
"ActionLoopDetector",
|
|
31
|
+
"ToolDefinition", "ToolParameter", "ToolRegistry", "Toolset",
|
|
32
|
+
"AgentLoop",
|
|
33
|
+
"SuperBrowser",
|
|
34
|
+
"SubagentDelegator",
|
|
35
|
+
"PluginRegistry", "PluginSlot",
|
|
36
|
+
"LLMClient", "create_llm",
|
|
37
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Agent configuration types.
|
|
2
|
+
|
|
3
|
+
``SuperBrowserConfig`` was removed in v2.0. Its fields are now flattened
|
|
4
|
+
onto :class:`AgentConfig` in ``config.py``.
|
|
5
|
+
|
|
6
|
+
This module provides ``AgentConfig`` via lazy import to avoid circular
|
|
7
|
+
dependency issues.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def __getattr__(name: str):
|
|
14
|
+
"""Lazy re-export for backward-compatible import path."""
|
|
15
|
+
if name == "AgentConfig":
|
|
16
|
+
from super_browser.config import AgentConfig
|
|
17
|
+
return AgentConfig
|
|
18
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Interactive debug session for agent failures.
|
|
2
|
+
|
|
3
|
+
When debug mode is enabled, the agent can pause on failure to allow
|
|
4
|
+
interactive inspection of browser state, capture error artifacts
|
|
5
|
+
(screenshot + DOM snapshot), and resume or abort execution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Optional
|
|
16
|
+
|
|
17
|
+
from super_browser.agent.types import DebugConfig
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class DebugSnapshot:
|
|
24
|
+
"""Captured debug state at point of failure."""
|
|
25
|
+
|
|
26
|
+
url: str = ""
|
|
27
|
+
title: str = ""
|
|
28
|
+
screenshot_path: str = ""
|
|
29
|
+
dom_path: str = ""
|
|
30
|
+
visible_text_summary: str = ""
|
|
31
|
+
error_message: str = ""
|
|
32
|
+
timestamp: float = field(default_factory=time.time)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class InteractiveDebugSession:
|
|
36
|
+
"""Manages interactive debugging when an agent step fails.
|
|
37
|
+
|
|
38
|
+
In interactive mode (default), the session pauses and waits for user
|
|
39
|
+
input before continuing. In non-interactive environments the session
|
|
40
|
+
logs state and auto-continues.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
config: DebugConfig,
|
|
46
|
+
*,
|
|
47
|
+
interactive: bool = True,
|
|
48
|
+
input_reader: Optional[Any] = None,
|
|
49
|
+
output_writer: Optional[Any] = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self._config = config
|
|
52
|
+
self._interactive = interactive
|
|
53
|
+
self._input_reader = input_reader
|
|
54
|
+
self._output_writer = output_writer
|
|
55
|
+
self._snapshots: list[DebugSnapshot] = []
|
|
56
|
+
|
|
57
|
+
# -- Public API --
|
|
58
|
+
|
|
59
|
+
async def pause_on_failure(
|
|
60
|
+
self,
|
|
61
|
+
page: Any,
|
|
62
|
+
error: Exception,
|
|
63
|
+
*,
|
|
64
|
+
step_number: int = 0,
|
|
65
|
+
) -> str:
|
|
66
|
+
"""Pause execution on failure for inspection.
|
|
67
|
+
|
|
68
|
+
Returns user command: ``"continue"``, ``"abort"``, or ``"inspect"``.
|
|
69
|
+
In non-interactive mode, always returns ``"continue"``.
|
|
70
|
+
"""
|
|
71
|
+
snapshot = await self._capture_snapshot(page, error)
|
|
72
|
+
self._snapshots.append(snapshot)
|
|
73
|
+
|
|
74
|
+
logger.warning(
|
|
75
|
+
"Debug pause on step %d: %s url=%s",
|
|
76
|
+
step_number,
|
|
77
|
+
str(error),
|
|
78
|
+
snapshot.url,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
if not self._interactive:
|
|
82
|
+
return "continue"
|
|
83
|
+
|
|
84
|
+
# Interactive: prompt user
|
|
85
|
+
self._write(f"\n🔴 STEP {step_number} FAILED: {error}")
|
|
86
|
+
self._write(f" URL: {snapshot.url}")
|
|
87
|
+
self._write(" Commands: [c]ontinue, [a]bort, [i]nspect → ")
|
|
88
|
+
|
|
89
|
+
command = await self._read_input()
|
|
90
|
+
if command is None:
|
|
91
|
+
return "continue"
|
|
92
|
+
|
|
93
|
+
cmd = command.strip().lower()
|
|
94
|
+
if cmd in ("a", "abort"):
|
|
95
|
+
return "abort"
|
|
96
|
+
if cmd in ("i", "inspect"):
|
|
97
|
+
state = await self.inspect_state(page)
|
|
98
|
+
self._write(f" Title: {state['title']}")
|
|
99
|
+
self._write(f" URL: {state['url']}")
|
|
100
|
+
self._write(f" Visible text (first 200 chars): {state['visible_text_summary'][:200]}")
|
|
101
|
+
return "continue"
|
|
102
|
+
return "continue"
|
|
103
|
+
|
|
104
|
+
async def capture_error_artifacts(
|
|
105
|
+
self,
|
|
106
|
+
page: Any,
|
|
107
|
+
error: Exception,
|
|
108
|
+
config: DebugConfig,
|
|
109
|
+
) -> DebugSnapshot:
|
|
110
|
+
"""Capture screenshot and optional DOM snapshot for an error."""
|
|
111
|
+
snapshot = await self._capture_snapshot(page, error)
|
|
112
|
+
|
|
113
|
+
screenshot_dir = Path(config.screenshot_dir)
|
|
114
|
+
screenshot_dir.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
|
|
116
|
+
ts = int(time.time() * 1000)
|
|
117
|
+
|
|
118
|
+
# Screenshot
|
|
119
|
+
try:
|
|
120
|
+
screenshot_path = screenshot_dir / f"error_{ts}.png"
|
|
121
|
+
if page and hasattr(page, "screenshot"):
|
|
122
|
+
await page.screenshot(path=str(screenshot_path))
|
|
123
|
+
snapshot.screenshot_path = str(screenshot_path)
|
|
124
|
+
logger.info("Debug screenshot saved: %s", screenshot_path)
|
|
125
|
+
except Exception as exc:
|
|
126
|
+
logger.warning("Failed to capture screenshot: %s", exc)
|
|
127
|
+
|
|
128
|
+
# DOM snapshot
|
|
129
|
+
if config.capture_dom:
|
|
130
|
+
try:
|
|
131
|
+
dom_path = screenshot_dir / f"error_{ts}.dom.html"
|
|
132
|
+
if page and hasattr(page, "content"):
|
|
133
|
+
content = await page.content()
|
|
134
|
+
dom_path.write_text(content, encoding="utf-8")
|
|
135
|
+
snapshot.dom_path = str(dom_path)
|
|
136
|
+
logger.info("Debug DOM snapshot saved: %s", dom_path)
|
|
137
|
+
except Exception as exc:
|
|
138
|
+
logger.warning("Failed to capture DOM snapshot: %s", exc)
|
|
139
|
+
|
|
140
|
+
self._snapshots.append(snapshot)
|
|
141
|
+
return snapshot
|
|
142
|
+
|
|
143
|
+
async def inspect_state(self, page: Any) -> dict[str, str]:
|
|
144
|
+
"""Return current page state: URL, title, visible text summary."""
|
|
145
|
+
state: dict[str, str] = {
|
|
146
|
+
"url": "",
|
|
147
|
+
"title": "",
|
|
148
|
+
"visible_text_summary": "",
|
|
149
|
+
}
|
|
150
|
+
if page is None:
|
|
151
|
+
return state
|
|
152
|
+
try:
|
|
153
|
+
state["url"] = page.url if hasattr(page, "url") else ""
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
try:
|
|
157
|
+
if hasattr(page, "title"):
|
|
158
|
+
title_result = page.title()
|
|
159
|
+
if asyncio.iscoroutine(title_result):
|
|
160
|
+
title_result = await title_result
|
|
161
|
+
state["title"] = title_result
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
try:
|
|
165
|
+
if hasattr(page, "evaluate"):
|
|
166
|
+
text = await page.evaluate("() => document.body?.innerText?.substring(0, 500) || ''")
|
|
167
|
+
state["visible_text_summary"] = text or ""
|
|
168
|
+
except Exception:
|
|
169
|
+
pass
|
|
170
|
+
return state
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def snapshots(self) -> list[DebugSnapshot]:
|
|
174
|
+
return list(self._snapshots)
|
|
175
|
+
|
|
176
|
+
# -- Internals --
|
|
177
|
+
|
|
178
|
+
async def _capture_snapshot(self, page: Any, error: Exception) -> DebugSnapshot:
|
|
179
|
+
state = await self.inspect_state(page)
|
|
180
|
+
return DebugSnapshot(
|
|
181
|
+
url=state["url"],
|
|
182
|
+
title=state["title"],
|
|
183
|
+
visible_text_summary=state["visible_text_summary"],
|
|
184
|
+
error_message=str(error),
|
|
185
|
+
timestamp=time.time(),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _write(self, message: str) -> None:
|
|
189
|
+
if self._output_writer:
|
|
190
|
+
self._output_writer(message)
|
|
191
|
+
else:
|
|
192
|
+
# Default: just log
|
|
193
|
+
logger.info("Debug: %s", message)
|
|
194
|
+
|
|
195
|
+
async def _read_input(self) -> Optional[str]:
|
|
196
|
+
if self._input_reader:
|
|
197
|
+
result = self._input_reader()
|
|
198
|
+
if asyncio.iscoroutine(result):
|
|
199
|
+
return await result
|
|
200
|
+
return result
|
|
201
|
+
return None
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""SubagentDelegator — parallel child agents with isolated browser contexts."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
import time
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
|
|
10
|
+
from super_browser.agent.loop import AgentLoop
|
|
11
|
+
from super_browser.agent.registry import ToolRegistry
|
|
12
|
+
from super_browser.agent.types import ChildTask, DelegationResult, DelegationStatus
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SubagentDelegator:
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
browser_session: Any,
|
|
22
|
+
registry: ToolRegistry,
|
|
23
|
+
llm_client: Any,
|
|
24
|
+
*,
|
|
25
|
+
max_concurrency: int = 4,
|
|
26
|
+
max_steps_per_child: int = 50,
|
|
27
|
+
recovery_coordinator: Optional[Any] = None,
|
|
28
|
+
budget_client: Optional[Any] = None,
|
|
29
|
+
flow_logger: Optional[Any] = None,
|
|
30
|
+
security_manager: Optional[Any] = None,
|
|
31
|
+
stealth_manager: Optional[Any] = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
self._session = browser_session
|
|
34
|
+
self._registry = registry
|
|
35
|
+
self._llm = llm_client
|
|
36
|
+
self._max_concurrency = max_concurrency
|
|
37
|
+
self._max_steps = max_steps_per_child
|
|
38
|
+
self._recovery_coordinator = recovery_coordinator
|
|
39
|
+
self._budget_client = budget_client
|
|
40
|
+
self._flow_logger = flow_logger
|
|
41
|
+
self._security_manager = security_manager
|
|
42
|
+
self._stealth_manager = stealth_manager
|
|
43
|
+
self._open_tabs = 0
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def open_tabs(self) -> int:
|
|
47
|
+
"""Current number of open child tabs (read-only)."""
|
|
48
|
+
return self._open_tabs
|
|
49
|
+
|
|
50
|
+
async def delegate(
|
|
51
|
+
self,
|
|
52
|
+
tasks: list[str],
|
|
53
|
+
*,
|
|
54
|
+
max_concurrency: Optional[int] = None,
|
|
55
|
+
abort_signal: Optional[asyncio.Event] = None,
|
|
56
|
+
) -> DelegationResult:
|
|
57
|
+
start = time.monotonic()
|
|
58
|
+
concurrency = max_concurrency or self._max_concurrency
|
|
59
|
+
semaphore = asyncio.Semaphore(concurrency)
|
|
60
|
+
|
|
61
|
+
children = [ChildTask(instruction=instr) for instr in tasks]
|
|
62
|
+
|
|
63
|
+
async def _run_with_semaphore(task: ChildTask) -> ChildTask:
|
|
64
|
+
async with semaphore:
|
|
65
|
+
if abort_signal and abort_signal.is_set():
|
|
66
|
+
task.status = DelegationStatus.CANCELLED
|
|
67
|
+
return task
|
|
68
|
+
return await self._run_child(task)
|
|
69
|
+
|
|
70
|
+
results = await asyncio.gather(
|
|
71
|
+
*[_run_with_semaphore(c) for c in children],
|
|
72
|
+
return_exceptions=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
final_tasks: list[ChildTask] = []
|
|
76
|
+
for r in results:
|
|
77
|
+
if isinstance(r, Exception):
|
|
78
|
+
failed = ChildTask(instruction="error", status=DelegationStatus.FAILED, result=str(r))
|
|
79
|
+
final_tasks.append(failed)
|
|
80
|
+
else:
|
|
81
|
+
final_tasks.append(r)
|
|
82
|
+
|
|
83
|
+
duration = (time.monotonic() - start) * 1000
|
|
84
|
+
completed = sum(1 for t in final_tasks if t.status == DelegationStatus.COMPLETED)
|
|
85
|
+
failed = sum(1 for t in final_tasks if t.status == DelegationStatus.FAILED)
|
|
86
|
+
cancelled = sum(1 for t in final_tasks if t.status == DelegationStatus.CANCELLED)
|
|
87
|
+
|
|
88
|
+
# Sanity check — should never happen, but catch programming errors.
|
|
89
|
+
assert self._open_tabs <= concurrency, (
|
|
90
|
+
f"Tab cap violated: {self._open_tabs} open tabs > {concurrency} max_concurrency"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return DelegationResult(
|
|
94
|
+
tasks=final_tasks,
|
|
95
|
+
total_duration_ms=duration,
|
|
96
|
+
completed_count=completed,
|
|
97
|
+
failed_count=failed,
|
|
98
|
+
cancelled_count=cancelled,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
async def _run_child(self, task: ChildTask) -> ChildTask:
|
|
102
|
+
task.status = DelegationStatus.RUNNING
|
|
103
|
+
task.started_at = time.monotonic()
|
|
104
|
+
|
|
105
|
+
page = None
|
|
106
|
+
try:
|
|
107
|
+
self._open_tabs += 1
|
|
108
|
+
assert self._open_tabs <= self._max_concurrency, (
|
|
109
|
+
f"Hard tab cap violated: {self._open_tabs} > {self._max_concurrency}"
|
|
110
|
+
)
|
|
111
|
+
page = await self._session.new_page()
|
|
112
|
+
|
|
113
|
+
from super_browser.interaction.controller import MultimodalController
|
|
114
|
+
controller = MultimodalController(page, page.cdp)
|
|
115
|
+
|
|
116
|
+
child_loop = AgentLoop(
|
|
117
|
+
controller=controller,
|
|
118
|
+
registry=self._registry,
|
|
119
|
+
llm_client=self._llm,
|
|
120
|
+
max_steps=self._max_steps,
|
|
121
|
+
recovery_coordinator=self._recovery_coordinator,
|
|
122
|
+
budget_client=self._budget_client,
|
|
123
|
+
flow_logger=self._flow_logger,
|
|
124
|
+
security_manager=self._security_manager,
|
|
125
|
+
stealth_manager=self._stealth_manager,
|
|
126
|
+
)
|
|
127
|
+
loop_result = await child_loop.run(task.instruction)
|
|
128
|
+
|
|
129
|
+
task.result = loop_result
|
|
130
|
+
task.status = DelegationStatus.COMPLETED
|
|
131
|
+
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
logger.warning("Child task %s failed: %s", task.task_id[:8], exc)
|
|
134
|
+
task.result = str(exc)
|
|
135
|
+
task.status = DelegationStatus.FAILED
|
|
136
|
+
finally:
|
|
137
|
+
if page:
|
|
138
|
+
try:
|
|
139
|
+
await page.close()
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
142
|
+
self._open_tabs -= 1
|
|
143
|
+
|
|
144
|
+
task.completed_at = time.monotonic()
|
|
145
|
+
return task
|