diff --git a/backend/apps/agents/agent_mock.py b/backend/apps/agents/agent_mock.py deleted file mode 100644 index 774f9265..00000000 --- a/backend/apps/agents/agent_mock.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Mock agent and session-completed analytics. - -Extracted from agent_loop.py to keep every file under 250 lines. -""" - -from __future__ import annotations - -import asyncio -import json -import logging -from datetime import datetime -from uuid import uuid4 - -from backend.apps.agents.models import AgentSession, ApprovalRequest, Message -from backend.apps.agents.ws_manager import ws_manager -from backend.apps.analytics.collector import record as _analytics - -logger = logging.getLogger(__name__) - - -async def stream_text(session_id: str, msg_id: str, text: str, delay: float = 0.03): - await ws_manager.emit_stream_start(session_id, msg_id, "assistant") - words = text.split(" ") - for i, word in enumerate(words): - chunk = word if i == 0 else " " + word - await ws_manager.emit_stream_delta(session_id, msg_id, chunk) - await asyncio.sleep(delay) - await ws_manager.emit_stream_end(session_id, msg_id) - - -async def stream_tool_input(session_id: str, msg_id: str, tool_name: str, input_json: str, delay: float = 0.02): - await ws_manager.emit_stream_start(session_id, msg_id, "tool_call", tool_name=tool_name) - chunk_size = 12 - for i in range(0, len(input_json), chunk_size): - await ws_manager.emit_stream_delta(session_id, msg_id, input_json[i:i + chunk_size]) - await asyncio.sleep(delay) - await ws_manager.emit_stream_end(session_id, msg_id) - - -def fire_session_completed(session: AgentSession, sessions_dict: dict[str, AgentSession]): - duration = 0.0 - if session.created_at: - end = session.closed_at or datetime.now() - duration = (end - session.created_at).total_seconds() - tool_names = [ - m.content.get("tool", "") for m in session.messages - if m.role == "tool_call" and isinstance(m.content, dict) - ] - user_messages = [ - (m.content if isinstance(m.content, str) else str(m.content))[:200] - for m in session.messages if m.role == "user" - ] - _analytics("session.completed", { - "model": session.model, - "provider": getattr(session, "provider", "anthropic"), - "mode": session.mode, - "cost_usd": session.cost_usd, - "message_count": len([m for m in session.messages if m.role in ("user", "assistant")]), - "duration_seconds": round(duration, 1), - "status": session.status, - "tool_count": len(tool_names), - "tools_list": list(set(tool_names)), - "session_title": session.name, - "first_user_message": user_messages[0] if user_messages else "", - "input_tokens": session.tokens.get("input", 0), - "output_tokens": session.tokens.get("output", 0), - "is_sub_agent": session.parent_session_id is not None, - "parent_session_id": session.parent_session_id, - "sub_agent_count": len([s for s in sessions_dict.values() if s.parent_session_id == session.id]), - "branch_count": len(session.branches), - }, session_id=session.id, dashboard_id=session.dashboard_id) - - -async def run_mock_agent(session_id: str, prompt: str, sessions: dict[str, AgentSession]): - session = sessions.get(session_id) - if not session: - return - - await asyncio.sleep(1) - - request_id = uuid4().hex - approval_req = ApprovalRequest( - id=request_id, session_id=session_id, tool_name="Bash", - tool_input={"command": f"echo 'Processing: {prompt}'", "description": "Echo the user prompt"}, - ) - session.pending_approvals.append(approval_req) - session.status = "waiting_approval" - await ws_manager.emit_status(session_id, "waiting_approval") - - decision = await ws_manager.send_approval_request( - session_id, request_id, "Bash", - {"command": f"echo 'Processing: {prompt}'", "description": "Echo the user prompt"}, - ) - - session.pending_approvals = [a for a in session.pending_approvals if a.id != request_id] - session.status = "running" - await ws_manager.emit_status(session_id, "running") - - tool_input_content = {"tool": "Bash", "input": {"command": f"echo 'Processing: {prompt}'"}, "approved": decision.get("behavior") == "allow"} - tool_msg_id = uuid4().hex - await stream_tool_input(session_id, tool_msg_id, "Bash", json.dumps(tool_input_content["input"], indent=2)) - tool_msg = Message(id=tool_msg_id, role="tool_call", content=tool_input_content, branch_id=session.active_branch_id) - session.messages.append(tool_msg) - await ws_manager.emit_message(session_id, tool_msg) - - await asyncio.sleep(1) - - if decision.get("behavior") == "allow": - tool_result = Message(role="tool_result", content=f"Processing: {prompt}", branch_id=session.active_branch_id) - session.messages.append(tool_result) - await ws_manager.emit_message(session_id, tool_result) - - await asyncio.sleep(1) - - asst_text = ( - f"I've processed your request: \"{prompt}\"\n\n" - "This is a mock response because `claude-agent-sdk` is not installed. " - "Install it with `pip install claude-agent-sdk` to use real Claude Code instances.\n\n" - f"The agent was configured with:\n- Model: {session.model}\n- Mode: {session.mode}" - ) - asst_msg_id = uuid4().hex - await stream_text(session_id, asst_msg_id, asst_text) - - asst_msg = Message(id=asst_msg_id, role="assistant", content=asst_text, branch_id=session.active_branch_id) - session.messages.append(asst_msg) - await ws_manager.emit_message(session_id, asst_msg) - - session.status = "completed" - session.closed_at = datetime.now() - session.cost_usd = 0.001 - await ws_manager.emit_status(session_id, "completed", session) - await ws_manager.emit_cost_update(session_id, session.cost_usd) diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index e7319fc7..033b149b 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -1,11 +1,10 @@ from backend.config.Apps import SubApp -from backend.apps.agents.agent_manager import agent_manager -from backend.apps.agents.ws_manager import ws_manager +from backend.apps.agents.manager.agent_manager import agent_manager from backend.apps.agents.models import AgentConfig, ApprovalResponse +from backend.apps.agents.browser.runner import run_browser_agents from contextlib import asynccontextmanager from fastapi import HTTPException, Request from fastapi.responses import JSONResponse -from uuid import uuid4 import logging logger = logging.getLogger(__name__) @@ -169,25 +168,9 @@ async def resume_session(session_id: str): return {"session": session.model_dump(mode="json")} -@agents.router.post("/browser/command") -async def browser_command(request: Request): - """Proxy browser commands to the frontend via WebSocket and wait for results.""" - body = await request.json() - action = body.get("action", "") - browser_id = body.get("browser_id", "") - tab_id = body.get("tab_id", "") - params = body.get("params", {}) - if not action or not browser_id: - return JSONResponse({"error": "action and browser_id are required"}, status_code=400) - request_id = uuid4().hex - result = await ws_manager.send_browser_command(request_id, action, browser_id, params, tab_id=tab_id) - return JSONResponse(result) - - @agents.router.post("/browser-agent/run") async def browser_agent_run(request: Request): """Run one or more browser sub-agents in parallel.""" - from backend.apps.agents.browser_agent import run_browser_agents body = await request.json() tasks = body.get("tasks", []) if not tasks: diff --git a/backend/apps/agents/browser_agent_mcp_schemas.py b/backend/apps/agents/browser/browser_agent_mcp_schemas.py similarity index 100% rename from backend/apps/agents/browser_agent_mcp_schemas.py rename to backend/apps/agents/browser/browser_agent_mcp_schemas.py diff --git a/backend/apps/agents/browser_agent_mcp_server.py b/backend/apps/agents/browser/browser_agent_mcp_server.py similarity index 100% rename from backend/apps/agents/browser_agent_mcp_server.py rename to backend/apps/agents/browser/browser_agent_mcp_server.py diff --git a/backend/apps/agents/browser/executor.py b/backend/apps/agents/browser/executor.py index 000584d5..bf3f3870 100644 --- a/backend/apps/agents/browser/executor.py +++ b/backend/apps/agents/browser/executor.py @@ -7,8 +7,8 @@ import logging from uuid import uuid4 from backend.apps.agents.models import AgentSession -from backend.apps.agents.ws_manager import ws_manager -from backend.apps.agents.approval import request_approval +from backend.apps.agents.manager.ws_manager import ws_manager +from backend.apps.agents.execution.approval import request_approval from backend.apps.agents.browser.schemas import ACTION_MAP logger = logging.getLogger(__name__) diff --git a/backend/apps/agents/browser/runner.py b/backend/apps/agents/browser/runner.py index de3fd131..22c77cd7 100644 --- a/backend/apps/agents/browser/runner.py +++ b/backend/apps/agents/browser/runner.py @@ -9,9 +9,17 @@ from datetime import datetime from uuid import uuid4 from backend.apps.agents.models import AgentSession, Message -from backend.apps.agents.ws_manager import ws_manager +from backend.apps.agents.manager.ws_manager import ws_manager from backend.apps.common.model_registry import resolve_model_id from backend.apps.tools_lib.tools_lib import load_builtin_permissions +from backend.apps.agents.manager.agent_manager import agent_manager +from backend.apps.settings.settings import load_settings +from backend.apps.settings.credentials import get_anthropic_client +from backend.apps.common.llm_helpers import _resolve_model as _resolve_9r +from backend.apps.dashboards.dashboards import _load, _save +from backend.apps.dashboards.models import BrowserCardPosition, BrowserTab +from backend.apps.analytics.collector import record as _analytics + from backend.apps.agents.browser.schemas import ( BROWSER_TOOLS_SCHEMA, SYSTEM_PROMPT, MAX_TURNS, ) @@ -28,7 +36,6 @@ async def run_browser_agent( pre_selected: bool = False, initial_url: str | None = None, parent_session_id: str | None = None, ) -> dict: - from backend.apps.agents.agent_manager import agent_manager _browser_perms = load_builtin_permissions() session_id = uuid4().hex @@ -49,9 +56,6 @@ async def run_browser_agent( logger.info(f"Browser agent {session_id}: navigated to {initial_url}: {nav_result.get('text', nav_result.get('error', ''))}") api_model = resolve_model_id(model) - from backend.apps.settings.settings import load_settings - from backend.apps.settings.credentials import get_anthropic_client - from backend.apps.common.llm_helpers import _resolve_model as _resolve_9r _settings = load_settings() api_model = _resolve_9r(api_model, _settings) client = get_anthropic_client(_settings) @@ -168,8 +172,6 @@ async def run_browser_agent( async def _create_browser_card(dashboard_id: str, url: str, parent_session_id: str | None = None) -> str: - from backend.apps.dashboards.dashboards import _load, _save - from backend.apps.dashboards.models import BrowserCardPosition, BrowserTab dashboard = _load(dashboard_id) browser_id = f"browser-{uuid4().hex[:8]}" @@ -196,7 +198,6 @@ async def run_browser_agents( pre_selected_browser_ids: list[str] | None = None, parent_session_id: str | None = None, ) -> list[dict]: - from backend.apps.analytics.collector import record as _analytics _analytics("feature.used", { "feature": "browser_agent.launched", "task_count": len(tasks), "model": model, }, dashboard_id=dashboard_id) diff --git a/backend/apps/agents/browser_agent.py b/backend/apps/agents/browser_agent.py deleted file mode 100644 index 1d8c04f7..00000000 --- a/backend/apps/agents/browser_agent.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Backward-compatible shim — re-exports from the browser sub-package.""" - -from backend.apps.agents.browser.runner import ( # noqa: F401 - run_browser_agent, - run_browser_agents, -) -from backend.apps.agents.browser.executor import ( # noqa: F401 - execute_browser_tool, -) diff --git a/backend/apps/agents/browser_mcp_schemas.py b/backend/apps/agents/browser_mcp_schemas.py deleted file mode 100644 index 7af777b5..00000000 --- a/backend/apps/agents/browser_mcp_schemas.py +++ /dev/null @@ -1,212 +0,0 @@ -"""Tool schema definitions for the browser MCP server.""" - -TAB_ID_PROP = { - "type": "string", - "description": "Optional tab ID within the browser card. If omitted, targets the active tab.", -} - -TOOLS = [ - { - "name": "BrowserScreenshot", - "description": ( - "Capture a screenshot of the browser page. Returns the screenshot as a " - "base64-encoded PNG image. Use this to see what is currently displayed." - ), - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID to capture. Use the ID from the selected browser card context.", - }, - "tab_id": TAB_ID_PROP, - }, - "required": ["browser_id"], - }, - }, - { - "name": "BrowserGetText", - "description": ( - "Get the visible text content of the browser page. Returns the page's " - "innerText (up to 15000 characters)." - ), - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID.", - }, - "tab_id": TAB_ID_PROP, - }, - "required": ["browser_id"], - }, - }, - { - "name": "BrowserNavigate", - "description": "Navigate the browser to a URL.", - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID.", - }, - "tab_id": TAB_ID_PROP, - "url": { - "type": "string", - "description": "The URL to navigate to.", - }, - }, - "required": ["browser_id", "url"], - }, - }, - { - "name": "BrowserClick", - "description": ( - "Click an element in the browser page identified by a CSS selector." - ), - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID.", - }, - "tab_id": TAB_ID_PROP, - "selector": { - "type": "string", - "description": "CSS selector of the element to click.", - }, - }, - "required": ["browser_id", "selector"], - }, - }, - { - "name": "BrowserType", - "description": ( - "Type text into an input element in the browser page. Clears the " - "existing value first, then types the new text." - ), - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID.", - }, - "tab_id": TAB_ID_PROP, - "selector": { - "type": "string", - "description": "CSS selector of the input element.", - }, - "text": { - "type": "string", - "description": "The text to type.", - }, - }, - "required": ["browser_id", "selector", "text"], - }, - }, - { - "name": "BrowserEvaluate", - "description": ( - "Evaluate a JavaScript expression in the browser page and return the result. " - "The expression is run via executeJavaScript on the webview." - ), - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID.", - }, - "tab_id": TAB_ID_PROP, - "expression": { - "type": "string", - "description": "JavaScript expression to evaluate.", - }, - }, - "required": ["browser_id", "expression"], - }, - }, - { - "name": "BrowserGetElements", - "description": ( - "Get a list of interactive elements on the page with their CSS selectors. " - "Returns clickable elements, inputs, links, and buttons with selector paths " - "you can use with BrowserClick and BrowserType. Call this BEFORE attempting " - "to click or type so you know which selectors are valid." - ), - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID.", - }, - "tab_id": TAB_ID_PROP, - "selector": { - "type": "string", - "description": ( - "Optional CSS selector to scope the search " - "(e.g. 'form', '#main'). Defaults to 'body'." - ), - }, - }, - "required": ["browser_id"], - }, - }, - { - "name": "BrowserScroll", - "description": ( - "Scroll the page up or down. Automatically finds the correct scrollable " - "container (works on SPAs like Notion, Gmail, etc. that use nested scroll " - "containers instead of window-level scrolling). Returns scroll position info " - "including whether top/bottom has been reached." - ), - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID.", - }, - "tab_id": TAB_ID_PROP, - "direction": { - "type": "string", - "enum": ["up", "down"], - "description": "Scroll direction. Defaults to 'down'.", - }, - "amount": { - "type": "number", - "description": "Pixels to scroll. Defaults to 500.", - }, - }, - "required": ["browser_id"], - }, - }, - { - "name": "BrowserWait", - "description": ( - "Wait for a specified duration. Useful after navigation or actions that " - "trigger page loads, animations, or async content rendering. " - "Min 100ms, max 10000ms." - ), - "inputSchema": { - "type": "object", - "properties": { - "browser_id": { - "type": "string", - "description": "The browser card ID.", - }, - "tab_id": TAB_ID_PROP, - "milliseconds": { - "type": "number", - "description": "Duration to wait in milliseconds. Defaults to 1000.", - }, - }, - "required": ["browser_id"], - }, - }, -] diff --git a/backend/apps/agents/browser_mcp_server.py b/backend/apps/agents/browser_mcp_server.py deleted file mode 100644 index a26e8e7d..00000000 --- a/backend/apps/agents/browser_mcp_server.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python3 -""" -Minimal stdio MCP server that exposes browser interaction tools. - -Launched as a subprocess by the Claude Agent SDK. Proxies tool calls -to the OpenSwarm backend via HTTP, which bridges them to the Electron -frontend via WebSocket where the actual webview lives. -""" - -import base64 -import json -import sys -import os -import urllib.request -import urllib.error -from io import BytesIO - -try: - from PIL import Image - HAS_PIL = True -except ImportError: - HAS_PIL = False - -from browser_mcp_schemas import TOOLS # noqa: E402 (sibling script import) - -BACKEND_PORT = os.environ.get("OPENSWARM_PORT", "8325") -BACKEND_URL = f"http://127.0.0.1:{BACKEND_PORT}/api/agents/browser/command" - - -def send_response(id_, result=None, error=None): - msg = {"jsonrpc": "2.0", "id": id_} - if error is not None: - msg["error"] = error - else: - msg["result"] = result - sys.stdout.write(json.dumps(msg) + "\n") - sys.stdout.flush() - -def call_backend(action: str, browser_id: str, params: dict | None = None, tab_id: str = "") -> dict: - payload = json.dumps({ - "action": action, - "browser_id": browser_id, - "tab_id": tab_id, - "params": params or {}, - }).encode() - req = urllib.request.Request( - BACKEND_URL, - data=payload, - headers={"Content-Type": "application/json"}, - method="POST", - ) - try: - with urllib.request.urlopen(req, timeout=30) as resp: - return json.loads(resp.read().decode()) - except urllib.error.HTTPError as e: - body = e.read().decode() if e.fp else str(e) - return {"error": f"HTTP {e.code}: {body}"} - except Exception as e: - return {"error": str(e)} - - -MAX_IMAGE_B64_BYTES = 400_000 - - -def compress_screenshot(b64_png: str) -> tuple[str, str] | None: - """Resize and re-encode as JPEG to stay under the stdio buffer limit.""" - if not HAS_PIL: - return None - try: - raw = base64.b64decode(b64_png) - img = Image.open(BytesIO(raw)) - max_width = 1024 - if img.width > max_width: - ratio = max_width / img.width - img = img.resize((max_width, int(img.height * ratio)), Image.LANCZOS) - buf = BytesIO() - img.convert("RGB").save(buf, format="JPEG", quality=45) - return base64.b64encode(buf.getvalue()).decode(), "image/jpeg" - except Exception: - return None - - -def handle_tool_call(tool_name: str, arguments: dict) -> dict: - browser_id = arguments.get("browser_id", "") - tab_id = arguments.get("tab_id", "") - if not browser_id: - return {"content": [{"type": "text", "text": "Error: browser_id is required"}], "isError": True} - - action_map = { - "BrowserScreenshot": "screenshot", - "BrowserGetText": "get_text", - "BrowserNavigate": "navigate", - "BrowserClick": "click", - "BrowserType": "type", - "BrowserEvaluate": "evaluate", - "BrowserGetElements": "get_elements", - "BrowserScroll": "scroll", - "BrowserWait": "wait", - } - action = action_map.get(tool_name) - if not action: - return {"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}], "isError": True} - - params = {k: v for k, v in arguments.items() if k not in ("browser_id", "tab_id")} - result = call_backend(action, browser_id, params, tab_id=tab_id) - - if "error" in result: - return {"content": [{"type": "text", "text": f"Error: {result['error']}"}], "isError": True} - - if action == "screenshot" and result.get("image"): - image_data = result["image"] - mime_type = "image/png" - - if len(image_data) > MAX_IMAGE_B64_BYTES: - compressed = compress_screenshot(image_data) - if compressed: - image_data, mime_type = compressed - - if len(image_data) > MAX_IMAGE_B64_BYTES: - return { - "content": [ - {"type": "text", "text": ( - f"Screenshot too large to return ({len(image_data)} bytes base64). " - f"URL: {result.get('url', 'unknown')}. " - "Use BrowserGetText to read the page content instead." - )}, - ], - } - - return { - "content": [ - {"type": "image", "data": image_data, "mimeType": mime_type}, - {"type": "text", "text": f"Screenshot captured. URL: {result.get('url', 'unknown')}"}, - ], - } - - text = result.get("text", result.get("data", json.dumps(result))) - return {"content": [{"type": "text", "text": str(text)}]} - - -def main(): - for line in sys.stdin: - line = line.strip() - if not line: - continue - try: - msg = json.loads(line) - except json.JSONDecodeError: - continue - - method = msg.get("method") - id_ = msg.get("id") - params = msg.get("params", {}) - - if method == "initialize": - send_response(id_, { - "protocolVersion": "2024-11-05", - "capabilities": {"tools": {}}, - "serverInfo": { - "name": "openswarm-browser", - "version": "1.0.0", - }, - }) - elif method == "notifications/initialized": - pass - elif method == "tools/list": - send_response(id_, {"tools": TOOLS}) - elif method == "tools/call": - tool_name = params.get("name", "") - arguments = params.get("arguments", {}) - result = handle_tool_call(tool_name, arguments) - send_response(id_, result) - elif method == "ping": - send_response(id_, {}) - elif id_ is not None: - send_response(id_, error={"code": -32601, "message": f"Method not found: {method}"}) - - -if __name__ == "__main__": - main() diff --git a/backend/apps/agents/execution/__init__.py b/backend/apps/agents/execution/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/agents/agent_hooks.py b/backend/apps/agents/execution/agent_hooks.py similarity index 95% rename from backend/apps/agents/agent_hooks.py rename to backend/apps/agents/execution/agent_hooks.py index 0c5813e1..c1e0aa95 100644 --- a/backend/apps/agents/agent_hooks.py +++ b/backend/apps/agents/execution/agent_hooks.py @@ -14,10 +14,11 @@ from datetime import datetime from uuid import uuid4 from backend.apps.agents.models import AgentSession, Message -from backend.apps.agents.ws_manager import ws_manager -from backend.apps.agents.approval import request_approval -from backend.apps.agents.mcp_builder import get_effective_policy +from backend.apps.agents.manager.ws_manager import ws_manager +from backend.apps.agents.execution.approval import request_approval +from backend.apps.agents.execution.mcp_builder import get_effective_policy from backend.apps.analytics.collector import record as _analytics +import asyncio logger = logging.getLogger(__name__) @@ -38,7 +39,7 @@ def create_sdk_hooks( safe_input = tool_input if isinstance(tool_input, dict) else {} return await request_approval(session, tool_name, safe_input, track_analytics=True) - async def can_use_tool(tool_name, input_data, context): + async def can_use_tool(tool_name, input_data): if tool_name != "AskUserQuestion": policy = get_effective_policy(tool_name, builtin_perms) if policy == "always_allow": @@ -50,7 +51,7 @@ def create_sdk_hooks( return PermissionResultAllow(updated_input=decision.get("updated_input", input_data)) return PermissionResultDeny(message=decision.get("message", "User denied this action")) - async def pre_tool_hook(input_data, tool_use_id, context): + async def pre_tool_hook(input_data, tool_use_id): tool_name = input_data.get("tool_name", "") hook_event = input_data.get("hook_event_name", "PreToolUse") if tool_name and tool_name != "AskUserQuestion": @@ -69,7 +70,7 @@ def create_sdk_hooks( tool_start_times[tool_use_id] = time.time() return {} - async def post_tool_hook(input_data, tool_use_id, context): + async def post_tool_hook(input_data, tool_use_id): elapsed_ms = None if tool_use_id and tool_use_id in tool_start_times: elapsed_ms = int((time.time() - tool_start_times.pop(tool_use_id)) * 1000) @@ -176,6 +177,5 @@ def _build_sub_agent_session( dashboard_id=session.dashboard_id, parent_session_id=session_id, ) sessions[sub_session_id] = sub_session - import asyncio asyncio.ensure_future(_broadcast_sub_session(sub_session)) return sub_session_id diff --git a/backend/apps/agents/agent_loop.py b/backend/apps/agents/execution/agent_loop.py similarity index 90% rename from backend/apps/agents/agent_loop.py rename to backend/apps/agents/execution/agent_loop.py index 4b0b82e7..70b4b168 100644 --- a/backend/apps/agents/agent_loop.py +++ b/backend/apps/agents/execution/agent_loop.py @@ -13,15 +13,25 @@ import logging from uuid import uuid4 from backend.apps.agents.models import AgentSession, Message -from backend.apps.agents.ws_manager import ws_manager -from backend.apps.agents.session_store import save_session -from backend.apps.agents.prompt_builder import build_prompt_content +from backend.apps.agents.manager.ws_manager import ws_manager +from backend.apps.agents.manager.session_store import save_session +from backend.apps.agents.execution.prompt_builder import build_prompt_content from backend.apps.tools_lib.tools_lib import ( _load_all as load_all_tools, load_builtin_permissions, ) from backend.apps.analytics.collector import record as _analytics -from backend.apps.agents.agent_mock import run_mock_agent, fire_session_completed +from backend.apps.agents.execution.agent_hooks import create_sdk_hooks + +from claude_agent_sdk import ( + query, ClaudeAgentOptions, AssistantMessage, ResultMessage, +) +from claude_agent_sdk.types import ( + PermissionResultAllow, PermissionResultDeny, + TextBlock, ToolUseBlock, StreamEvent, SystemMessage, +) +from backend.apps.agents.execution.agent_options import build_agent_options + logger = logging.getLogger(__name__) @@ -47,30 +57,14 @@ async def run_agent_loop( load_all_tools_fn=load_all_tools, ) - try: - from claude_agent_sdk import ( - query, ClaudeAgentOptions, AssistantMessage, ResultMessage, - ) - from claude_agent_sdk.types import ( - HookMatcher, PermissionResultAllow, PermissionResultDeny, - TextBlock, ToolUseBlock, StreamEvent, SystemMessage, - ) - except ImportError: - logger.warning("claude_agent_sdk not installed, running in mock mode") - await run_mock_agent(session_id, prompt, sessions) - return - session.status = "running" builtin_perms = load_builtin_permissions() - from backend.apps.agents.agent_hooks import create_sdk_hooks can_use_tool, pre_tool_hook, post_tool_hook = create_sdk_hooks( session, session_id, sessions, builtin_perms, PermissionResultAllow, PermissionResultDeny, ) - from backend.apps.agents.agent_options import build_agent_options - try: options_kwargs = await build_agent_options( session, builtin_perms, can_use_tool, pre_tool_hook, post_tool_hook, diff --git a/backend/apps/agents/execution/agent_mock.py b/backend/apps/agents/execution/agent_mock.py new file mode 100644 index 00000000..8316016a --- /dev/null +++ b/backend/apps/agents/execution/agent_mock.py @@ -0,0 +1,48 @@ +"""Mock agent and session-completed analytics. + +Extracted from agent_loop.py to keep every file under 250 lines. +""" + +from __future__ import annotations + +import logging +from datetime import datetime + +from backend.apps.agents.models import AgentSession +from backend.apps.analytics.collector import record as _analytics + +logger = logging.getLogger(__name__) + +def fire_session_completed(session: AgentSession, sessions_dict: dict[str, AgentSession]): + duration = 0.0 + if session.created_at: + end = session.closed_at or datetime.now() + duration = (end - session.created_at).total_seconds() + tool_names = [ + m.content.get("tool", "") for m in session.messages + if m.role == "tool_call" and isinstance(m.content, dict) + ] + user_messages = [ + (m.content if isinstance(m.content, str) else str(m.content))[:200] + for m in session.messages if m.role == "user" + ] + _analytics("session.completed", { + "model": session.model, + "provider": getattr(session, "provider", "anthropic"), + "mode": session.mode, + "cost_usd": session.cost_usd, + "message_count": len([m for m in session.messages if m.role in ("user", "assistant")]), + "duration_seconds": round(duration, 1), + "status": session.status, + "tool_count": len(tool_names), + "tools_list": list(set(tool_names)), + "session_title": session.name, + "first_user_message": user_messages[0] if user_messages else "", + "input_tokens": session.tokens.get("input", 0), + "output_tokens": session.tokens.get("output", 0), + "is_sub_agent": session.parent_session_id is not None, + "parent_session_id": session.parent_session_id, + "sub_agent_count": len([s for s in sessions_dict.values() if s.parent_session_id == session.id]), + "branch_count": len(session.branches), + }, session_id=session.id, dashboard_id=session.dashboard_id) + diff --git a/backend/apps/agents/agent_options.py b/backend/apps/agents/execution/agent_options.py similarity index 93% rename from backend/apps/agents/agent_options.py rename to backend/apps/agents/execution/agent_options.py index 0aa7eaec..6a229ff7 100644 --- a/backend/apps/agents/agent_options.py +++ b/backend/apps/agents/execution/agent_options.py @@ -11,23 +11,33 @@ import os import sys from backend.apps.agents.models import AgentSession -from backend.apps.agents.prompt_builder import resolve_mode, compose_system_prompt -from backend.apps.agents.prompt_context import ( +from backend.apps.agents.execution.prompt_builder import resolve_mode, compose_system_prompt +from backend.apps.agents.execution.prompt_context import ( build_connected_tools_context, build_outputs_context, build_browser_context, get_pre_selected_browser_ids, ) -from backend.apps.agents.mcp_builder import ( +from backend.apps.agents.execution.mcp_builder import ( FULL_TOOLS, build_mcp_servers, get_all_tool_names, _get_denied_tool_names, _get_all_known_tool_names, _is_fully_denied, ) from backend.apps.settings.settings import load_settings from backend.apps.tools_lib.tools_lib import ( - _load_all as load_all_tools, - load_builtin_permissions, + _load_all as load_all_tools ) from backend.apps.common.mcp_utils import sanitize_server_name as _sanitize_server_name from backend.ports import BACKEND_DEV_PORT, NINE_ROUTER_PORT +from claude_agent_sdk.types import HookMatcher + +from backend.apps.outputs.view_builder_templates import VIEW_BUILDER_SKILL + +from backend.apps.common.model_registry import resolve_model_id as _resolve_mid + +from backend.apps.nine_router import is_running as _9r_running + + + + logger = logging.getLogger(__name__) @@ -45,8 +55,6 @@ async def build_agent_options( Requires claude_agent_sdk types to be imported by the caller; they are passed in via the hook callables. """ - from claude_agent_sdk import ClaudeAgentOptions - from claude_agent_sdk.types import HookMatcher _, mode_sys_prompt, _ = resolve_mode(session.mode, get_all_tool_names) connected_tools_ctx = build_connected_tools_context( @@ -61,7 +69,6 @@ async def build_agent_options( ) if session.mode == "view-builder": - from backend.apps.outputs.view_builder_templates import VIEW_BUILDER_SKILL skill_block = f"\n{VIEW_BUILDER_SKILL}\n" composed_prompt = f"{composed_prompt}\n\n{skill_block}" if composed_prompt else skill_block @@ -122,7 +129,6 @@ async def build_agent_options( "include_partial_messages": True, } - from backend.apps.nine_router import is_running as _9r_running if global_settings.anthropic_api_key: options_kwargs["env"] = {"ANTHROPIC_API_KEY": global_settings.anthropic_api_key} logger.info("[MCP-DEBUG] Using direct API key") @@ -132,7 +138,6 @@ async def build_agent_options( "ANTHROPIC_BASE_URL": f"http://localhost:{NINE_ROUTER_PORT}", } options_kwargs["extra_args"] = {"bare": None} - from backend.apps.common.model_registry import resolve_model_id as _resolve_mid resolved = _resolve_mid(session.model) if not resolved.startswith("cc/"): options_kwargs["model"] = f"cc/{resolved}" diff --git a/backend/apps/agents/approval.py b/backend/apps/agents/execution/approval.py similarity index 92% rename from backend/apps/agents/approval.py rename to backend/apps/agents/execution/approval.py index 473cc842..2e780ff3 100644 --- a/backend/apps/agents/approval.py +++ b/backend/apps/agents/execution/approval.py @@ -11,7 +11,10 @@ from datetime import datetime from uuid import uuid4 from backend.apps.agents.models import AgentSession, ApprovalRequest -from backend.apps.agents.ws_manager import ws_manager +from backend.apps.agents.manager.ws_manager import ws_manager + +from backend.apps.analytics.collector import record as _analytics + logger = logging.getLogger(__name__) @@ -40,7 +43,6 @@ async def request_approval( session.status = "waiting_approval" if track_analytics: - from backend.apps.analytics.collector import record as _analytics _analytics("approval.requested", { "tool_name": tool_name, "is_first_approval_in_session": len(session.pending_approvals) == 1, @@ -63,7 +65,6 @@ async def request_approval( ) if track_analytics: - from backend.apps.analytics.collector import record as _analytics latency_ms = int((datetime.now() - approval_req.created_at).total_seconds() * 1000) _analytics("approval.resolved", { "tool_name": tool_name, diff --git a/backend/apps/agents/invoke_agent_mcp_server.py b/backend/apps/agents/execution/invoke_agent_mcp_server.py similarity index 100% rename from backend/apps/agents/invoke_agent_mcp_server.py rename to backend/apps/agents/execution/invoke_agent_mcp_server.py diff --git a/backend/apps/agents/mcp_builder.py b/backend/apps/agents/execution/mcp_builder.py similarity index 100% rename from backend/apps/agents/mcp_builder.py rename to backend/apps/agents/execution/mcp_builder.py diff --git a/backend/apps/agents/prompt_builder.py b/backend/apps/agents/execution/prompt_builder.py similarity index 96% rename from backend/apps/agents/prompt_builder.py rename to backend/apps/agents/execution/prompt_builder.py index 6aa4b661..ba98a041 100644 --- a/backend/apps/agents/prompt_builder.py +++ b/backend/apps/agents/execution/prompt_builder.py @@ -11,7 +11,9 @@ from typing import Any from backend.apps.modes.modes import load_mode from backend.apps.common.mcp_utils import sanitize_server_name as _sanitize_server_name -from backend.apps.agents.prompt_context import resolve_context_paths +from backend.apps.agents.execution.prompt_context import resolve_context_paths +from backend.apps.tools_lib.models import BUILTIN_TOOLS + logger = logging.getLogger(__name__) @@ -47,7 +49,6 @@ def resolve_forced_tools( ) -> str: if not forced_tools: return "" - from backend.apps.tools_lib.models import BUILTIN_TOOLS desc_map: dict[str, str] = {t.name: t.description for t in BUILTIN_TOOLS} tool_to_server: dict[str, str] = {} tool_to_email: dict[str, str] = {} diff --git a/backend/apps/agents/prompt_context.py b/backend/apps/agents/execution/prompt_context.py similarity index 98% rename from backend/apps/agents/prompt_context.py rename to backend/apps/agents/execution/prompt_context.py index 737d3f86..a13c6ab5 100644 --- a/backend/apps/agents/prompt_context.py +++ b/backend/apps/agents/execution/prompt_context.py @@ -13,6 +13,9 @@ import os from backend.apps.outputs.outputs import _load_all as load_all_outputs from backend.apps.common.mcp_utils import sanitize_server_name as _sanitize_server_name +from backend.apps.dashboards.dashboards import _load as load_dashboard + + logger = logging.getLogger(__name__) @@ -109,7 +112,6 @@ def build_browser_context( if not dashboard_id: return None try: - from backend.apps.dashboards.dashboards import _load as load_dashboard dashboard = load_dashboard(dashboard_id) except Exception: return None @@ -160,7 +162,6 @@ def get_pre_selected_browser_ids(dashboard_id: str | None) -> list[str]: if not dashboard_id: return [] try: - from backend.apps.dashboards.dashboards import _load as load_dashboard dashboard = load_dashboard(dashboard_id) except Exception: return [] diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/manager/agent_manager.py similarity index 94% rename from backend/apps/agents/agent_manager.py rename to backend/apps/agents/manager/agent_manager.py index b0a2ce7d..dafa950e 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/manager/agent_manager.py @@ -19,25 +19,26 @@ from typing import Optional from uuid import uuid4 from backend.apps.agents.models import AgentConfig, AgentSession, Message -from backend.apps.agents.ws_manager import ws_manager -from backend.apps.agents.prompt_builder import resolve_mode -from backend.apps.agents.mcp_builder import get_all_tool_names -from backend.apps.agents.session_store import ( - delete_session_file, get_history, +from backend.apps.agents.manager.ws_manager import ws_manager +from backend.apps.agents.execution.prompt_builder import resolve_mode +from backend.apps.agents.execution.mcp_builder import get_all_tool_names +from backend.apps.agents.manager.session_store import ( + get_history, reconcile_on_startup, get_browser_agent_children, ) -from backend.apps.agents.agent_loop import run_agent_loop -from backend.apps.agents.agent_manager_ops import ( +from backend.apps.agents.execution.agent_loop import run_agent_loop +from backend.apps.agents.manager.agent_manager_ops import ( edit_message_op, close_session_op, resume_session_op, - duplicate_session_op, invoke_agent_op, + duplicate_session_op, invoke_agent_op ) -from backend.apps.agents.agent_manager_meta import ( +from backend.apps.agents.manager.agent_manager_meta import ( generate_title_op, generate_group_meta_op, - persist_all_sessions_op, restore_all_sessions_op, + persist_all_sessions_op, restore_all_sessions_op, delete_session_op ) from backend.apps.settings.settings import load_settings from backend.apps.analytics.collector import record as _analytics + logger = logging.getLogger(__name__) os.environ.setdefault("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "3600000") @@ -201,7 +202,6 @@ class AgentManager: await close_session_op(self.sessions, self.tasks, session_id) async def delete_session(self, session_id: str) -> None: - from backend.apps.agents.agent_manager_ops import delete_session_op await delete_session_op(self, session_id) async def resume_session(self, session_id: str) -> AgentSession: diff --git a/backend/apps/agents/agent_manager_meta.py b/backend/apps/agents/manager/agent_manager_meta.py similarity index 94% rename from backend/apps/agents/agent_manager_meta.py rename to backend/apps/agents/manager/agent_manager_meta.py index 673d70be..3d9180a7 100644 --- a/backend/apps/agents/agent_manager_meta.py +++ b/backend/apps/agents/manager/agent_manager_meta.py @@ -9,11 +9,14 @@ import asyncio import logging from backend.apps.agents.models import AgentSession, ToolGroupMeta -from backend.apps.agents.ws_manager import ws_manager -from backend.apps.agents.session_store import ( +from backend.apps.agents.manager.ws_manager import ws_manager +from backend.apps.agents.manager.session_store import ( save_session, delete_session_file, build_search_text, ) from backend.apps.common.llm_helpers import quick_llm_call, quick_llm_json +from backend.apps.agents.manager.session_store import load_all_session_data +from backend.apps.agents.execution.agent_mock import fire_session_completed + logger = logging.getLogger(__name__) @@ -75,7 +78,6 @@ async def generate_group_meta_op( async def persist_all_sessions_op(sessions: dict, tasks: dict) -> None: - from backend.apps.agents.agent_mock import fire_session_completed for session_id, session in list(sessions.items()): if session.status in ("running", "waiting_approval"): session.status = "stopped" @@ -92,7 +94,6 @@ async def persist_all_sessions_op(sessions: dict, tasks: dict) -> None: async def restore_all_sessions_op(sessions: dict) -> None: - from backend.apps.agents.session_store import load_all_session_data for sid, data in load_all_session_data(): try: session = AgentSession(**data) diff --git a/backend/apps/agents/agent_manager_ops.py b/backend/apps/agents/manager/agent_manager_ops.py similarity index 94% rename from backend/apps/agents/agent_manager_ops.py rename to backend/apps/agents/manager/agent_manager_ops.py index e0ccb552..c559e1a9 100644 --- a/backend/apps/agents/agent_manager_ops.py +++ b/backend/apps/agents/manager/agent_manager_ops.py @@ -14,12 +14,19 @@ from uuid import uuid4 from backend.apps.agents.models import ( AgentSession, Message, MessageBranch, ) -from backend.apps.agents.ws_manager import ws_manager -from backend.apps.agents.session_store import ( +from backend.apps.agents.manager.ws_manager import ws_manager +from backend.apps.agents.manager.session_store import ( save_session, load_session_data, delete_session_file, build_search_text, copy_session_messages, ) from backend.apps.analytics.collector import record as _analytics +from backend.apps.agents.execution.agent_loop import run_agent_loop + +from backend.apps.agents.execution.agent_loop import run_agent_loop + +from backend.apps.agents.execution.agent_mock import fire_session_completed + + logger = logging.getLogger(__name__) @@ -75,7 +82,6 @@ async def edit_message_op( session.sdk_session_id = None session.status = "running" await ws_manager.emit_status(session_id, "running", session) - from backend.apps.agents.agent_loop import run_agent_loop task = asyncio.create_task(run_agent_loop( sessions, session_id, new_content, images=target_msg.images, context_paths=target_msg.context_paths, @@ -88,8 +94,9 @@ async def close_session_op( sessions: dict, tasks: dict, session_id: str, ): - from backend.apps.agents.agent_manager import agent_manager children = [s for s in sessions.values() if s.parent_session_id == session_id and s.mode == "browser-agent"] + from backend.apps.agents.manager.agent_manager import agent_manager + # NOTE: this is a circular dependency, must be fixed soon by fixing the ai slop for child in children: await agent_manager.stop_agent(child.id) task = tasks.get(session_id) @@ -110,7 +117,6 @@ async def close_session_op( session.pending_approvals = [] if hasattr(session, '_cancel_event'): session._cancel_event.set() - from backend.apps.agents.agent_mock import fire_session_completed fire_session_completed(session, sessions) doc_data = session.model_dump(mode="json") doc_data["search_text"] = build_search_text(session) @@ -204,7 +210,6 @@ async def invoke_agent_op( user_msg = Message(role="user", content=message, branch_id=fork.active_branch_id) fork.messages.append(user_msg) await ws_manager.emit_message(fork.id, user_msg) - from backend.apps.agents.agent_loop import run_agent_loop await run_agent_loop(sessions, fork.id, message, fork_session=True) last_assistant = None for msg in reversed(fork.messages): @@ -222,11 +227,4 @@ async def invoke_agent_op( "forked_session_id": fork.id, "source_name": source_name, "response": last_assistant or "No response from invoked agent.", "cost_usd": fork.cost_usd, - } - - -from backend.apps.agents.agent_manager_meta import ( # noqa: F401 — re-exports - generate_title_op, generate_group_meta_op, - persist_all_sessions_op, restore_all_sessions_op, - delete_session_op, -) + } \ No newline at end of file diff --git a/backend/apps/agents/session_store.py b/backend/apps/agents/manager/session_store.py similarity index 100% rename from backend/apps/agents/session_store.py rename to backend/apps/agents/manager/session_store.py diff --git a/backend/apps/agents/ws_manager.py b/backend/apps/agents/manager/ws_manager.py similarity index 100% rename from backend/apps/agents/ws_manager.py rename to backend/apps/agents/manager/ws_manager.py diff --git a/backend/apps/agents/ws_routes.py b/backend/apps/agents/ws_routes.py index 9752712c..c15abaed 100644 --- a/backend/apps/agents/ws_routes.py +++ b/backend/apps/agents/ws_routes.py @@ -8,15 +8,14 @@ from __future__ import annotations import logging -from backend.apps.agents.ws_manager import ws_manager +from backend.apps.agents.manager.ws_manager import ws_manager +from backend.apps.agents.manager.agent_manager import agent_manager logger = logging.getLogger(__name__) - async def handle_session_message(session_id: str, event: str, payload: dict): """Dispatch an incoming WebSocket message for a session.""" if event == "agent:send_message": - from backend.apps.agents.agent_manager import agent_manager await agent_manager.send_message( session_id, payload.get("prompt", ""), @@ -26,28 +25,23 @@ async def handle_session_message(session_id: str, event: str, payload: dict): images=payload.get("images"), ) elif event == "agent:approval_response": - from backend.apps.agents.agent_manager import agent_manager agent_manager.handle_approval(payload.get("request_id"), { "behavior": payload.get("behavior", "deny"), "message": payload.get("message"), "updated_input": payload.get("updated_input"), }) elif event == "agent:edit_message": - from backend.apps.agents.agent_manager import agent_manager await agent_manager.edit_message( session_id, payload.get("message_id", ""), payload.get("content", ""), ) elif event == "agent:stop": - from backend.apps.agents.agent_manager import agent_manager await agent_manager.stop_agent(session_id) - async def handle_dashboard_message(event: str, payload: dict): """Dispatch an incoming WebSocket message for the dashboard.""" if event == "agent:approval_response": - from backend.apps.agents.agent_manager import agent_manager agent_manager.handle_approval(payload.get("request_id"), { "behavior": payload.get("behavior", "deny"), "message": payload.get("message"), diff --git a/backend/apps/analytics/analytics.py b/backend/apps/analytics/analytics.py index 082e7625..3a4e330f 100644 --- a/backend/apps/analytics/analytics.py +++ b/backend/apps/analytics/analytics.py @@ -10,6 +10,14 @@ from backend.config.Apps import SubApp from backend.apps.analytics.collector import init as init_collector, shutdown as shutdown_collector, record, identify from backend.apps.analytics.usage_summary import load_all_sessions, compute_session_stats, enrich_with_nine_router +from backend.apps.agents.manager.agent_manager import agent_manager +from backend.apps.settings.settings import load_settings, _save_settings +from backend.apps.agents.manager.agent_manager import agent_manager + +from backend.apps.nine_router import get_usage_stats, is_running as _9r_running + + + logger = logging.getLogger(__name__) APP_VERSION = "1.0.20" @@ -22,13 +30,11 @@ async def _heartbeat_loop(): while True: await asyncio.sleep(60) try: - from backend.apps.agents.agent_manager import agent_manager props = { "active_session_count": len(agent_manager.sessions), } try: - from backend.apps.nine_router import get_usage_stats, is_running as _9r_running if _9r_running(): stats = await get_usage_stats() if stats: @@ -64,7 +70,6 @@ async def analytics_lifespan(): logger.info("PostHog analytics initialised") try: - from backend.apps.settings.settings import load_settings, _save_settings settings = load_settings() is_first_open = settings.first_opened_at is None @@ -132,8 +137,6 @@ analytics = SubApp("analytics", analytics_lifespan) @analytics.router.get("/usage-summary") async def usage_summary(): """Compute usage stats from persisted sessions for the Settings page.""" - from backend.apps.agents.agent_manager import agent_manager - from backend.apps.nine_router import get_usage_stats, is_running as _9r_running sessions = load_all_sessions() for s in agent_manager.get_all_sessions(): diff --git a/backend/apps/analytics/collector.py b/backend/apps/analytics/collector.py index 88b54eb4..6f45e938 100644 --- a/backend/apps/analytics/collector.py +++ b/backend/apps/analytics/collector.py @@ -12,6 +12,8 @@ import platform from uuid import uuid4 from posthog import Posthog +from backend.apps.settings.settings import load_settings, _save_settings + logger = logging.getLogger(__name__) @@ -50,7 +52,6 @@ def _get_installation_id() -> str: if _installation_id: return _installation_id try: - from backend.apps.settings.settings import load_settings, _save_settings settings = load_settings() iid = getattr(settings, "installation_id", None) if not iid: diff --git a/backend/main.py b/backend/main.py index b704222a..cc89d9cc 100644 --- a/backend/main.py +++ b/backend/main.py @@ -8,7 +8,7 @@ logger = logging.getLogger(__name__) from backend.config.Apps import MainApp from backend.apps.health.health import health from backend.apps.agents.agents import agents -from backend.apps.agents.ws_manager import ws_manager +from backend.apps.agents.manager.ws_manager import ws_manager from backend.apps.agents.ws_routes import handle_session_message, handle_dashboard_message from backend.apps.skills.skills import skills from backend.apps.tools_lib.tools_lib import tools_lib diff --git a/frontend/package.json b/frontend/package.json index 2f297583..ee1b531b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,12 +12,11 @@ "knip": "knip" }, "dependencies": { - "@assistant-ui/core": "^0.1.9", - "@assistant-ui/core": "^0.1.9", "@assistant-ui/core": "^0.1.9", "@assistant-ui/react": "^0.12.21", "@assistant-ui/react-lexical": "^0.0.3", "@assistant-ui/react-markdown": "^0.12.7", + "@codemirror/commands": "^6.10.3", "@codemirror/lang-html": "^6.4.11", "@codemirror/lang-json": "^6.0.2", "@codemirror/lang-python": "^6.2.1", diff --git a/linter/config/config.json b/linter/config/config.json index e47b933c..326e84ce 100644 --- a/linter/config/config.json +++ b/linter/config/config.json @@ -1,13 +1,13 @@ { "enabled": { "max-file-lines": false, - "max-folder-items": false, - "no-nested-imports": false, - "vulture": false, + "max-folder-items": true, + "no-nested-imports": true, + "vulture": true, "eslint": false, "knip": true, - "endpoints": false, - "classes": false + "endpoints": true, + "classes": true }, "rules": { "max-file-lines": 250,