mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-17 18:25:42 +02:00
[eric] agents: typed AgentManagerState mixin base re-enables pyright attr-access + fixes 2 broken outputs imports (pyright caught them)
This commit is contained in:
@@ -215,7 +215,8 @@ class AgentManager(SessionLifecycleMixin, SessionPersistenceMixin, MessagingMixi
|
||||
# the row's name is still the default placeholder.
|
||||
if session.mode == "view-builder":
|
||||
try:
|
||||
from backend.apps.outputs.outputs import sync_output_from_meta_json, _load_all as load_outputs
|
||||
from backend.apps.outputs.outputs import sync_output_from_meta_json
|
||||
from backend.apps.outputs.workspace_io import load_all as load_outputs
|
||||
if sync_output_from_meta_json(session_id, fallback_name=session.name):
|
||||
# Broadcast the renamed row so the sidebar
|
||||
# flips from "Untitled App" to the real name
|
||||
|
||||
@@ -29,7 +29,10 @@ from backend.apps.agents.manager.prompt.prompt_context import resolve_mode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentLaunchMixin:
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
|
||||
class AgentLaunchMixin(AgentManagerState):
|
||||
@typechecked
|
||||
async def launch_agent(self, config: AgentConfig) -> AgentSession:
|
||||
session_id = uuid4().hex
|
||||
@@ -78,8 +81,8 @@ class AgentLaunchMixin:
|
||||
try:
|
||||
from backend.apps.outputs.outputs import (
|
||||
ensure_webapp_workspace_seeded_and_registered,
|
||||
_load as load_output,
|
||||
)
|
||||
from backend.apps.outputs.workspace_io import load as load_output
|
||||
output_id = ensure_webapp_workspace_seeded_and_registered(
|
||||
workspace_id=session_id,
|
||||
folder=effective_cwd,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""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
|
||||
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
|
||||
methods) so each mixin inherits a typed view of the whole. It carries NO runtime
|
||||
behavior: the attribute lines are bare annotations (lazy via __future__) and the
|
||||
methods live in a TYPE_CHECKING block, so at runtime this is an empty class that
|
||||
just sits once in the MRO. Re-enables the linter's pyright reportAttributeAccessIssue.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, Any, Dict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.apps.agents.core.models import AgentSession
|
||||
from backend.apps.agents.manager.streaming.LivePartial import LivePartial
|
||||
|
||||
|
||||
class AgentManagerState:
|
||||
# State set in AgentManager.__init__.
|
||||
sessions: Dict[str, AgentSession]
|
||||
tasks: Dict[str, asyncio.Task]
|
||||
live_partial: Dict[str, LivePartial]
|
||||
cancel_events: Dict[str, asyncio.Event]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Methods implemented on sibling mixins / AgentManager itself and called
|
||||
# cross-mixin. Loose signatures on purpose: typeCheckingMode is off, so this
|
||||
# only has to assert the names exist, not pin their call shapes.
|
||||
def run_agent_loop(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def generate_turn_label(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def commit_partial_now(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def stop_agent(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def drain_task(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def sync_session_close(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def build_mcp_servers(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def build_search_text(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def stream_text(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
def stream_tool_input(self, *args: Any, **kwargs: Any) -> Any: ...
|
||||
@@ -21,7 +21,10 @@ from backend.apps.agents.manager.prompt.prompt_context import resolve_mode
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MessagingMixin:
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
|
||||
class MessagingMixin(AgentManagerState):
|
||||
@typechecked
|
||||
async def send_message(
|
||||
self,
|
||||
|
||||
@@ -17,7 +17,10 @@ from backend.apps.agents.core.ws_manager import ws_manager
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MockAgentMixin:
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
|
||||
class MockAgentMixin(AgentManagerState):
|
||||
@typechecked
|
||||
async def run_mock_agent(self, session_id: str, prompt: str):
|
||||
"""Mock agent loop for development without claude_agent_sdk installed."""
|
||||
|
||||
@@ -33,11 +33,12 @@ from backend.apps.tools_lib.tools_lib import (
|
||||
refresh_google_token,
|
||||
refresh_hubspot_token,
|
||||
)
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunSupportMixin:
|
||||
class RunSupportMixin(AgentManagerState):
|
||||
@typechecked
|
||||
async def build_mcp_servers(
|
||||
self,
|
||||
|
||||
@@ -15,7 +15,10 @@ from backend.apps.agents.manager.session.session_store import save_session
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionControlMixin:
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
|
||||
class SessionControlMixin(AgentManagerState):
|
||||
@typechecked
|
||||
async def stop_agent(self, session_id: str):
|
||||
"""Stop a running agent and all its browser-agent children."""
|
||||
|
||||
@@ -10,6 +10,7 @@ from typing import Dict, List, Optional
|
||||
from typeguard import typechecked
|
||||
|
||||
from backend.apps.agents.core.models import AgentSession
|
||||
from backend.apps.settings.models import AppSettings
|
||||
from backend.auth import get_auth_token
|
||||
|
||||
logger = __import__("logging").getLogger(__name__)
|
||||
@@ -21,7 +22,7 @@ async def configure_provider_env(
|
||||
session: AgentSession,
|
||||
resolved_model: object,
|
||||
api_type: Optional[str],
|
||||
global_settings: object,
|
||||
global_settings: AppSettings,
|
||||
sub_conns: List,
|
||||
) -> None:
|
||||
from backend.apps.nine_router import is_running as nine_router_running
|
||||
|
||||
@@ -187,7 +187,7 @@ async def generate_group_meta(
|
||||
if not session:
|
||||
raise ValueError(f"Session {session_id} not found")
|
||||
|
||||
fallback_name = tool_calls[0].get("tool", "Tool calls") if tool_calls else "Tool calls"
|
||||
fallback_name = str(tool_calls[0].get("tool", "Tool calls")) if tool_calls else "Tool calls"
|
||||
fallback_name = fallback_name.split("__")[-1].replace("_", " ").title() if "__" in fallback_name else fallback_name
|
||||
|
||||
name = fallback_name
|
||||
|
||||
@@ -34,7 +34,10 @@ from backend.apps.agents.manager.run.run_options_helpers import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RunOptionsMixin:
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
|
||||
class RunOptionsMixin(AgentManagerState):
|
||||
# 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
|
||||
|
||||
@@ -22,7 +22,10 @@ from backend.apps.settings.models import AppSettings
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TurnRunnerMixin:
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
|
||||
class TurnRunnerMixin(AgentManagerState):
|
||||
# `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
|
||||
|
||||
@@ -28,7 +28,10 @@ from backend.apps.agents.manager.view_builder_state import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionLifecycleMixin:
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
|
||||
class SessionLifecycleMixin(AgentManagerState):
|
||||
@staticmethod
|
||||
@typechecked
|
||||
def build_search_text(session: AgentSession, max_len: int = 5000) -> str:
|
||||
|
||||
@@ -19,7 +19,10 @@ from backend.apps.agents.manager.session.apply_context_window import apply_conte
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SessionPersistenceMixin:
|
||||
from backend.apps.agents.manager.AgentManagerState import AgentManagerState
|
||||
|
||||
|
||||
class SessionPersistenceMixin(AgentManagerState):
|
||||
@typechecked
|
||||
async def reconcile_on_startup(self) -> None:
|
||||
"""Mark any stale running sessions as stopped."""
|
||||
|
||||
@@ -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) but DISABLES reportAttributeAccessIssue: our AgentManager is decomposed into mixins that read attributes defined on the composed class (self.sessions etc.), which that rule can't see without a typed mixin base — 82 false positives. Kept reportUndefinedVariable + reportMissingImports, which caught a real dangling `_conns` ref in configure_provider_env. Re-enabling attribute-access cleanly needs a typed mixin contract (future). 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 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.",
|
||||
"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": {
|
||||
@@ -248,10 +248,19 @@
|
||||
"backend/tests/test_ws_integration.py"
|
||||
],
|
||||
"pyright": [
|
||||
"backend/apps/agents/agents.py",
|
||||
"backend/apps/agents/browser_agent_mcp_server.py",
|
||||
"backend/apps/agents/manager/streaming/handle_assistant_message.py",
|
||||
"backend/apps/google_workspace_mcp_shim/run.py",
|
||||
"backend/apps/health/health.py",
|
||||
"backend/apps/settings/settings.py",
|
||||
"backend/apps/subscription/free_trial.py",
|
||||
"backend/apps/swarm/closure.py",
|
||||
"backend/apps/swarm/entities/modes.py",
|
||||
"backend/apps/web/web.py",
|
||||
"backend/apps/workflows/executor.py",
|
||||
"backend/apps/workflows/workflows.py",
|
||||
"backend/auth.py",
|
||||
"backend/config/Apps.py"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"extraPaths": ["../.."],
|
||||
"reportMissingTypeStubs": false,
|
||||
"typeCheckingMode": "off",
|
||||
"reportAttributeAccessIssue": "none",
|
||||
"reportAttributeAccessIssue": "error",
|
||||
"reportUndefinedVariable": "error",
|
||||
"reportMissingImports": "error"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user