From d6d6bc2271b1859f2e191a5bfd2873165047da2d Mon Sep 17 00:00:00 2001 From: haikdc Date: Sat, 18 Apr 2026 07:43:31 -0700 Subject: [PATCH] [hAIk]: added in debug statements instead of printing/logging --- .cursor/skills/swarm-debug/SKILL.md | 176 ++++++++++++++++++ .../COMMS_MANAGER/classes/FutureBridge.py | 3 +- backend/apps/agents/agents.py | 3 +- backend/apps/dashboards/dashboards.py | 5 +- .../RegistryRefreshLoop.py | 3 +- .../fetch_all_registry_skills.py | 7 +- .../utils/fetch_one_skill.py | 3 +- backend/apps/skills/skills.py | 3 - .../NineRouterClient/NineRouterClient.py | 9 +- .../helpers/forward_output.py | 1 + backend/apps/subscriptions/subscriptions.py | 5 +- .../apps/tools/OAuthService/OAuthService.py | 12 +- .../tools/discover_tools/discover_tools.py | 3 +- .../helpers/build_stdio_tool.py | 3 +- .../tool_definition_to_mcp_tool.py | 3 +- backend/apps/tools/tools.py | 10 +- backend/config/Apps.py | 4 +- backend/core/Agent/Agent.py | 6 +- .../Agent/run_agent_loop/run_agent_loop.py | 6 +- backend/run.sh | 4 +- 20 files changed, 223 insertions(+), 46 deletions(-) create mode 100644 .cursor/skills/swarm-debug/SKILL.md diff --git a/.cursor/skills/swarm-debug/SKILL.md b/.cursor/skills/swarm-debug/SKILL.md new file mode 100644 index 00000000..223a8280 --- /dev/null +++ b/.cursor/skills/swarm-debug/SKILL.md @@ -0,0 +1,176 @@ +--- +name: swarm-debug +description: >- + Instrument Python code with toggleable debug output using swarm-debug. + Use when adding debug statements, print statemens, or logging. Use when toggling debug visibility, managing debug + statements, or working with the swarm_debug module. NOTE: you should never use print or logging statements, only use debug. +--- + +# swarm-debug + +A non-invasive debug logger for Python. You add `debug()` calls to code; visibility is controlled per-file via CLI or GUI without modifying source. + +## CRITICAL: Always use the CLI, never read raw files + +- **NEVER** read `~/.swarm-debug/projects//debug_toggles.json` directly with `cat`, `head`, `tail`, `read`, or any file tool. The raw JSON is an internal per-project cache and may be stale or inconsistent with the actual codebase. +- **ALWAYS** use `swarm-debug status` or `swarm-debug status --json` to inspect state. The CLI rescans for `debug(` calls and returns the true resolved state. +- **NEVER** write to `debug_toggles.json` directly (stored per-project under `~/.swarm-debug/projects//`). Use `swarm-debug toggle`, `swarm-debug set-color`, `swarm-debug set-emoji`, etc. + +## CRITICAL: Locate and activate the correct Python environment first + +Before running **any** `swarm-debug` command, you must find the environment that has the `swarm-debug` package installed: + +1. **Check `.vscode/settings.json`** in the project root for a `python.defaultInterpreterPath`. If it points to a venv (e.g. `${workspaceFolder}/backend/.venv/bin/python`), activate that venv first: + ```bash + source /bin/activate + ``` +2. **If no `.vscode/settings.json` exists** (or it has no interpreter path), search the project for a `.venv` directory that contains the `swarm-debug` package: + ```bash + find . -path '*/.venv/bin/swarm-debug' -print -quit + ``` + If found, activate that venv. +3. **If neither exists**, ask the user where the `swarm-debug` package is installed. Do **NOT** fall back to reading the raw JSON file or guessing a system Python path. + +## Adding debug statements + +```python +from swarm_debug import debug + +debug(my_var) # prints: [func_name] : my_var: int = 42 +debug("checkpoint") # prints: [func_name] : checkpoint (italic) +debug(err) # errors auto-force ON with red output +debug("x=%s y=%s", x, y) # %-style formatting +``` + +`debug()` inspects the call stack to extract the caller's file, function, variable names, and indentation. No format strings or manual labels needed -- pass variables directly. + +### Full signature + +```python +debug(*args, mode='debug', override_max_chars=False, sep=, end='\n', + pretty=True, lang=None, table=) +``` + +| Kwarg | Type | Default | Description | +|---|---|---|---| +| `mode` | `str` | `"debug"` | Log level. `"all"` (always), `"debug"` (default), `"test"` (high priority) | +| `override_max_chars` | `bool` | `False` | Bypass the 3000-char truncation limit | +| `sep` | `str` | auto | Join all args with this separator (like `print(sep=...)`) | +| `pretty` | `bool` | `True` | Pretty-print dicts, lists, sets, tuples, dataclasses with Rich | +| `lang` | `str\|None` | `None` | Syntax-highlight all args as this language (e.g. `"sql"`, `"json"`, `"html"`) | +| `table` | `bool` | auto | Force table layout on/off. Auto-on when >1 non-text data args | + +### Rich output features + +All output is rendered with Rich. Function names in the output are clickable file links (in terminals that support OSC 8 hyperlinks like iTerm2 and Windows Terminal). Type annotations are shown in dim text for non-string values. + +**Pretty-printed data structures** (on by default): +```python +debug(my_dict) # dicts, lists, sets, dataclasses are pretty-printed with Rich +debug(my_dict, pretty=False) # opt out for flat single-line output +``` + +**Syntax-highlighted strings** (explicit lang= kwarg): +```python +debug(sql_query, lang="sql") # SQL keyword highlighting +debug(json_string, lang="json") # JSON syntax coloring +debug(html_body, lang="html") # HTML highlighting +``` + +**Table layout** (auto: on when >1 non-text data args, off otherwise): +```python +debug(x, y, z) # 3 data args -> table with Name | Type | Value columns +debug(x) # single arg -> inline output (no table) +debug("msg", x) # 1 text + 1 data arg -> inline (only 1 data arg) +debug("msg", x, y) # 1 text + 2 data args -> table +debug(x, y, z, table=False) # force per-line output +debug(x, table=True) # force table even for a single arg +``` + +**Diff output** -- compare two values with a unified diff: +```python +debug.diff(old_state, new_state) # default label "diff" +debug.diff(old_state, new_state, label="state") # custom label +``` + +**Timing** -- measure how long a block takes: +```python +with debug.time("database query"): + result = db.execute(query) +# prints: [func] : ⏱ database query took 0.123s (green/yellow/red based on duration) +``` + +**Truncation**: values over 3000 chars are truncated (first 1500 + `...` + last 1500). Pass `override_max_chars=True` to disable. + +**Indent group markers**: when indentation level changes between `debug()` calls, a visual rule line is emitted to mark the group boundary. + +## CLI reference + +All commands work standalone (no server required). Paths are relative to project root. + +```bash +# View current state +swarm-debug status # human-readable tree with [ON]/[OFF] tags +swarm-debug status --json # machine-readable JSON (pipe to jq, python, etc.) +swarm-debug stats # flat table of all files with path/status/color/emoji + +# Toggle visibility +swarm-debug toggle on src/agents/planner.py # single file +swarm-debug toggle off src/agents/ # whole directory (recursive) +swarm-debug toggle on --all # everything + +# Configuration +swarm-debug set-root /path/to/project +swarm-debug set-color src/agents/planner.py "#ff0000" # single file +swarm-debug set-color src/agents/ "#ff0000" # directory (propagates lightened color to children) +swarm-debug set-emoji src/agents/planner.py "🔴" # single file +swarm-debug set-emoji src/agents/ "🔴" # directory (propagates emoji to children) +swarm-debug reset # reset all colors/emojis (with confirmation) + +# GUI +swarm-debug gui # launches web UI at localhost:6969 +swarm-debug gui --port 8080 # custom port +swarm-debug gui --verbose # show all server logs in the terminal + +# Cursor skill management +swarm-debug install-cursor-skill # copy SKILL.md to .cursor/skills/swarm-debug/ +swarm-debug uninstall-cursor-skill # remove the skill directory + +# Package management +swarm-debug --version # show version (also checks for updates and skill staleness) +swarm-debug --upgrade # upgrade to latest version from PyPI +swarm-debug --help-all # detailed help for all commands + API-only endpoints +``` + +## Typical workflow + +1. **Instrument**: Add `debug()` calls to files you want to observe. +2. **Set root**: `swarm-debug set-root /path/to/project` (only needed once; persisted in `~/.swarm-debug/projects//root_dir.txt`). +3. **Toggle on** the files you care about: `swarm-debug toggle on src/core/engine.py`. +4. **Run** the program -- only toggled-on files produce debug output. +5. **Toggle off** when done: `swarm-debug toggle off src/core/engine.py`. + +## How it works + +- Internal state is cached per-project in `~/.swarm-debug/projects//debug_toggles.json` (where `` is derived from the project root path), but this file should never be read or written directly. Always use the CLI commands to inspect or modify state. +- The CLI and GUI both read/write through the same underlying store. +- After any CLI/GUI change, a `needs_resync.txt` flag is set. The next `debug()` call in the running program reloads the config automatically -- no restart needed. +- Only `.py` files that contain `debug(` calls appear in the tree. + +## Key behaviors + +- **Directory changes propagate**: toggling, setting color, or setting emoji on a directory propagates to all children recursively. +- **Manual-override flags**: each concern has its own flag (`set_manually` for toggles, `set_manually_color` for color, `set_manually_emoji` for emoji). When you explicitly set a file's value, its flag is set so parent propagation won't override it. +- **Errors bypass toggles**: if a `debug()` argument is an Exception or contains "error", it always prints (red, with a cross emoji), regardless of toggle state. +- **Indentation preserved**: `debug()` reads source indentation and renders nested output with visual indent bars. + +## Reading status programmatically + +```bash +# Get JSON and extract toggled-on files with jq +swarm-debug status --json | jq '.. | objects | select(.is_toggled == true and (.children | not)) | .name' +``` + +## Environment variables + +- `SWARM_DEBUG_ROOT` -- overrides the project root (highest priority, above persisted file and cwd). diff --git a/backend/apps/agents/COMMS_MANAGER/classes/FutureBridge.py b/backend/apps/agents/COMMS_MANAGER/classes/FutureBridge.py index c36c81cc..217ac048 100644 --- a/backend/apps/agents/COMMS_MANAGER/classes/FutureBridge.py +++ b/backend/apps/agents/COMMS_MANAGER/classes/FutureBridge.py @@ -1,6 +1,7 @@ import asyncio from typing import Callable, Awaitable, Dict from pydantic import BaseModel, Field, InstanceOf +from swarm_debug import debug from typeguard import typechecked class FutureBridge(BaseModel): @@ -26,7 +27,7 @@ class FutureBridge(BaseModel): try: return await asyncio.wait_for(future, timeout=timeout) except asyncio.TimeoutError: - print(f"[FutureBridge.request] Request {request_id} timed out after {timeout}s") + debug(f"[FutureBridge.request] Request {request_id} timed out after {timeout}s") return {"error": "Timed out"} finally: self.p_pending.pop(request_id, None) diff --git a/backend/apps/agents/agents.py b/backend/apps/agents/agents.py index 79a4fa38..cd9088a1 100644 --- a/backend/apps/agents/agents.py +++ b/backend/apps/agents/agents.py @@ -31,6 +31,7 @@ from backend.apps.agents.COMMS_MANAGER.COMMS_MANAGER import COMMS_MANAGER from backend.apps.settings.settings import load_settings from backend.core.llm.resolve_sdk_env import resolve_sdk_env from backend.ports import NINE_ROUTER_PORT +from swarm_debug import debug from claude_agent_sdk import ClaudeAgentOptions from claude_agent_sdk.types import HookMatcher, McpServerConfig from backend.core.tools.shared_structs.Toolkit import Toolkit @@ -70,7 +71,7 @@ async def agents_lifespan(): ) SESSIONS[stored.session_id] = stored except Exception as e: - print(f"[agents lifespan] Skipping corrupt session {stored.session_id}: {e}") + debug(f"[agents lifespan] Skipping corrupt session {stored.session_id}: {e}") yield for agent in list[Agent](SESSIONS.values()): await agent.stop_agent() diff --git a/backend/apps/dashboards/dashboards.py b/backend/apps/dashboards/dashboards.py index 0e2ffae2..adf6e6e9 100644 --- a/backend/apps/dashboards/dashboards.py +++ b/backend/apps/dashboards/dashboards.py @@ -13,6 +13,7 @@ from backend.apps.agents.agents import get_all_sessions, delete_session from backend.apps.settings.settings import load_settings from backend.apps.dashboards.generate_dashboard_name import generate_dashboard_name from backend.ports import NINE_ROUTER_PORT +from swarm_debug import debug from typing import Optional from backend.config.paths import DB_ROOT @@ -93,7 +94,7 @@ async def generate_name(dashboard_id: str): if generated: fallback = generated except Exception as e: - print(f"[dashboards.generate_name] ERROR: Dashboard name generation failed, using fallback: {e}") + debug(f"[dashboards.generate_name] ERROR: Dashboard name generation failed, using fallback: {e}") dashboard.name = fallback dashboard.auto_named = True @@ -139,7 +140,7 @@ async def delete_dashboard(dashboard_id: str): try: await delete_session(session["session_id"]) except Exception: - print(f"[dashboards.delete_dashboard] ERROR: Failed to delete session {session.get('session_id')} during dashboard deletion") + debug(f"[dashboards.delete_dashboard] ERROR: Failed to delete session {session.get('session_id')} during dashboard deletion") DASHBOARD_STORE.delete(dashboard_id) return {"ok": True} diff --git a/backend/apps/skills/RegistryRefreshLoop/RegistryRefreshLoop.py b/backend/apps/skills/RegistryRefreshLoop/RegistryRefreshLoop.py index 74ebd5b1..922185f5 100644 --- a/backend/apps/skills/RegistryRefreshLoop/RegistryRefreshLoop.py +++ b/backend/apps/skills/RegistryRefreshLoop/RegistryRefreshLoop.py @@ -2,6 +2,7 @@ import asyncio import time from typing import Optional +from swarm_debug import debug from typeguard import typechecked from pydantic import BaseModel, Field, InstanceOf @@ -45,5 +46,5 @@ class RegistryRefreshLoop(BaseModel): ) self.updated_at = time.time() except Exception as e: - print(f"[RegistryRefreshLoop] Skill registry refresh error: {e}") + debug(f"[RegistryRefreshLoop] Skill registry refresh error: {e}") await asyncio.sleep(self.refresh_interval_s) \ No newline at end of file diff --git a/backend/apps/skills/RegistryRefreshLoop/fetch_all_registry_skills/fetch_all_registry_skills.py b/backend/apps/skills/RegistryRefreshLoop/fetch_all_registry_skills/fetch_all_registry_skills.py index 03722c12..91c351a2 100644 --- a/backend/apps/skills/RegistryRefreshLoop/fetch_all_registry_skills/fetch_all_registry_skills.py +++ b/backend/apps/skills/RegistryRefreshLoop/fetch_all_registry_skills/fetch_all_registry_skills.py @@ -4,6 +4,7 @@ import asyncio import httpx +from swarm_debug import debug from typeguard import typechecked from backend.apps.skills.RegistryRefreshLoop.fetch_all_registry_skills.utils.fetch_skill_paths import fetch_skill_paths from backend.apps.skills.RegistryRefreshLoop.fetch_all_registry_skills.utils.fetch_one_skill import fetch_one_skill @@ -25,9 +26,9 @@ async def fetch_all_registry_skills( manifest_url=f"{github_base_url}/{github_repo}/{github_branch}{manifest_extension}" ) except Exception as e: - print(f"[fetch_all_registry_skills] Skill registry manifest fetch failed: {e}") + debug(f"[fetch_all_registry_skills] Skill registry manifest fetch failed: {e}") return result - print(f"[fetch_all_registry_skills] Skill registry: found {len(paths)} skills in manifest, fetching...") + debug(f"[fetch_all_registry_skills] Skill registry: found {len(paths)} skills in manifest, fetching...") sem = asyncio.Semaphore(num_concurrent_fetches) records = await asyncio.gather( *[fetch_one_skill( @@ -43,5 +44,5 @@ async def fetch_all_registry_skills( for rec in records: if rec: result[rec["name"]] = rec - print(f"[fetch_all_registry_skills] Skill registry cache refreshed: {len(result)} skills") + debug(f"[fetch_all_registry_skills] Skill registry cache refreshed: {len(result)} skills") return result diff --git a/backend/apps/skills/RegistryRefreshLoop/fetch_all_registry_skills/utils/fetch_one_skill.py b/backend/apps/skills/RegistryRefreshLoop/fetch_all_registry_skills/utils/fetch_one_skill.py index 1043e2d2..e72f2247 100644 --- a/backend/apps/skills/RegistryRefreshLoop/fetch_all_registry_skills/utils/fetch_one_skill.py +++ b/backend/apps/skills/RegistryRefreshLoop/fetch_all_registry_skills/utils/fetch_one_skill.py @@ -2,6 +2,7 @@ import asyncio from typing import Optional import httpx from backend.apps.skills.parse_frontmatter import parse_frontmatter +from swarm_debug import debug from typeguard import typechecked from pydantic import InstanceOf @@ -22,7 +23,7 @@ async def fetch_one_skill( return None raw = resp.text except Exception as exc: - print(f"[fetch_one_skill] Failed to fetch {folder}/SKILL.md: {exc}") + debug(f"[fetch_one_skill] Failed to fetch {folder}/SKILL.md: {exc}") return None meta, body = parse_frontmatter(raw) diff --git a/backend/apps/skills/skills.py b/backend/apps/skills/skills.py index fef7271f..a2af8b2f 100644 --- a/backend/apps/skills/skills.py +++ b/backend/apps/skills/skills.py @@ -1,7 +1,6 @@ """Skills SubApp — local skill CRUD, workspace management, and remote registry.""" import json -import logging import os from contextlib import asynccontextmanager from typing import Any, Optional @@ -15,8 +14,6 @@ from backend.apps.skills.SkillStore.SkillStore import SkillStore from backend.apps.skills.parse_frontmatter import parse_frontmatter from backend.apps.skills.RegistryRefreshLoop.RegistryRefreshLoop import RegistryRefreshLoop -logger = logging.getLogger(__name__) - # --------------------------------------------------------------------------- # Paths & singletons # --------------------------------------------------------------------------- diff --git a/backend/apps/subscriptions/NineRouter/helpers/NineRouterClient/NineRouterClient.py b/backend/apps/subscriptions/NineRouter/helpers/NineRouterClient/NineRouterClient.py index 0bd9a582..c2f23bb2 100644 --- a/backend/apps/subscriptions/NineRouter/helpers/NineRouterClient/NineRouterClient.py +++ b/backend/apps/subscriptions/NineRouter/helpers/NineRouterClient/NineRouterClient.py @@ -2,6 +2,7 @@ import httpx from pydantic import Field, BaseModel, InstanceOf +from swarm_debug import debug from typeguard import typechecked from backend.apps.subscriptions.NineRouter.helpers.constants import NINE_ROUTER_API, NINE_ROUTER_V1 @@ -21,7 +22,7 @@ class NineRouterClient(BaseModel): if r.status_code == 200: return r.json() except Exception as e: - print(f"9Router providers fetch failed: {e}") + debug(f"9Router providers fetch failed: {e}") return [] @typechecked @@ -94,9 +95,9 @@ class NineRouterClient(BaseModel): "codeVerifier": code_verifier, "state": state, } - print(f"exchange_oauth: provider={provider} redirect_uri={redirect_uri}") + debug(f"exchange_oauth: provider={provider} redirect_uri={redirect_uri}") r = await self.p_http.post(f"{NINE_ROUTER_API}/oauth/{provider}/exchange", json=payload) - print(f"exchange_oauth: status={r.status_code}") + debug(f"exchange_oauth: status={r.status_code}") r.raise_for_status() return r.json() @@ -117,7 +118,7 @@ class NineRouterClient(BaseModel): for m in models ] except Exception as e: - print(f"9Router models fetch failed: {e}") + debug(f"9Router models fetch failed: {e}") return [] @typechecked diff --git a/backend/apps/subscriptions/NineRouter/helpers/NineRouterProcess/helpers/forward_output.py b/backend/apps/subscriptions/NineRouter/helpers/NineRouterProcess/helpers/forward_output.py index ac0a3986..608b2c68 100644 --- a/backend/apps/subscriptions/NineRouter/helpers/NineRouterProcess/helpers/forward_output.py +++ b/backend/apps/subscriptions/NineRouter/helpers/NineRouterProcess/helpers/forward_output.py @@ -1,5 +1,6 @@ import re +from swarm_debug import debug from typeguard import typechecked _ANSI_RE = re.compile(r"\x1b\[[0-9;]*[A-Za-z]|\x1b[c78]") diff --git a/backend/apps/subscriptions/subscriptions.py b/backend/apps/subscriptions/subscriptions.py index 4babf892..2437f60a 100644 --- a/backend/apps/subscriptions/subscriptions.py +++ b/backend/apps/subscriptions/subscriptions.py @@ -14,6 +14,7 @@ from fastapi.responses import HTMLResponse, JSONResponse from backend.config.Apps import SubApp from backend.apps.subscriptions.NineRouter.NineRouter import NineRouter from backend.apps.subscriptions.html_constants import SUCCESS_HTML, ERROR_STYLE +from swarm_debug import debug P_PENDING_OAUTH: Dict[str, dict] = {} @@ -27,7 +28,7 @@ async def subscriptions_lifespan(): try: await router.ensure_running() except Exception as e: - print(f"9Router auto-start failed: {e}") + debug(f"9Router auto-start failed: {e}") yield try: await router.stop() @@ -156,7 +157,7 @@ async def subscriptions_callback(request: Request): pending["redirect_uri"], pending["code_verifier"], state, ) except Exception as e: - print(f"OAuth callback: exchange failed for provider={pending['provider']}: {e}") + debug(f"OAuth callback: exchange failed for provider={pending['provider']}: {e}") return HTMLResponse( f'
' f'

