mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 12:47:42 +02:00
[eric] agents: drop the Mixin suffix, name the 9 manager classes by domain (Messaging, SessionLifecycle, ...) + AgentManagerState -> AgentManagerProtocol
This commit is contained in:
@@ -19,23 +19,23 @@ from backend.apps.agents.manager.session.session_store import (
|
||||
)
|
||||
from backend.apps.agents.manager.streaming.state import ThinkingState, TurnState
|
||||
from backend.apps.agents.manager.streaming.LivePartial import LivePartial
|
||||
from backend.apps.agents.manager.session.SessionLifecycleMixin import SessionLifecycleMixin
|
||||
from backend.apps.agents.manager.session.SessionPersistenceMixin import SessionPersistenceMixin
|
||||
from backend.apps.agents.manager.MessagingMixin import MessagingMixin
|
||||
from backend.apps.agents.manager.SessionControlMixin import SessionControlMixin
|
||||
from backend.apps.agents.manager.AgentLaunchMixin import AgentLaunchMixin
|
||||
from backend.apps.agents.manager.MockAgentMixin import MockAgentMixin
|
||||
from backend.apps.agents.manager.RunSupportMixin import RunSupportMixin
|
||||
from backend.apps.agents.manager.session.SessionLifecycle import SessionLifecycle
|
||||
from backend.apps.agents.manager.session.SessionPersistence import SessionPersistence
|
||||
from backend.apps.agents.manager.Messaging import Messaging
|
||||
from backend.apps.agents.manager.SessionControl import SessionControl
|
||||
from backend.apps.agents.manager.AgentLaunch import AgentLaunch
|
||||
from backend.apps.agents.manager.MockAgent import MockAgent
|
||||
from backend.apps.agents.manager.RunSupport import RunSupport
|
||||
from backend.apps.agents.manager.run.handle_run_error import handle_run_error
|
||||
from backend.apps.agents.manager.run.TurnRunnerMixin import TurnRunnerMixin
|
||||
from backend.apps.agents.manager.run.RunOptionsMixin import RunOptionsMixin
|
||||
from backend.apps.agents.manager.run.TurnRunner import TurnRunner
|
||||
from backend.apps.agents.manager.run.RunOptions import RunOptions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
os.environ.setdefault("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "3600000")
|
||||
|
||||
|
||||
class AgentManager(SessionLifecycleMixin, SessionPersistenceMixin, MessagingMixin, SessionControlMixin, AgentLaunchMixin, MockAgentMixin, TurnRunnerMixin, RunOptionsMixin, RunSupportMixin):
|
||||
class AgentManager(SessionLifecycle, SessionPersistence, Messaging, SessionControl, AgentLaunch, MockAgent, TurnRunner, RunOptions, RunSupport):
|
||||
@typechecked
|
||||
def __init__(self):
|
||||
self.sessions: Dict[str, AgentSession] = {}
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
"""Agent run entry points for AgentManager: launch a new top-level run and the staticmethod
|
||||
invoke_agent helper (fork-and-send a sub-agent). The no-SDK mock fallback lives in MockAgentMixin.
|
||||
invoke_agent helper (fork-and-send a sub-agent). The no-SDK mock fallback lives in MockAgent.
|
||||
Split into a mixin to keep the manager file under the size ceiling; self.run_agent_loop /
|
||||
self.sessions resolve across the MRO exactly as before."""
|
||||
|
||||
@@ -29,10 +29,10 @@ from backend.apps.agents.manager.prompt.prompt_context import resolve_mode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
|
||||
class AgentLaunchMixin(AgentManagerState):
|
||||
class AgentLaunch(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def launch_agent(self, config: AgentConfig) -> AgentSession:
|
||||
session_id = uuid4().hex
|
||||
+3
-3
@@ -1,7 +1,7 @@
|
||||
"""Typing-only contract shared by the AgentManager mixins.
|
||||
|
||||
The AgentManager god-object was decomposed into behavior mixins (MessagingMixin,
|
||||
SessionLifecycleMixin, ...) that read state set in AgentManager.__init__ and call
|
||||
The AgentManager god-object was decomposed into behavior mixins (Messaging,
|
||||
SessionLifecycle, ...) that read state set in AgentManager.__init__ and call
|
||||
methods implemented on sibling mixins. From inside one mixin a type checker can't
|
||||
see that composed surface, so it flags self.sessions / self.run_agent_loop as
|
||||
unknown. This base declares that surface (the __init__ state + the cross-mixin
|
||||
@@ -20,7 +20,7 @@ if TYPE_CHECKING:
|
||||
from backend.apps.agents.manager.streaming.LivePartial import LivePartial
|
||||
|
||||
|
||||
class AgentManagerState:
|
||||
class AgentManagerProtocol:
|
||||
# State set in AgentManager.__init__.
|
||||
sessions: Dict[str, AgentSession]
|
||||
tasks: Dict[str, asyncio.Task]
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
"""Turn-producing message operations for AgentManager (send + edit), the ones that append a
|
||||
user Message and spawn the agent loop. Session-control ops (stop / approve / branch / update)
|
||||
live in SessionControlMixin. Pure relocation: self.* resolves across the MRO as before."""
|
||||
live in SessionControl. Pure relocation: self.* resolves across the MRO as before."""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
@@ -21,10 +21,10 @@ from backend.apps.agents.manager.prompt.prompt_context import resolve_mode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
|
||||
class MessagingMixin(AgentManagerState):
|
||||
class Messaging(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def send_message(
|
||||
self,
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
"""The no-SDK mock agent loop, split out of AgentLaunchMixin so each file is one concern. This
|
||||
"""The no-SDK mock agent loop, split out of AgentLaunch so each file is one concern. This
|
||||
fires only when claude_agent_sdk isn't installed (dev fallback): it fakes one Bash approval +
|
||||
tool-result + assistant reply so the UI is exercisable without a real model. self.p_stream_* /
|
||||
self.sessions resolve across the MRO exactly as when this lived on AgentLaunchMixin."""
|
||||
self.sessions resolve across the MRO exactly as when this lived on AgentLaunch."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
@@ -17,10 +17,10 @@ from backend.apps.agents.core.ws_manager import ws_manager
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
|
||||
class MockAgentMixin(AgentManagerState):
|
||||
class MockAgent(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def run_mock_agent(self, session_id: str, prompt: str):
|
||||
"""Mock agent loop for development without claude_agent_sdk installed."""
|
||||
+2
-2
@@ -33,12 +33,12 @@ from backend.apps.tools_lib.tools_lib import (
|
||||
refresh_google_token,
|
||||
refresh_hubspot_token,
|
||||
)
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunSupportMixin(AgentManagerState):
|
||||
class RunSupport(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def build_mcp_servers(
|
||||
self,
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
"""Session-control operations for AgentManager (stop / approve / switch-branch / update),
|
||||
split from MessagingMixin so each file stays one responsibility: these control or mutate a
|
||||
split from Messaging so each file stays one responsibility: these control or mutate a
|
||||
session WITHOUT producing a new agent turn. Pure relocation, self.* resolves across the MRO."""
|
||||
|
||||
import asyncio
|
||||
@@ -15,10 +15,10 @@ from backend.apps.agents.manager.session.session_store import save_session
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
|
||||
class SessionControlMixin(AgentManagerState):
|
||||
class SessionControl(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def stop_agent(self, session_id: str):
|
||||
"""Stop a running agent and all its browser-agent children."""
|
||||
@@ -211,7 +211,7 @@ async def configure_provider_env(
|
||||
# Pin subagent ids to whichever lane the user has, else CLI's
|
||||
# default Haiku 4.5 hits 9Router with no Claude route and 401s.
|
||||
# NOTE: sub_conns is the connection list passed in. Callers currently pass []
|
||||
# (see RunOptionsMixin), so `active` is empty and this pinning is inert until the
|
||||
# (see RunOptions), so `active` is empty and this pinning is inert until the
|
||||
# real connection list is wired through — a latent regression from the run/ split,
|
||||
# surfaced by pyright (the old inline `_conns` reference was left dangling here).
|
||||
active = {c.get("provider") for c in sub_conns
|
||||
|
||||
+2
-2
@@ -34,10 +34,10 @@ from backend.apps.agents.manager.run.run_options_helpers import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
|
||||
class RunOptionsMixin(AgentManagerState):
|
||||
class RunOptions(AgentManagerProtocol):
|
||||
# No return annotation: the returned tuple carries an SDK ClaudeAgentOptions, which can't be
|
||||
# module-imported here (mock-mode would fail to import the manager); it's lazy-imported below.
|
||||
@typechecked
|
||||
+2
-2
@@ -22,10 +22,10 @@ from backend.apps.settings.models import AppSettings
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
|
||||
class TurnRunnerMixin(AgentManagerState):
|
||||
class TurnRunner(AgentManagerProtocol):
|
||||
# `options` is the SDK ClaudeAgentOptions, lazy-imported below (so mock-mode can import the
|
||||
# manager without the SDK present), so it's left unannotated; everything else is typed.
|
||||
@typechecked
|
||||
+2
-2
@@ -28,10 +28,10 @@ from backend.apps.agents.manager.view_builder_state import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
|
||||
class SessionLifecycleMixin(AgentManagerState):
|
||||
class SessionLifecycle(AgentManagerProtocol):
|
||||
@staticmethod
|
||||
@typechecked
|
||||
def build_search_text(session: AgentSession, max_len: int = 5000) -> str:
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
"""Bulk session persistence across the WHOLE store, the startup/shutdown orchestration that
|
||||
operates on every session at once (reconcile stale-running, flush-all on shutdown, restore-all
|
||||
on boot). Split from SessionLifecycleMixin (which handles ONE session at a time) so each file is
|
||||
on boot). Split from SessionLifecycle (which handles ONE session at a time) so each file is
|
||||
one concern. self.sessions / self.sync_session_close resolve across the MRO as before."""
|
||||
|
||||
import logging
|
||||
@@ -19,10 +19,10 @@ from backend.apps.agents.manager.session.apply_context_window import apply_conte
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
from backend.apps.agents.manager.AgentManagerProtocol import AgentManagerProtocol
|
||||
|
||||
|
||||
class SessionPersistenceMixin(AgentManagerState):
|
||||
class SessionPersistence(AgentManagerProtocol):
|
||||
@typechecked
|
||||
async def reconcile_on_startup(self) -> None:
|
||||
"""Mark any stale running sessions as stopped."""
|
||||
@@ -22,7 +22,7 @@ def p_provider_configured(monkeypatch):
|
||||
in from the dev machine's settings (which made these tests pass by accident and fail in a
|
||||
clean checkout). The harness tests the streaming loop, not provider config."""
|
||||
import backend.apps.agents.agent_manager as am
|
||||
import backend.apps.agents.manager.run.RunOptionsMixin as run_opts
|
||||
import backend.apps.agents.manager.run.RunOptions as run_opts
|
||||
from backend.apps.settings.models import AppSettings
|
||||
settings = AppSettings(connection_mode="own_key", anthropic_api_key="sk-ant-test")
|
||||
monkeypatch.setattr(am, "load_settings", lambda: settings, raising=True)
|
||||
@@ -78,7 +78,7 @@ def p_capture_env(monkeypatch, settings, api_type, resolved_model, model_entry):
|
||||
into ClaudeAgentOptions (the provider-route auth config the SDK runs under)."""
|
||||
import backend.apps.agents.providers.registry as reg
|
||||
import backend.apps.agents.agent_manager as am
|
||||
import backend.apps.agents.manager.run.RunOptionsMixin as run_opts
|
||||
import backend.apps.agents.manager.run.RunOptions as run_opts
|
||||
monkeypatch.setattr(am, "load_settings", lambda: settings, raising=True)
|
||||
monkeypatch.setattr(run_opts, "load_settings", lambda: settings, raising=True)
|
||||
monkeypatch.setattr(reg, "get_api_type", lambda model: api_type, raising=True)
|
||||
@@ -185,7 +185,7 @@ def test_loop_builds_direct_anthropic_key_env(monkeypatch):
|
||||
from backend.apps.settings.models import AppSettings
|
||||
import backend.apps.agents.providers.registry as reg
|
||||
import backend.apps.agents.agent_manager as am
|
||||
import backend.apps.agents.manager.run.RunOptionsMixin as run_opts
|
||||
import backend.apps.agents.manager.run.RunOptions as run_opts
|
||||
|
||||
settings = AppSettings(anthropic_api_key="sk-ant-test123", connection_mode="own_key")
|
||||
monkeypatch.setattr(am, "load_settings", lambda: settings, raising=True)
|
||||
@@ -222,7 +222,7 @@ def test_loop_with_session_cwd_runs_workspace_git_init(monkeypatch):
|
||||
# sessions normally have no cwd, which masked a NameError (the call said ensure_cwd_git_repo
|
||||
# while only _ensure_cwd_git_repo was imported). raising=True here would fail if the name were
|
||||
# missing again; the assertions confirm the cwd path actually runs and the turn completes.
|
||||
import backend.apps.agents.manager.run.RunOptionsMixin as run_opts
|
||||
import backend.apps.agents.manager.run.RunOptions as run_opts
|
||||
called = {}
|
||||
|
||||
def fake_ensure(cwd, home=None):
|
||||
|
||||
@@ -79,8 +79,8 @@ async def test_gate_blocks_when_active_mcps_empty():
|
||||
p_fake_tool("Slack"),
|
||||
p_fake_tool("Notion"),
|
||||
]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
# allowed_tools includes mcp:Gmail, but active_mcps is empty
|
||||
result = await mgr.build_mcp_servers(
|
||||
@@ -99,8 +99,8 @@ async def test_gate_allows_only_activated_servers():
|
||||
p_fake_tool("Slack"),
|
||||
p_fake_tool("Notion"),
|
||||
]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
result = await mgr.build_mcp_servers(
|
||||
allowed_tools=["mcp:Gmail", "mcp:Slack", "mcp:Notion"],
|
||||
@@ -117,8 +117,8 @@ async def test_gate_unset_active_mcps_legacy_allows_all():
|
||||
"""Pre-gate sessions use active_mcps=None → everything allowed (back-compat)."""
|
||||
from backend.apps.agents.agent_manager import AgentManager
|
||||
fake_tools = [p_fake_tool("Gmail"), p_fake_tool("Slack")]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
result = await mgr.build_mcp_servers(
|
||||
allowed_tools=["mcp:Gmail", "mcp:Slack"],
|
||||
@@ -133,7 +133,7 @@ async def test_gate_disabled_tool_blocked_even_when_activated():
|
||||
"""Tool with enabled=False stays blocked even if in active_mcps."""
|
||||
from backend.apps.agents.agent_manager import AgentManager
|
||||
fake_tools = [p_fake_tool("Gmail", enabled=False)]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools):
|
||||
mgr = AgentManager()
|
||||
result = await mgr.build_mcp_servers(
|
||||
allowed_tools=["mcp:Gmail"],
|
||||
@@ -147,7 +147,7 @@ async def test_gate_unauthed_tool_blocked():
|
||||
"""Tool with auth_status='disconnected' stays blocked."""
|
||||
from backend.apps.agents.agent_manager import AgentManager
|
||||
fake_tools = [p_fake_tool("Gmail", auth_status="disconnected")]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools):
|
||||
mgr = AgentManager()
|
||||
result = await mgr.build_mcp_servers(
|
||||
allowed_tools=["mcp:Gmail"],
|
||||
@@ -161,8 +161,8 @@ async def test_gate_allowed_tools_filter_intersects_active_mcps():
|
||||
"""Activate gmail+slack but allowed_tools only has gmail → only gmail passes."""
|
||||
from backend.apps.agents.agent_manager import AgentManager
|
||||
fake_tools = [p_fake_tool("Gmail"), p_fake_tool("Slack")]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
result = await mgr.build_mcp_servers(
|
||||
allowed_tools=["mcp:Gmail"], # mode-restricted
|
||||
@@ -192,10 +192,10 @@ async def test_gate_stress_random_activations():
|
||||
# allowed_tools mirrors raw names of connected
|
||||
allowed = [f"mcp:{raw_names[i]}" for i in connected_idx]
|
||||
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_airtable_token", new=AsyncMock(return_value=True)), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_hubspot_token", new=AsyncMock(return_value=True)):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_airtable_token", new=AsyncMock(return_value=True)), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_hubspot_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
result = await mgr.build_mcp_servers(
|
||||
allowed_tools=allowed,
|
||||
@@ -642,11 +642,11 @@ async def test_mcp_gate_only_forwards_activated_servers():
|
||||
|
||||
# allowed_tools == get_all_tool_names() bypasses the (separate) permission
|
||||
# gate so we isolate the ACTIVATION gate. sanitize_server_name -> identity.
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", side_effect=installed), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.get_all_tool_names", return_value=["__ALL__"]), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.sanitize_server_name", side_effect=lambda n: n), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.is_fully_denied", return_value=False), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.derive_mcp_config", side_effect=lambda t: {"command": "x"}):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", side_effect=installed), \
|
||||
patch("backend.apps.agents.manager.RunSupport.get_all_tool_names", return_value=["__ALL__"]), \
|
||||
patch("backend.apps.agents.manager.RunSupport.sanitize_server_name", side_effect=lambda n: n), \
|
||||
patch("backend.apps.agents.manager.RunSupport.is_fully_denied", return_value=False), \
|
||||
patch("backend.apps.agents.manager.RunSupport.derive_mcp_config", side_effect=lambda t: {"command": "x"}):
|
||||
allowed = ["__ALL__"]
|
||||
# Boundary 1: empty activation list -> zero servers, always.
|
||||
assert await mgr.build_mcp_servers(allowed, active_mcps=[]) == {}
|
||||
@@ -963,8 +963,8 @@ async def test_concurrent_gate_calls_isolated():
|
||||
"""Two concurrent _build_mcp_servers calls with different active_mcps must not cross-contaminate."""
|
||||
from backend.apps.agents.agent_manager import AgentManager
|
||||
fake_tools = [p_fake_tool("Gmail"), p_fake_tool("Slack"), p_fake_tool("Notion")]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
results = await asyncio.gather(
|
||||
mgr.build_mcp_servers(allowed_tools=["mcp:Gmail", "mcp:Slack", "mcp:Notion"], active_mcps=["gmail"]),
|
||||
@@ -1184,7 +1184,7 @@ async def test_gate_handles_missing_refresh_token_gracefully():
|
||||
from backend.apps.agents.agent_manager import AgentManager
|
||||
fake = p_fake_tool("MyApiTool", auth_status="configured")
|
||||
fake.auth_type = None # no oauth
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=[fake]):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=[fake]):
|
||||
mgr = AgentManager()
|
||||
result = await mgr.build_mcp_servers(
|
||||
allowed_tools=["mcp:MyApiTool"],
|
||||
@@ -3026,8 +3026,8 @@ def test_view_builder_mode_has_default_folder():
|
||||
async def test_gate_100_sequential_calls_no_leak():
|
||||
from backend.apps.agents.agent_manager import AgentManager
|
||||
fake_tools = [p_fake_tool(f"Server{i}") for i in range(10)]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
for i in range(100):
|
||||
n = i % 10
|
||||
@@ -3141,8 +3141,8 @@ async def test_e2e_session_lifecycle_with_mcp_activation():
|
||||
from backend.apps.agents.agent_manager import AgentManager
|
||||
from backend.apps.agents.core.models import AgentSession
|
||||
fake_tools = [p_fake_tool("Gmail"), p_fake_tool("Slack")]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools", return_value=fake_tools), \
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
s = AgentSession(id="e2e", name="End-to-end", model="sonnet", mode="agent")
|
||||
|
||||
@@ -3184,9 +3184,9 @@ async def test_e2e_50_random_activation_sequences():
|
||||
("Discord", "discord"), ("GitHub", "github"), ("Linear", "linear")]
|
||||
raw_names = [r for r, _ in server_pool]
|
||||
sanitized = [s for _, s in server_pool]
|
||||
with patch("backend.apps.agents.manager.RunSupportMixin.load_all_tools",
|
||||
with patch("backend.apps.agents.manager.RunSupport.load_all_tools",
|
||||
return_value=[p_fake_tool(r) for r in raw_names]), \
|
||||
patch("backend.apps.agents.manager.RunSupportMixin.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
patch("backend.apps.agents.manager.RunSupport.refresh_google_token", new=AsyncMock(return_value=True)):
|
||||
mgr = AgentManager()
|
||||
for _ in range(50):
|
||||
n = random.randint(0, len(sanitized))
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"max-file-lines-exceptions": "Grandfather list of pre-existing >300-line files (existing debt, not new). Paths updated after the folder-tree restructure moved several of them. The two manager/prompt/* entries are from the agent_manager decomposition: prompt_context.py aggregates the system-prompt context builders and attachments.py is one cohesive 230-line attachment resolver; both are single-responsibility and a few lines over, not splittable without an artificial seam.",
|
||||
"max-folder-items-exceptions": "Exact-path allow for folders intentionally over the cap. The rule trips at >7 (7 items is fine, the 8th tips it), so only genuinely 8+ folders are listed. backend/ and backend/apps are FastAPI feature-package registries (each child is an app mounted in main.py); agents/ aggregates agent subsystems; agents/manager/ is the agent_manager god-object decomposition (cohesive AgentManager mixins + standalone run helpers + the streaming/permissions/prompt/session subtrees), conventionally flat like agents/ and core/ since its standalone helpers are heterogeneous and don't group cleanly; agents/manager/streaming and agents/manager/session are flat peer collections of one-module-per-concern handlers; core/, tools_lib/, tests/ are conventionally flat. Frontend: app/pages is the page registry, AgentChat/ChatInput/Settings-sections/Onboarding are organizational parents, and shared/state (Redux slices) plus hooks/steps/mcp-cards/Views are flat peer collections. scripts/, electron/, linter/checks/ are flat tool dirs. These replaced blanket .lintignore-max-folder-items sentinels (backend, frontend, scripts, electron, linter/checks) so the rule still catches NEW unplanned bloat everywhere else. Kept as whole-subtree sentinels on purpose: debugger/ (self-contained injected sub-tool with its own Vite GUI), webapp_template (Vite scaffold payload), and vendored mcp-bundles.",
|
||||
"import-cycles": "Flags RUNTIME circular imports only (SCC>1). Skips type-only imports (import type / export type) and dynamic import() since neither runs at module init, which is why the idiomatic Redux store<->hooks type cycle is not flagged. Frontend alias resolution comes from import-cycle-aliases. Zero cycles today; the check keeps it that way.",
|
||||
"ruff + pyright": "Ported from Haik's linter (haik/feat/ingest). ruff is narrowed to F401/F811/F841 (unused imports/redefs/locals) and intentionally DROPS Haik's ARG001/ARG002 (unused args): our SDK-callback signatures require unused params (can_use_tool/pre_tool_hook take a `context` they don't use) and we ban the `_unused` prefix, so ARG is noise here. pyright runs Haik's existence-only config (typeCheckingMode off) with reportAttributeAccessIssue ENABLED: the AgentManager mixins now inherit a typing-only AgentManagerState base (manager/AgentManagerState.py) that declares the composed __init__ state + cross-mixin methods, so the checker sees self.sessions etc. from inside a mixin. pyright caught real bugs: a dangling `_conns` ref + TWO broken lazy imports (`_load_all`/`_load` from outputs.py, renamed to load_all/load in workspace_io but the import sites weren't updated — App Builder workspace seeding/name-sync was silently failing in a try/except). The one grandfathered SURFACE file (handle_assistant_message) is the SDK-optional try/except-import boundary (TextBlock=object fallback defeats isinstance narrowing). Both grandfather pre-existing debt by file; the refactor surface is clean. Requires `ruff` + `pyright` on PATH (added to requirements-dev.txt); pyright's config expects the venv at backend/.venv.",
|
||||
"ruff + pyright": "Ported from Haik's linter (haik/feat/ingest). ruff is narrowed to F401/F811/F841 (unused imports/redefs/locals) and intentionally DROPS Haik's ARG001/ARG002 (unused args): our SDK-callback signatures require unused params (can_use_tool/pre_tool_hook take a `context` they don't use) and we ban the `_unused` prefix, so ARG is noise here. pyright runs Haik's existence-only config (typeCheckingMode off) with reportAttributeAccessIssue ENABLED: the AgentManager behavior classes now inherit a typing-only AgentManagerProtocol base (manager/AgentManagerProtocol.py) that declares the composed __init__ state + cross-class methods, so the checker sees self.sessions etc. from inside a mixin. pyright caught real bugs: a dangling `_conns` ref + TWO broken lazy imports (`_load_all`/`_load` from outputs.py, renamed to load_all/load in workspace_io but the import sites weren't updated — App Builder workspace seeding/name-sync was silently failing in a try/except). The one grandfathered SURFACE file (handle_assistant_message) is the SDK-optional try/except-import boundary (TextBlock=object fallback defeats isinstance narrowing). Both grandfather pre-existing debt by file; the refactor surface is clean. Requires `ruff` + `pyright` on PATH (added to requirements-dev.txt); pyright's config expects the venv at backend/.venv.",
|
||||
"no-underscore-names + p-private": "Convention checks ported verbatim from Haik's linter (haik/feat/ingest): no-underscore-names bans leading-underscore names (a dead-code-tooling blind spot; use p_ for private), p-private enforces that p_-prefixed names are accessed only inside their owning file/class (cross-file/class use means the name should be public). Backend Python only. The exception lists grandfather pre-existing debt that landed with the workflows/analytics forward-ports (eric's 'don't mass-migrate untouched files' rule); the agent_manager refactor surface is clean. NOTE: Haik's full linter (his branch also adds pyright + ruff and runs a different enabled set) should eventually supersede this; these two were lifted to enforce the p_ conventions on eric/dev now."
|
||||
},
|
||||
"rules": {
|
||||
|
||||
Reference in New Issue
Block a user