[eric] debugger: swap vendored debugger/ for PyPI swarm-debug in app workspaces

This commit is contained in:
ciregenz
2026-07-02 00:16:53 -07:00
parent fb0044f156
commit 77ff2b79e9
15 changed files with 82 additions and 100 deletions
-3
View File
@@ -2,14 +2,11 @@ from backend.config.Apps import SubApp
from contextlib import asynccontextmanager
from fastapi.responses import PlainTextResponse
from typeguard import typechecked
import debug
from fastapi import status, HTTPException
@asynccontextmanager
async def health_lifespan():
debug("START")
yield
debug("END")
health = SubApp("health", health_lifespan)
+1 -1
View File
@@ -211,7 +211,7 @@ class AppRuntime:
self.port = None
env = self.p_spawn_env_base()
# bash run.sh reads .env itself; we don't need to set FRONTEND_PORT / BACKEND_PORT here. We DO export the install paths so the template's `backend/run.sh` can find our debugger to satisfy its `from swarm_debug import debug`. (Also written into .env at seed time, but env-var path is the more reliable read site for subshells.) NOTE: keep these in sync with seed_webapp_template_workspace.
# bash run.sh reads .env itself; we don't need to set FRONTEND_PORT / BACKEND_PORT here. We DO export the install paths as env vars (the more reliable read site for subshells). OPENSWARM_DEBUGGER_PATH is legacy-only: workspaces seeded before the PyPI swap have a run.sh that editable-installs the bundled debugger from it; new templates resolve `swarm-debug` from PyPI via pyproject.
from backend.apps.outputs.view_builder_templates import (
DEBUGGER_PATH,
TEMPLATE_BACKEND_PATH,
+47 -11
View File
@@ -1,9 +1,9 @@
# swarm-debug — OpenSwarm's logger for App backends
`swarm_debug` (also importable as `debug` for legacy reasons) is OpenSwarm's
opinionated `print()` replacement for the App Builder's backend code. It
prints colored, indented, frame-aware log lines that read at a glance and
land in the App Builder's **Terminal** tab under the `[BACKEND]` prefix.
`swarm_debug` (the `swarm-debug` package on PyPI) is OpenSwarm's opinionated
`print()` replacement for the App Builder's backend code. It prints colored,
indented, frame-aware log lines that read at a glance and land in the App
Builder's **Terminal** tab under the `[BACKEND]` prefix.
It's pre-installed in every App Builder workspace that has a backend (i.e.
after `bash backend_init.sh`). Use it instead of `print()`.
@@ -100,13 +100,46 @@ debug(huge_payload, override_max_chars=True)
## Modes (custom log levels)
`debug` accepts a `mode` kwarg that maps to a configurable log channel.
Default is `'debug'`. The Terminal pane shows all modes; if you want to
hide a category, configure it in `Debugleton` (see `debugger_backend/`).
`debug` accepts a `mode` kwarg that maps to a log channel. Valid values are
`'all'` (always shown), `'debug'` (the default), and `'test'` (high
priority). Anything else raises.
```python
debug(payload, mode='info')
debug(suspicious_input, mode='warning')
debug(payload, mode='all')
debug(flaky_result, mode='test')
```
---
## More tools (pretty-print, tables, diffs, timing)
```python
debug(my_dict) # structured values pretty-print by default
debug(my_dict, pretty=False) # force flat single-line output
debug(sql_query, lang="sql") # syntax-highlight a string (sql, json, html, ...)
debug(x, y, z) # 2+ data args auto-render as a Name|Type|Value table
debug(x, y, z, table=False) # force per-line instead
debug("a", "b", sep=", ") # join args into one line, like print(sep=...)
debug("about to retry", error=True) # force red error styling on a non-exception
debug.diff(old_value, new_value) # unified diff of two values
with debug.time("fetch users"): # times the block, prints the duration
rows = fetch_users()
```
---
## Visibility (why output might not show)
Output is gated per-file: only files toggled ON print. OpenSwarm re-toggles
every file ON at each backend boot, and new code needs a backend restart to
load anyway (no auto-reload), so in practice your `debug()` lines are always
visible after the restart that loads them. If you ever need to manage this
yourself, the CLI lives in the workspace venv:
```bash
.venv/bin/swarm-debug status # what's toggled where
.venv/bin/swarm-debug toggle on --all # everything visible (run from the workspace root)
```
---
@@ -159,5 +192,8 @@ and effect across the two halves of your stack.
| Log several values in one call | `debug(a, b, c)` |
| Log an exception with red coloring | `debug(err)` (variable name must contain "err" or "error", or pass an `Exception` instance) |
| Avoid truncation | `debug(value, override_max_chars=True)` |
| Use a different log channel | `debug(value, mode='info')` |
| Same thing, legacy import | `import debug; debug(value)` (function and module share the name — see swarm_debug.py shim) |
| Always-shown log channel | `debug(value, mode='all')` |
| Diff two values | `debug.diff(old, new)` |
| Time a block | `with debug.time("label"): ...` |
| Flat instead of pretty-printed | `debug(value, pretty=False)` |
| Syntax-highlight a string | `debug(query, lang="sql")` |
+8 -10
View File
@@ -472,12 +472,12 @@ def p_ensure_warm_python_venv() -> str | None:
logger.warning("warm-venv create failed: %s", r.stderr[-1500:])
return None
# Install the template's dependencies (fastapi[standard], typeguard, transitives); NOT the workspace's own backend, which gets editable-installed per-workspace by run.sh after the cache copy. The venv layout differs by platform: POSIX puts executables in `bin/`, Windows in `Scripts/`, and the executable name itself gets `.exe`.
# Install the template's dependencies (fastapi[standard], typeguard, swarm-debug, transitives); keep this list in sync with webapp_template/backend/pyproject.toml. NOT the workspace's own backend, which gets editable-installed per-workspace by run.sh after the cache copy. The venv layout differs by platform: POSIX puts executables in `bin/`, Windows in `Scripts/`, and the executable name itself gets `.exe`.
if os.name == "nt":
pip = os.path.join(venv_dir, "Scripts", "pip.exe")
else:
pip = os.path.join(venv_dir, "bin", "pip")
deps = ["fastapi[standard]", "typeguard==4.4.2"]
deps = ["fastapi[standard]", "typeguard==4.4.2", "swarm-debug"]
r = subprocess.run(
[pip, "install", "--disable-pip-version-check", *deps],
capture_output=True, text=True, timeout=600,
@@ -557,14 +557,13 @@ def seed_webapp_template_workspace(workspace_dir: str, frontend_port: int) -> No
2. Sed both `.env` and `.env.example` to set `FRONTEND_PORT=<port>`.
BACKEND_PORT stays NONE in both (per spec; the agent flips it
via backend_init.sh when it needs a backend).
3. Append two install-specific paths to `.env` ONLY (NOT
`.env.example`; these are absolute paths on the current
machine, not template defaults):
3. Append an install-specific path to `.env` ONLY (NOT
`.env.example`; it is an absolute path on the current
machine, not a template default):
OPENSWARM_TEMPLATE_BACKEND_PATH=<abs path to master template's backend/>
OPENSWARM_DEBUGGER_PATH=<abs path to OpenSwarm's debugger/ package>
The first is read by `backend_init.sh`; the second is read by
the template's `backend/run.sh` to install our local debugger
before `pip install -e .`.
It is read by `backend_init.sh`. The debugger is no longer
seeded from a local path; the template's `pip install -e .`
resolves `swarm-debug` from PyPI.
Idempotent within reason; re-running over an existing workspace
overwrites template files and re-asserts the env values.
@@ -593,7 +592,6 @@ def seed_webapp_template_workspace(workspace_dir: str, frontend_port: int) -> No
# Install-specific paths; .env only.
patch_env_port(env_path, "OPENSWARM_TEMPLATE_BACKEND_PATH", TEMPLATE_BACKEND_PATH)
patch_env_port(env_path, "OPENSWARM_DEBUGGER_PATH", DEBUGGER_PATH)
# Backend-venv warm-cache path; backend_init.sh checks this for a pre-populated `.venv/` to cp -aR into the workspace instead of paying the ~25s venv-create + pip-install cost. Written even if the cache isn't ready yet; backend_init.sh re-checks at run time.
patch_env_port(env_path, "OPENSWARM_BACKEND_VENV_CACHE", warm_venv_dir())
@@ -6,6 +6,7 @@ requires-python = ">=3.10"
dependencies = [
"fastapi[standard]",
"typeguard==4.4.2",
"swarm-debug",
]
[tool.setuptools]
@@ -92,10 +92,6 @@ else
# --- Install Python dependencies ---
echo "Installing dependencies..."
cd "$BACKEND_DIR_ABSPATH"
if [[ -n "${OPENSWARM_DEBUGGER_PATH:-}" && -d "$OPENSWARM_DEBUGGER_PATH" ]]; then
echo "Installing OpenSwarm debugger (swarm_debug) from $OPENSWARM_DEBUGGER_PATH"
"$VENV_PY" -m pip install -e "$OPENSWARM_DEBUGGER_PATH"
fi
"$VENV_PY" -m pip install -e .
if [[ $? -ne 0 ]]; then
echo "Error: Failed to install Python dependencies."
@@ -112,6 +108,10 @@ fi
# the backend to pick up new code it can hit OpenSwarm's
# /api/outputs/workspace/{ws}/runtime/restart endpoint, which sends a
# clean SIGTERM and restarts via this same script.
# swarm-debug gates output on per-file toggles that default OFF; force all ON each boot so agent-added files show in the Terminal.
if [[ "$IS_WIN" == "1" ]]; then SWARM_DEBUG_BIN="$VENV_DIR/Scripts/swarm-debug.exe"; else SWARM_DEBUG_BIN="$VENV_DIR/bin/swarm-debug"; fi
( cd "$BACKEND_DIR_ABSPATH/.." && "$SWARM_DEBUG_BIN" toggle on --all >/dev/null 2>&1 ) || true
echo "Starting backend server on http://0.0.0.0:${BACKEND_PORT:-8324} ..."
cd "$BACKEND_DIR_ABSPATH/.."
"$VENV_PY" -m uvicorn backend.main:app --host 0.0.0.0 --port "${BACKEND_PORT:-8324}"
@@ -38,7 +38,7 @@ if [[ -d ./backend ]]; then
fi
# Resolve master template backend/ path. OPENSWARM_TEMPLATE_BACKEND_PATH
# is written into .env at seed time; OPENSWARM_DEBUGGER_PATH the same.
# is written into .env at seed time.
if [[ -z "${OPENSWARM_TEMPLATE_BACKEND_PATH:-}" ]]; then
echo "ERROR: OPENSWARM_TEMPLATE_BACKEND_PATH not set in .env. This" >&2
echo " workspace was seeded by an older OpenSwarm; ask the" >&2
+1 -3
View File
@@ -137,7 +137,7 @@ def p_free_port() -> int:
def p_localize_env(folder: str) -> None:
"""Regenerate the workspace .env on the importer's machine: a fresh port plus
this install's absolute template/debugger paths (the source's were dropped)."""
this install's absolute template path (the source's was dropped)."""
env_path = os.path.join(folder, ".env")
example = os.path.join(folder, ".env.example")
if not os.path.exists(env_path):
@@ -147,7 +147,6 @@ def p_localize_env(folder: str) -> None:
return # flat app: no run.sh, no env needed
try:
from backend.apps.outputs.view_builder_templates import (
DEBUGGER_PATH,
TEMPLATE_BACKEND_PATH,
link_node_modules,
patch_env_port,
@@ -157,7 +156,6 @@ def p_localize_env(folder: str) -> None:
return
patch_env_port(env_path, "FRONTEND_PORT", str(p_free_port()))
patch_env_port(env_path, "OPENSWARM_TEMPLATE_BACKEND_PATH", TEMPLATE_BACKEND_PATH)
patch_env_port(env_path, "OPENSWARM_DEBUGGER_PATH", DEBUGGER_PATH)
try:
patch_env_port(env_path, "OPENSWARM_BACKEND_VENV_CACHE", warm_venv_dir())
except Exception:
-4
View File
@@ -17,16 +17,12 @@ from fastapi import HTTPException
from pydantic import BaseModel, Field
from typeguard import typechecked
import debug
from backend.config.Apps import SubApp
@asynccontextmanager
async def web_lifespan():
debug("START")
yield
debug("END")
web = SubApp("web", web_lifespan)
+2 -9
View File
@@ -2,7 +2,6 @@ import os
import time
from fastapi import FastAPI, APIRouter
import debug
from uuid import uuid4
from typing import List
from contextlib import asynccontextmanager
@@ -12,28 +11,23 @@ from typing import Callable
class SubApp:
def __init__(self, name:str, lifespan:Callable):
debug("START", name)
self.id = uuid4()
self.name = name
self.prefix = f"/api/{name}"
self.lifespan = lifespan
self.router = APIRouter()
debug("END")
def __str__(self):
return f"SubApp(name={self.name}, prefix={self.prefix}, id={self.id})"
class MainApp:
def __init__(self, sub_apps: List[SubApp]):
debug("START")
@asynccontextmanager
async def lifespan(app: FastAPI):
async with AsyncExitStack() as stack:
# [perf] per-lifespan boot timing. debug() is a no-op in the packaged build, so without this the packaged backend.log has no per-SubApp markers and a cold-start stall can only be guessed at. One perf_counter + flushed print per app pins exactly which lifespan (or the cold first-touch I/O entering it) dominates.
# [perf] per-lifespan boot timing. Without this the packaged backend.log has no per-SubApp markers and a cold-start stall can only be guessed at. One perf_counter + flushed print per app pins exactly which lifespan (or the cold first-touch I/O entering it) dominates.
p_boot_t0 = time.perf_counter()
for sub_app in sub_apps:
debug(sub_app.name)
p_t0 = time.perf_counter()
await stack.enter_async_context(sub_app.lifespan())
p_dt = (time.perf_counter() - p_t0) * 1000
@@ -51,5 +45,4 @@ class MainApp:
sub_app.router,
prefix=sub_app.prefix,
tags=[sub_app.name]
)
debug("END")
)
-12
View File
@@ -32,18 +32,6 @@ if [[ ! -d "$VENV_DIR" ]]; then
fi
source "$VENV_DIR/bin/activate"
# --- Install custom debugger module if not already installed ---
DEBUGGER_DIR_ABSPATH="$PROJECT_ROOT_ABSPATH/debugger"
if ! pip3 show debug > /dev/null 2>&1; then
echo "Installing debugger module..."
cd "$DEBUGGER_DIR_ABSPATH"
pip3 install -e .
if [[ $? -ne 0 ]]; then
echo "Failed to install debugger module."
exit 1
fi
fi
# --- Install Python dependencies ---
echo "Installing dependencies..."
cd "$BACKEND_DIR_ABSPATH"
-2
View File
@@ -59,8 +59,6 @@ try {
if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed" }
& $VenvPy -m pip install --quiet -r (Join-Path $ScriptDir 'backend\requirements.txt')
if ($LASTEXITCODE -ne 0) { throw "pip install backend reqs failed" }
& $VenvPy -m pip install --quiet -e (Join-Path $ScriptDir 'debugger')
if ($LASTEXITCODE -ne 0) { throw "pip install debugger failed" }
} finally {
$ErrorActionPreference = $prevEAP
}
-4
View File
@@ -79,10 +79,6 @@ if ($LASTEXITCODE -ne 0) { throw "pip upgrade failed" }
& $PythonBin -m pip install -r (Join-Path $ProjectRoot 'backend\requirements.lock')
if ($LASTEXITCODE -ne 0) { throw "pip install requirements failed" }
Write-Host "Installing debugger module..."
& $PythonBin -m pip install (Join-Path $ProjectRoot 'debugger')
if ($LASTEXITCODE -ne 0) { throw "pip install debugger failed" }
Write-Host "Verifying claude-agent-sdk..."
& $PythonBin -c "import claude_agent_sdk; print('claude-agent-sdk installed')"
if ($LASTEXITCODE -ne 0) { throw "claude-agent-sdk verification failed" }
-4
View File
@@ -89,10 +89,6 @@ echo "Installing backend dependencies (from requirements.lock)..."
"$PYTHON_BIN" -m pip install --upgrade pip
"$PYTHON_BIN" -m pip install -r "$PROJECT_ROOT/backend/requirements.lock"
# Install the debugger module
echo "Installing debugger module..."
"$PYTHON_BIN" -m pip install "$PROJECT_ROOT/debugger"
# Verify claude-agent-sdk and its bundled binary
echo "Verifying claude-agent-sdk..."
"$PYTHON_BIN" -c "import claude_agent_sdk; print(f'claude-agent-sdk installed')"
+17 -32
View File
@@ -3,13 +3,11 @@
#
# Idempotent — wipes the existing vendored dir and re-clones at the pinned ref.
# Strips files we don't want shipped (LICENSE, README.md, .gitignore — we
# author our own minimal .gitignore inside the snapshot). Applies our two
# patches:
# 1. backend/run.sh: pip-install $OPENSWARM_DEBUGGER_PATH if set, before
# the existing `pip install -e .` — resolves the `swarm-debug` dep
# from OpenSwarm's bundled debugger/ package instead of PyPI (where
# it doesn't exist).
# 2. Add our own backend_init.sh at the snapshot root.
# author our own minimal .gitignore inside the snapshot). Applies our
# patches (swarm-debug toggle-on at boot, vite config pinning, .gitignore,
# backend_init.sh). The template's `swarm-debug` dependency now resolves
# from PyPI like any other dep; the old local-debugger injection patches
# (editable-install of the bundled debugger/) are gone.
#
# Update REF to bump the pinned snapshot. CI / a future test could compare
# `git rev-parse HEAD` of a fresh clone against REF and fail on drift.
@@ -36,22 +34,19 @@ mkdir -p "$DEST"
( cd "$TMP/clone" && rm -rf .git LICENSE README.md .gitignore )
cp -R "$TMP/clone/." "$DEST/"
# Patch 1: backend/run.sh installs OpenSwarm's local debugger/ before the
# template's own `pip install -e .` so `from swarm_debug import debug` in
# the template's backend code resolves to our bundled package (the PyPI
# `swarm-debug` doesn't exist — our local package registers as `debug`
# and exposes both `debug` and `swarm_debug` module names via setup.py
# py_modules).
# Patch 1: backend/run.sh forces all swarm-debug per-file toggles ON at
# every boot (they default OFF, including files the agent creates later),
# so `debug()` output actually lands in the App Builder Terminal. Runs
# from the workspace root because that's uvicorn's cwd = the package's
# per-project data-dir key.
RUN_SH="$DEST/backend/run.sh"
if ! grep -q "OPENSWARM_DEBUGGER_PATH" "$RUN_SH"; then
# Insert the install line just before `pip install -e .`. macOS sed
# vs GNU sed: use a portable awk inline rewrite.
if ! grep -q "swarm-debug gates output" "$RUN_SH"; then
awk '
/pip install -e \./ && !inserted {
print "if [[ -n \"${OPENSWARM_DEBUGGER_PATH:-}\" && -d \"$OPENSWARM_DEBUGGER_PATH\" ]]; then"
print " echo \"Installing OpenSwarm debugger (swarm_debug) from $OPENSWARM_DEBUGGER_PATH\""
print " pip install -e \"$OPENSWARM_DEBUGGER_PATH\""
print "fi"
/^echo "Starting backend server/ && !inserted {
print "# swarm-debug gates output on per-file toggles that default OFF; force all ON each boot so agent-added files show in the Terminal."
print "if [[ \"$IS_WIN\" == \"1\" ]]; then SWARM_DEBUG_BIN=\"$VENV_DIR/Scripts/swarm-debug.exe\"; else SWARM_DEBUG_BIN=\"$VENV_DIR/bin/swarm-debug\"; fi"
print "( cd \"$BACKEND_DIR_ABSPATH/..\" && \"$SWARM_DEBUG_BIN\" toggle on --all >/dev/null 2>&1 ) || true"
print ""
inserted = 1
}
{ print }
@@ -59,16 +54,6 @@ if ! grep -q "OPENSWARM_DEBUGGER_PATH" "$RUN_SH"; then
chmod +x "$RUN_SH"
fi
# Patch 1b: drop `"swarm-debug"` from the template's backend/pyproject.toml
# dependencies. The OpenSwarm debugger gets installed separately via Patch
# 1's `pip install -e $OPENSWARM_DEBUGGER_PATH`. Leaving the dep listed
# would make pip 404 against PyPI (no such package).
PYPROJECT="$DEST/backend/pyproject.toml"
awk '
/^[[:space:]]*"swarm-debug",?[[:space:]]*$/ { next }
{ print }
' "$PYPROJECT" > "$PYPROJECT.tmp" && mv "$PYPROJECT.tmp" "$PYPROJECT"
# Patch 1c: vite.config.ts — pin host to 127.0.0.1 (so our IPv4-only
# bind poller in runtime.py:_await_frontend_bind() actually sees the
# bound socket on macOS, where `localhost` can resolve to ::1), disable
@@ -161,7 +146,7 @@ if [[ -d ./backend ]]; then
fi
# Resolve master template backend/ path. OPENSWARM_TEMPLATE_BACKEND_PATH
# is written into .env at seed time; OPENSWARM_DEBUGGER_PATH the same.
# is written into .env at seed time.
if [[ -z "${OPENSWARM_TEMPLATE_BACKEND_PATH:-}" ]]; then
echo "ERROR: OPENSWARM_TEMPLATE_BACKEND_PATH not set in .env. This" >&2
echo " workspace was seeded by an older OpenSwarm; ask the" >&2