Connection failed

{e}

' diff --git a/backend/apps/tools/OAuthService/OAuthService.py b/backend/apps/tools/OAuthService/OAuthService.py index f132add3..188359aa 100644 --- a/backend/apps/tools/OAuthService/OAuthService.py +++ b/backend/apps/tools/OAuthService/OAuthService.py @@ -5,7 +5,6 @@ Pure business logic with no HTTP/FastAPI dependencies. import base64 import hashlib -import logging import os import secrets import time @@ -19,11 +18,10 @@ from backend.apps.tools.OAuthService.OAUTH_PROVIDERS.OAuthProvider import OAuthP from backend.apps.tools.OAuthService.OAUTH_PROVIDERS.OAUTH_PROVIDERS import OAUTH_PROVIDERS from backend.apps.tools.shared_utils.ToolDefinition import ToolDefinition from backend.core.db.PydanticStore import PydanticStore +from swarm_debug import debug from typeguard import typechecked from backend.ports import BACKEND_DEV_PORT -logger = logging.getLogger(__name__) - class OAuthService(BaseModel): store: PydanticStore[ToolDefinition] @@ -130,7 +128,7 @@ class OAuthService(BaseModel): resp = await client.post(provider.token_url, data=token_data, headers=headers) if resp.status_code != 200: - logger.warning("OAuth token exchange failed: %s", resp.text) + debug(f"OAuth token exchange failed: {resp.text}") raise RuntimeError(resp.text) tokens: Dict[str, Any] = resp.json() @@ -190,7 +188,7 @@ class OAuthService(BaseModel): headers={"Content-Type": "application/x-www-form-urlencoded"}, ) except Exception as e: - logger.warning("Failed to revoke token for tool %s: %s", tool.id, e) + debug(f"Failed to revoke token for tool {tool.id}: {e}") tool.oauth_tokens = {} tool.auth_status = "configured" @@ -241,7 +239,7 @@ class OAuthService(BaseModel): self.store.save(tool) return new_token except Exception as e: - logger.warning("OAuth token refresh failed for tool %s: %s", tool.id, e) + debug(f"OAuth token refresh failed for tool {tool.id}: {e}") return None @typechecked @@ -254,5 +252,5 @@ class OAuthService(BaseModel): if resp.status_code == 200: return resp.json().get(field) except Exception as e: - logger.warning("Failed to fetch userinfo%s: %s", f" for {label}" if label else "", e) + debug(f"Failed to fetch userinfo{f' for {label}' if label else ''}: {e}") return None \ No newline at end of file diff --git a/backend/apps/tools/discover_tools/discover_tools.py b/backend/apps/tools/discover_tools/discover_tools.py index 5758be02..95f8e212 100644 --- a/backend/apps/tools/discover_tools/discover_tools.py +++ b/backend/apps/tools/discover_tools/discover_tools.py @@ -1,4 +1,5 @@ from typing import Any +from swarm_debug import debug from backend.apps.tools.discover_tools.DiscoveryError import DiscoveryError, DiscoveryConfigError from backend.apps.tools.discover_tools.utils.discover_mcp_tools_stdio import discover_mcp_tools_stdio from backend.apps.tools.discover_tools.utils.discover_mcp_tools_http import discover_mcp_tools_http @@ -36,7 +37,7 @@ async def discover_tools(config: dict[str, Any], tool_name: str = "") -> list[di try: return await discover_mcp_tools_http(url, config.get("headers")) except DiscoveryError: - print(f"[discover_tools] Streamable HTTP failed for {tool_name}, retrying with SSE") + debug(f"[discover_tools] Streamable HTTP failed for {tool_name}, retrying with SSE") return await discover_mcp_tools_sse(url, config.get("headers")) raise DiscoveryConfigError(f"Unsupported MCP transport type: '{transport}'") diff --git a/backend/apps/tools/tool_definition_to_mcp_tool/helpers/build_stdio_tool.py b/backend/apps/tools/tool_definition_to_mcp_tool/helpers/build_stdio_tool.py index 888c1b76..d0d181da 100644 --- a/backend/apps/tools/tool_definition_to_mcp_tool/helpers/build_stdio_tool.py +++ b/backend/apps/tools/tool_definition_to_mcp_tool/helpers/build_stdio_tool.py @@ -5,6 +5,7 @@ from backend.core.tools.shared_structs.MCP_Tool import STDIO_MCP_Tool from backend.apps.tools.shared_utils.mcp_config import resolve_command, augmented_path # TODO: either remove the import, or make them non private vars from backend.config.paths import P_BACKEND_DIR, p_is_packaged +from swarm_debug import debug from typeguard import typechecked @@ -21,7 +22,7 @@ def build_stdio_tool( if resolved: command = resolved else: - print(f"[build_stdio_tool] Command '{command}' not found on PATH or bundled directories") + debug(f"[build_stdio_tool] Command '{command}' not found on PATH or bundled directories") env = config.get("env", {}) env.setdefault("PATH", augmented_path()) diff --git a/backend/apps/tools/tool_definition_to_mcp_tool/tool_definition_to_mcp_tool.py b/backend/apps/tools/tool_definition_to_mcp_tool/tool_definition_to_mcp_tool.py index 0776e809..d6ce3b63 100644 --- a/backend/apps/tools/tool_definition_to_mcp_tool/tool_definition_to_mcp_tool.py +++ b/backend/apps/tools/tool_definition_to_mcp_tool/tool_definition_to_mcp_tool.py @@ -12,6 +12,7 @@ from backend.core.tools.shared_structs.MCP_Tool import MCP_Tool from backend.apps.tools.tool_definition_to_mcp_tool.helpers.inject_credentials import inject_credentials from backend.apps.tools.tool_definition_to_mcp_tool.helpers.build_stdio_tool import build_stdio_tool from backend.apps.tools.tool_definition_to_mcp_tool.helpers.build_http_sse_tool import build_http_sse_tool +from swarm_debug import debug from typeguard import typechecked import re @@ -48,5 +49,5 @@ def tool_definition_to_mcp_tool( elif transport in ("http", "sse"): return build_http_sse_tool(tool_def, config, server_name, transport) - print(f"[tool_definition_to_mcp_tool] Unsupported MCP transport type '{transport}' for tool {tool_def.name}") + debug(f"[tool_definition_to_mcp_tool] Unsupported MCP transport type '{transport}' for tool {tool_def.name}") return None diff --git a/backend/apps/tools/tools.py b/backend/apps/tools/tools.py index fe118d33..acea3779 100644 --- a/backend/apps/tools/tools.py +++ b/backend/apps/tools/tools.py @@ -4,7 +4,6 @@ from typing_extensions import List from claude_agent_sdk.types import McpServerConfig from pydantic import BaseModel, Field import json -import logging import os import time from contextlib import asynccontextmanager @@ -27,10 +26,9 @@ from backend.core.tools.shared_structs.TOOL_PERMISSIONS import TOOL_PERMISSIONS from backend.core.tools.shared_structs.Toolkit import Toolkit from backend.core.tools.shared_structs.Tool import Tool from backend.core.tools.shared_structs.MCP_Tool import MCP_Tool +from swarm_debug import debug from typeguard import typechecked -logger = logging.getLogger(__name__) - TOOLS_DIR = os.path.join(DB_ROOT, "tools") BUILTIN_PERMS_PATH = os.path.join(TOOLS_DIR, "builtin_permissions.json") @@ -77,12 +75,12 @@ def save_builtin_permissions(perms: dict[str, str]) -> None: json.dump(perms, f, indent=2) -@tools.router.get("/builtin/permissions") +@tools.router.get("/get_builtin_permissions") async def get_builtin_permissions() -> dict: return {"permissions": load_builtin_permissions()} -@tools.router.put("/builtin/permissions") +@tools.router.put("/update_builtin_permissions") async def update_builtin_permissions(body: dict) -> dict: valid_names = {t["name"] for t in BUILTIN_TOOLS} valid_policies = {"allow", "ask", "deny"} @@ -200,7 +198,7 @@ async def discover(tool_id: str) -> dict: raise HTTPException(status_code=502, detail=str(e)) except Exception as e: msg = str(e).strip() or type(e).__name__ - logger.warning(f"MCP tool discovery failed for {tool.name}: {msg}", exc_info=True) + debug(f"MCP tool discovery failed for {tool.name}: {msg}") raise HTTPException(status_code=502, detail=f"Discovery failed: {msg}") permissions: dict[str, Any] = {} diff --git a/backend/config/Apps.py b/backend/config/Apps.py index 9b8fece2..b21d9d43 100644 --- a/backend/config/Apps.py +++ b/backend/config/Apps.py @@ -2,7 +2,7 @@ import os from backend.ports import BACKEND_DEV_PORT from fastapi import FastAPI, APIRouter -# import debug +from swarm_debug import debug from uuid import uuid4 from typing import List from contextlib import asynccontextmanager @@ -34,7 +34,7 @@ class MainApp: # debug(sub_app.name) await stack.enter_async_context(sub_app.lifespan()) _port = os.environ.get("OPENSWARM_PORT", str(BACKEND_DEV_PORT)) - print(f"\nCheck out the API docs at: http://127.0.0.1:{_port}/docs\n") + debug(f"\nCheck out the API docs at: http://127.0.0.1:{_port}/docs\n") yield self.app = FastAPI(lifespan=lifespan) diff --git a/backend/core/Agent/Agent.py b/backend/core/Agent/Agent.py index d4f0373c..4f60a7a4 100644 --- a/backend/core/Agent/Agent.py +++ b/backend/core/Agent/Agent.py @@ -1,5 +1,4 @@ import asyncio -import logging import os from copy import deepcopy from typing import Any, Dict, List, Literal, Optional @@ -18,11 +17,10 @@ from backend.core.events.events import ( ApprovalRequestEvent, EventCallback, AnyEvent, ) from backend.core.tools.shared_structs.Toolkit import Toolkit +from swarm_debug import debug os.environ.setdefault("CLAUDE_CODE_STREAM_CLOSE_TIMEOUT", "3600000") -logger = logging.getLogger(__name__) - class Agent(BaseModel): model: str @@ -125,7 +123,7 @@ class Agent(BaseModel): async def send_message(self, msg: Message) -> None: async with self.lock: if self.task is not None and not self.task.done(): - logger.warning("[Agent.send_message] Agent %s is already running", self.session_id) + debug(f"[Agent.send_message] Agent {self.session_id} is already running") return await self.emit(AgentMessageEvent( diff --git a/backend/core/Agent/run_agent_loop/run_agent_loop.py b/backend/core/Agent/run_agent_loop/run_agent_loop.py index 11ff8bb8..6355257b 100644 --- a/backend/core/Agent/run_agent_loop/run_agent_loop.py +++ b/backend/core/Agent/run_agent_loop/run_agent_loop.py @@ -1,6 +1,6 @@ import asyncio -import logging +from swarm_debug import debug from typeguard import typechecked from claude_agent_sdk import ( @@ -18,8 +18,6 @@ from backend.core.events.events import ( ) from typing import List, Dict, Optional -logger = logging.getLogger(__name__) - @typechecked async def run_agent_loop( @@ -79,7 +77,7 @@ async def run_agent_loop( raise except Exception as e: - logger.exception("Agent %s error: %s", session_id, e) + debug(f"Agent {session_id} error: {e}") error_msg = SystemMessage( content=f"Error: {e}", branch_id=branch_id, diff --git a/backend/run.sh b/backend/run.sh index 53015705..bab8e2ac 100755 --- a/backend/run.sh +++ b/backend/run.sh @@ -38,7 +38,7 @@ BACKEND_PORT=$(python3 -c "import json; print(json.load(open('$PROJECT_ROOT_ABSP # --- Start the backend server --- echo "Starting backend server on http://0.0.0.0:${BACKEND_PORT} ..." cd "$PROJECT_ROOT_ABSPATH" -"$UV_BIN" run --project "$BACKEND_DIR_ABSPATH" python -m uvicorn backend.main:app \ +WATCHFILES_FORCE_POLLING=true "$UV_BIN" run --project "$BACKEND_DIR_ABSPATH" python -m uvicorn backend.main:app \ --host 0.0.0.0 --port "$BACKEND_PORT" --reload \ --reload-dir "$BACKEND_DIR_ABSPATH" \ - --reload-exclude '*.pyc' + --reload-exclude '*.pyc' \ No newline at end of file