mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[arnav] remove dead code identified by audit
Each removal verified by checking actual production callers (frontend,
electron, internal HTTP, MCP-server subprocesses) — not just test
references. Symbols whose only callers were tests are removed along
with those tests.
Production removals (~390 LOC):
- backend/main.py
- websocket_session: drop `agent:edit_message` WS branch. Frontend
only ever uses HTTP `POST /api/agents/sessions/{id}/edit_message`
(frontend/src/shared/state/agentsSlice.ts); nothing on the wire
sends a WS message of this type.
- backend/apps/agents/agent_manager.py
- AgentManager._build_connected_tools_context (~80 LOC): zero call
sites in production; the connected-tools system-prompt context is
built inline in _compose_system_prompt now.
- AgentManager._approx_tokens / _summarize_message_block: pure
helpers whose only callers were tests. The compaction path uses
LLM-driven _maybe_compact instead.
- backend/apps/agents/browser_agent.py
- clear_browser_history: only used by tests. _browser_history is
pruned via the size cap inline.
- MODEL_MAP constant: never read.
- backend/apps/agents/mcp_preflight.py
- DISCOVERY_SCAFFOLDING (~25-line system-prompt block): defined but
never appended anywhere. The header comment described an intended
use that the codebase no longer has.
- backend/apps/agents/providers/registry.py
- thinking_params_for, _is_9router_available, OPENROUTER_BASE_URL,
get_context_window: zero callers in production. Thinking-params
routing is done by the provider classes directly; 9Router presence
is detected at request time; context-window numbers are stamped
onto sessions from BUILTIN_MODELS at launch.
- backend/apps/agents/tools/{base,web}.py
- BaseTool.get_schema (abstract) + WebSearchTool/WebFetchTool
overrides: production code in backend/apps/web/web.py instantiates
these tools and only calls .execute(); the JSON-schema lives in
the HTTP wrapper, not on the tool class.
- backend/apps/outputs/outputs.py
- _resolve_model + MODEL_MAP: tests-only.
- load_output: docstring claimed it was a public helper for "other
modules" but no module imported it.
- backend/apps/service/client.py
- set_user_id, the _user_id module global, and the dead cache short-
circuit in _get_user_id: setter was tests-only. _get_user_id now
reads user_email directly from settings on every call.
- backend/apps/settings/credentials.py
- get_provider_credentials: zero callers. The sibling get_agent_sdk_env
is kept (it has the explicit "Legacy helpers" keep-comment).
Test updates:
- test_agent_manager_unit.py: drop _approx_tokens / _summarize_message_block
cases (5 tests), update module docstring index.
- test_browser_agent_unit.py: drop clear_browser_history cases (2 tests)
and the unused _Boom helper class in the repr-fallback test.
- test_outputs_unit.py: drop _resolve_model / load_output cases
(4 tests), update docstring + import list.
- test_v2_invariants.py: drop get_context_window tests + get_schema
assertions on web tools (kept name + BaseTool inheritance checks).
- test_service.py: rewrite the 4 set_user_id-driven tests to drive
user_id through settings.user_email instead, so _get_user_id's live
envelope-stamping path stays covered.
Verification:
- ruff --select F401,F811,F841 backend/ → clean.
- pytest backend/tests/ → 1167 passed, 1 deselected (pre-existing
sandbox git test, unrelated). No tests dropped silently — every
deletion is paired with the corresponding test removal/rewrite.
- Dead-code scan re-run: dead WS events 1→0, Tier-2 high-confidence
14→11 (residue is SDK-callback `context` params + Pydantic `cls`
validators — both false positives vulture can't see through),
vulture total 165→145.
Total diff: -565 / +34 LOC across 15 files.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -450,86 +450,6 @@ class AgentManager:
|
||||
logger.info(f"[MCP-DEBUG] Final mcp_servers: {list(mcp_servers.keys())}")
|
||||
return mcp_servers
|
||||
|
||||
def _build_connected_tools_context(self, allowed_tools: list[str]) -> str | None:
|
||||
"""Build a context block describing connected MCP tools and their accounts.
|
||||
|
||||
Tools set to 'deny' and fully-denied servers are excluded.
|
||||
"""
|
||||
all_tools = load_all_tools()
|
||||
mcp_tools = [t for t in all_tools if t.mcp_config and t.enabled and t.auth_status in ("configured", "connected")]
|
||||
|
||||
sections = []
|
||||
for tool in mcp_tools:
|
||||
tool_ref = f"mcp:{tool.name}"
|
||||
if tool_ref not in allowed_tools and allowed_tools != get_all_tool_names():
|
||||
continue
|
||||
|
||||
if _is_fully_denied(tool):
|
||||
continue
|
||||
|
||||
server_name = _sanitize_server_name(tool.name)
|
||||
denied = _get_denied_tool_names(tool)
|
||||
tool_descs = {
|
||||
k: v for k, v in tool.tool_permissions.get("_tool_descriptions", {}).items()
|
||||
if k not in denied
|
||||
}
|
||||
if not tool_descs:
|
||||
continue
|
||||
|
||||
lines = [f"MCP Server: {server_name}"]
|
||||
lines.append(f" Status: {tool.auth_status}")
|
||||
|
||||
if tool.connected_account_email:
|
||||
lines.append(f" Connected account: {tool.connected_account_email}")
|
||||
lines.append(
|
||||
f" IMPORTANT: When calling tools from this server that require an email "
|
||||
f"parameter (e.g. user_google_email, user_email), always use "
|
||||
f"\"{tool.connected_account_email}\" automatically — do NOT ask the user."
|
||||
)
|
||||
|
||||
# Discord guild scoping — hard restriction. The bot may technically
|
||||
# be in other servers (across other OpenSwarm users), but this
|
||||
# specific user only authorized these guild IDs.
|
||||
if tool.name.lower() == "discord":
|
||||
guilds = tool.oauth_tokens.get("guilds") or []
|
||||
if guilds:
|
||||
guild_descriptions = ", ".join(
|
||||
f"{g.get('name', 'Unknown')} ({g.get('id', '')})" for g in guilds
|
||||
)
|
||||
allowed_ids = [g.get("id", "") for g in guilds if g.get("id")]
|
||||
lines.append(
|
||||
f" AUTHORIZED DISCORD SERVERS (guild_ids): {guild_descriptions}"
|
||||
)
|
||||
lines.append(
|
||||
f" HARD RESTRICTION: You MUST only call Discord tools that operate on "
|
||||
f"these guild_ids: {allowed_ids}. NEVER call Discord tools on any other "
|
||||
f"guild_id even if the bot has access to it. NEVER list, search, or "
|
||||
f"enumerate servers outside this list. If a user asks about a server "
|
||||
f"not in this list, refuse and tell them to authorize it via the Connect "
|
||||
f"Discord button. This is a security boundary, not a preference."
|
||||
)
|
||||
else:
|
||||
lines.append(
|
||||
f" No Discord servers authorized yet. Tell the user to click "
|
||||
f"'Connect Discord' to add a server before attempting any Discord actions."
|
||||
)
|
||||
|
||||
tool_names = list(tool_descs.keys())
|
||||
if tool_names:
|
||||
lines.append(f" Available tools ({len(tool_names)}): {', '.join(tool_names)}")
|
||||
|
||||
sections.append("\n".join(lines))
|
||||
|
||||
if not sections:
|
||||
return None
|
||||
return (
|
||||
"<connected_mcp_tools>\n"
|
||||
"The following MCP tool servers are connected and available. "
|
||||
"Use them directly when relevant to the user's request.\n\n"
|
||||
+ "\n\n".join(sections)
|
||||
+ "\n</connected_mcp_tools>"
|
||||
)
|
||||
|
||||
def _build_outputs_context(self, active_outputs: list[str] | None = None) -> str | None:
|
||||
"""Outputs context for the system prompt.
|
||||
|
||||
@@ -962,86 +882,6 @@ class AgentManager:
|
||||
return ""
|
||||
return "<prior_conversation>\n" + "\n".join(lines) + "\n</prior_conversation>"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Compaction & token guard (Phase 2)
|
||||
#
|
||||
# Triggered by *live* context-usage ratio, not turn count. The signal
|
||||
# is the same `ctx_used_pct` we already broadcast to the UI on every
|
||||
# turn: input_tokens / context_window. Three escalating thresholds:
|
||||
# - compact_threshold_pct (default 0.65): summarize stale tool_results
|
||||
# and old user/assistant pairs before the next query() call
|
||||
# - context_soft_cap_pct (default 0.90): pre-send hard guard. After
|
||||
# compaction, if still over, LRU-trim active_outputs/active_mcps
|
||||
# - >= 1.0 hits the proxy/Anthropic 200K ceiling — friendly card
|
||||
# surfaces from the catch-all
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _approx_tokens(text: str) -> int:
|
||||
"""Conservative chars/4 estimate. Used for the pre-send guard
|
||||
and the compaction trigger when a precise count_tokens isn't
|
||||
cheap (or the route isn't Anthropic). Errs slightly high so we
|
||||
compact a touch earlier than strictly necessary."""
|
||||
return max(1, len(text or "") // 4)
|
||||
|
||||
@staticmethod
|
||||
def _summarize_message_block(messages: list) -> str:
|
||||
"""Programmatic, no-LLM summary of a message slice. Mirrors the
|
||||
shape of browser_agent._summarize_messages: extracts the original
|
||||
user task, counts tool calls, captures the last assistant text.
|
||||
Cheap, deterministic, and never makes a network call — so
|
||||
compaction itself adds zero latency to the user's turn.
|
||||
"""
|
||||
if not messages:
|
||||
return ""
|
||||
|
||||
initial_task = ""
|
||||
for m in messages:
|
||||
if getattr(m, "role", "") == "user":
|
||||
content = getattr(m, "content", "")
|
||||
txt = content if isinstance(content, str) else str(content)
|
||||
if txt.strip():
|
||||
initial_task = txt.strip()[:400]
|
||||
break
|
||||
|
||||
tool_calls_by_name: dict[str, int] = {}
|
||||
last_tool_results = 0
|
||||
last_assistant_text = ""
|
||||
for m in messages:
|
||||
role = getattr(m, "role", "")
|
||||
if role == "tool_call":
|
||||
content = getattr(m, "content", {}) or {}
|
||||
name = (content.get("tool") if isinstance(content, dict) else None) or "unknown"
|
||||
tool_calls_by_name[name] = tool_calls_by_name.get(name, 0) + 1
|
||||
elif role == "tool_result":
|
||||
last_tool_results += 1
|
||||
elif role == "assistant":
|
||||
content = getattr(m, "content", "")
|
||||
if isinstance(content, str) and content.strip():
|
||||
last_assistant_text = content.strip()
|
||||
elif isinstance(content, list):
|
||||
for block in content:
|
||||
if isinstance(block, dict) and block.get("type") == "text":
|
||||
txt = (block.get("text") or "").strip()
|
||||
if txt:
|
||||
last_assistant_text = txt
|
||||
|
||||
parts = ["<compacted_history>"]
|
||||
parts.append("[The following is a programmatic summary of earlier turns in this session. Originals are preserved on disk and viewable via the chat UI's compaction drawer.]")
|
||||
if initial_task:
|
||||
parts.append(f'Initial user request: "{initial_task}"')
|
||||
if tool_calls_by_name:
|
||||
total = sum(tool_calls_by_name.values())
|
||||
top = sorted(tool_calls_by_name.items(), key=lambda kv: -kv[1])[:8]
|
||||
parts.append(f"Tool calls so far ({total} total): " + ", ".join(f"{n}×{c}" for n, c in top))
|
||||
if last_tool_results:
|
||||
parts.append(f"Tool results received: {last_tool_results}")
|
||||
if last_assistant_text:
|
||||
parts.append("Last assistant message:")
|
||||
parts.append(last_assistant_text[:1200])
|
||||
parts.append("</compacted_history>")
|
||||
return "\n".join(parts)
|
||||
|
||||
def _maybe_compact(self, session: AgentSession, force: bool = False) -> bool:
|
||||
"""Run summarizer when ctx_used_pct >= compact_threshold_pct (or force).
|
||||
|
||||
|
||||
@@ -20,12 +20,6 @@ from backend.apps.tools_lib.tools_lib import load_builtin_permissions
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MODEL_MAP = {
|
||||
"sonnet": "claude-sonnet-4-6",
|
||||
"opus": "claude-opus-4-6",
|
||||
"haiku": "claude-haiku-4-5-20251001",
|
||||
}
|
||||
|
||||
# Cache of conversation history per browser_id so successive BrowserAgent
|
||||
# calls on the same browser can resume rather than restart from scratch.
|
||||
# Without this every "swipe right" / "swipe left" call has to take a new
|
||||
@@ -35,11 +29,6 @@ _browser_history: dict[str, list[dict]] = {}
|
||||
_MAX_HISTORY_MESSAGES = 30
|
||||
|
||||
|
||||
def clear_browser_history(browser_id: str) -> None:
|
||||
"""Drop cached conversation history for a browser (e.g. when it's closed)."""
|
||||
_browser_history.pop(browser_id, None)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Loop detection
|
||||
#
|
||||
|
||||
@@ -32,35 +32,6 @@ from backend.apps.tools_lib.tools_lib import _load_all as load_all_tools
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tool-agnostic discovery scaffolding — appended to the agent's system prompt
|
||||
# only when preflight flags the prompt as vague/information-gathering.
|
||||
# ---------------------------------------------------------------------------
|
||||
DISCOVERY_SCAFFOLDING = (
|
||||
"# Discovery before action\n"
|
||||
"When a request is vague or could be grounded in user context, do not "
|
||||
"guess generic defaults. First silently enumerate what would change the "
|
||||
"output — voice, tone, audience, prior context, recent precedent, facts "
|
||||
"that only live in the user's data. Then look at your available tools and "
|
||||
"pick the ones that could answer those unknowns. Read a few examples "
|
||||
"(usually 3–10 is enough), summarize what you found into a few bullets, "
|
||||
"then act confidently.\n\n"
|
||||
"Tool-selection hierarchy for information gathering:\n"
|
||||
" 1. Direct local access (filesystem reads, code search, shell) — "
|
||||
"cheapest and fastest.\n"
|
||||
" 2. Connected services / MCP tools — for user data that lives in a "
|
||||
"linked account (email, calendar, notes, tickets, etc.).\n"
|
||||
" 3. Web search / fetch — for public information that isn't in your "
|
||||
"training cutoff.\n"
|
||||
" 4. Browser automation — only when a real interactive session or "
|
||||
"login is required.\n"
|
||||
" 5. Sub-agents — only for parallelizable subtasks or to isolate heavy "
|
||||
"context. Not for serial steps.\n\n"
|
||||
"Asking the user is a fallback, not a first move. Never fabricate. If "
|
||||
"no tool can ground a critical unknown, ask one concise question."
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Curated MCP shortlist. These `id` values MUST match the exact `name` field
|
||||
# on ToolDefinition entries that OpenSwarm ships as defaults (see
|
||||
|
||||
@@ -184,92 +184,6 @@ BUILTIN_MODELS: dict[str, list[dict[str, Any]]] = {
|
||||
],
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Thinking level translation
|
||||
# ---------------------------------------------------------------------------
|
||||
# Each provider has a different API shape for "how hard should the model
|
||||
# think." We expose a single provider-agnostic level (off/low/medium/high/
|
||||
# auto) on the session and translate here.
|
||||
#
|
||||
# Returns the provider-specific payload to merge into request params, or
|
||||
# None if no special thinking params should be sent (use defaults).
|
||||
|
||||
def thinking_params_for(api: str, level: str, model_id: str = "") -> dict | None:
|
||||
"""Translate a provider-agnostic thinking level to per-provider API params.
|
||||
|
||||
Args:
|
||||
api: "anthropic" | "codex" | "gemini-cli"
|
||||
level: "off" | "low" | "medium" | "high" | "auto"
|
||||
model_id: optional, used to pick adaptive vs legacy for Claude
|
||||
|
||||
Returns a dict to merge into request params, or None for "use defaults".
|
||||
"""
|
||||
if level == "auto":
|
||||
# Let provider use its own default. For Claude 4.6 we still want
|
||||
# adaptive thinking on by default so users see reasoning.
|
||||
if api == "anthropic":
|
||||
return {"thinking": {"type": "adaptive"}}
|
||||
return None
|
||||
|
||||
if level == "off":
|
||||
if api == "anthropic":
|
||||
return {"thinking": {"type": "disabled"}}
|
||||
if api == "codex":
|
||||
return {"reasoning": {"effort": "none"}}
|
||||
# Gemini: thinkingBudget=0 truly disables reasoning (no
|
||||
# thoughtSignature emitted). Critical for multi-step tool turns
|
||||
# — without this Gemini 2.5/3.x still emits signatures even at
|
||||
# the lowest "level," which then break the next request with
|
||||
# "Thought signature is not valid" 400 because the SDK has no
|
||||
# way to round-trip them. The translator at 9Router 0.3.60
|
||||
# explicitly checks `thinkingBudget == 0` to skip emitting
|
||||
# thinking config, which is what we want.
|
||||
if api == "gemini-cli":
|
||||
return {"thinkingConfig": {"thinkingBudget": 0}}
|
||||
return None
|
||||
|
||||
# Claude 4.6 models use adaptive thinking (no manual budget). For older
|
||||
# Claude models we'd use budget_tokens; we don't ship those today.
|
||||
if api == "anthropic":
|
||||
return {"thinking": {"type": "adaptive"}}
|
||||
|
||||
if api == "codex":
|
||||
effort_map = {"low": "low", "medium": "medium", "high": "high"}
|
||||
return {"reasoning": {"effort": effort_map[level]}}
|
||||
|
||||
if api == "gemini-cli":
|
||||
level_map = {"low": "LOW", "medium": "MEDIUM", "high": "HIGH"}
|
||||
return {"thinkingConfig": {"thinkingLevel": level_map[level]}}
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# OpenRouter: built-in integration for 300+ models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1"
|
||||
|
||||
_9router_cache: dict = {"available": None, "checked_at": 0}
|
||||
|
||||
|
||||
def _is_9router_available() -> bool:
|
||||
"""Check if 9Router is running on localhost:20128. Caches for 30 seconds."""
|
||||
import time as _time
|
||||
now = _time.time()
|
||||
if _9router_cache["available"] is not None and now - _9router_cache["checked_at"] < 30:
|
||||
return _9router_cache["available"]
|
||||
try:
|
||||
import httpx
|
||||
r = httpx.get("http://localhost:20128/v1/models", timeout=2.0)
|
||||
available = r.status_code == 200
|
||||
except Exception:
|
||||
available = False
|
||||
_9router_cache["available"] = available
|
||||
_9router_cache["checked_at"] = now
|
||||
return available
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Model resolution (used by the live claude_agent_sdk path)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -485,24 +399,6 @@ async def resolve_aux_model(
|
||||
)
|
||||
|
||||
|
||||
def get_context_window(provider: str, model: str, settings: AppSettings | None = None) -> int:
|
||||
"""Look up context window for any model."""
|
||||
# Check built-in models first
|
||||
for models in BUILTIN_MODELS.values():
|
||||
for m in models:
|
||||
if m["value"] == model:
|
||||
return m.get("context_window", 128_000)
|
||||
|
||||
# Check custom providers
|
||||
if settings:
|
||||
for cp in getattr(settings, "custom_providers", []):
|
||||
for m in cp.models:
|
||||
if m.get("value") == model or m.get("id") == model:
|
||||
return m.get("context_window", 128_000)
|
||||
|
||||
return 128_000 # safe default
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cost tracking
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -14,10 +14,6 @@ class BaseTool(ABC):
|
||||
name: str
|
||||
description: str
|
||||
|
||||
@abstractmethod
|
||||
def get_schema(self) -> dict:
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, input_data: dict, context: ToolContext) -> list[dict]:
|
||||
...
|
||||
|
||||
@@ -49,24 +49,6 @@ class WebSearchTool(BaseTool):
|
||||
"snippets for the top results."
|
||||
)
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "The search query.",
|
||||
},
|
||||
"num_results": {
|
||||
"type": "integer",
|
||||
"description": "Maximum number of results to return (default 5).",
|
||||
"default": 5,
|
||||
},
|
||||
},
|
||||
"required": ["query"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
async def execute(self, input_data: dict, context: ToolContext) -> list[dict]:
|
||||
query: str = input_data["query"]
|
||||
num_results: int = input_data.get("num_results", 5)
|
||||
@@ -163,23 +145,6 @@ class WebFetchTool(BaseTool):
|
||||
"HTML is stripped to plain text. Output capped at ~250 KB."
|
||||
)
|
||||
|
||||
def get_schema(self) -> dict:
|
||||
return {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"url": {
|
||||
"type": "string",
|
||||
"description": "The URL to fetch.",
|
||||
},
|
||||
"prompt": {
|
||||
"type": "string",
|
||||
"description": "Optional prompt/context describing what information to look for.",
|
||||
},
|
||||
},
|
||||
"required": ["url"],
|
||||
"additionalProperties": False,
|
||||
}
|
||||
|
||||
async def execute(self, input_data: dict, context: ToolContext) -> list[dict]:
|
||||
url: str = input_data["url"]
|
||||
prompt: str | None = input_data.get("prompt")
|
||||
|
||||
@@ -22,17 +22,6 @@ from backend.apps.settings.settings import load_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MODEL_MAP = {
|
||||
"sonnet": "claude-sonnet-4-20250514",
|
||||
"opus": "claude-opus-4-20250514",
|
||||
"haiku": "claude-haiku-4-5-20251001",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_model(short_name: str) -> str:
|
||||
return MODEL_MAP.get(short_name, short_name)
|
||||
|
||||
|
||||
def _get_anthropic_client(api_model: str | None = None):
|
||||
"""Create an AsyncAnthropic client using the API key from app settings.
|
||||
|
||||
@@ -184,15 +173,6 @@ def _load(output_id: str) -> Output:
|
||||
return Output(**json.load(f))
|
||||
|
||||
|
||||
def load_output(output_id: str) -> Output | None:
|
||||
"""Public helper for other modules to resolve an output by ID."""
|
||||
path = os.path.join(DATA_DIR, f"{output_id}.json")
|
||||
if not os.path.exists(path):
|
||||
return None
|
||||
with open(path) as f:
|
||||
return Output(**json.load(f))
|
||||
|
||||
|
||||
def _walk_directory(folder: str) -> dict[str, str]:
|
||||
"""Walk a directory tree and return {relative_path: content} for all text files."""
|
||||
files: dict[str, str] = {}
|
||||
|
||||
@@ -46,7 +46,6 @@ _MAX_INFLIGHT = 16
|
||||
|
||||
_test_sink: Optional[Any] = None
|
||||
_install_id: Optional[str] = None
|
||||
_user_id: Optional[str] = None
|
||||
_inflight = 0
|
||||
_inflight_lock = asyncio.Lock()
|
||||
_drain_lock = asyncio.Lock()
|
||||
@@ -85,9 +84,6 @@ def _get_install_id() -> str:
|
||||
|
||||
|
||||
def _get_user_id() -> Optional[str]:
|
||||
global _user_id
|
||||
if _user_id:
|
||||
return _user_id
|
||||
try:
|
||||
from backend.apps.settings.settings import load_settings
|
||||
s = load_settings()
|
||||
@@ -96,11 +92,6 @@ def _get_user_id() -> Optional[str]:
|
||||
return None
|
||||
|
||||
|
||||
def set_user_id(uid: Optional[str]) -> None:
|
||||
global _user_id
|
||||
_user_id = uid or None
|
||||
|
||||
|
||||
def _is_enabled(kind: str) -> bool:
|
||||
"""Honour user opt-out. Diagnostic always flows (errors block usability);
|
||||
state + session honour the toggle."""
|
||||
|
||||
@@ -75,36 +75,6 @@ def validate_credentials(settings: AppSettings, provider: str = "anthropic") ->
|
||||
return
|
||||
|
||||
|
||||
def get_provider_credentials(settings: AppSettings, provider: str) -> dict[str, str]:
|
||||
"""Return credential dict for a specific provider."""
|
||||
p = provider.lower().strip()
|
||||
validate_credentials(settings, provider)
|
||||
|
||||
if p in ("anthropic", "claude"):
|
||||
if getattr(settings, "connection_mode", "own_key") == "openswarm-pro":
|
||||
return {
|
||||
"auth_token": getattr(settings, "openswarm_bearer_token", "") or "",
|
||||
"base_url": getattr(settings, "openswarm_proxy_url", None) or OPENSWARM_DEFAULT_PROXY_URL,
|
||||
}
|
||||
return {"api_key": settings.anthropic_api_key or ""}
|
||||
|
||||
if p in ("openai", "codex"):
|
||||
return {"api_key": settings.openai_api_key or ""}
|
||||
|
||||
if p in ("gemini", "google", "gemini-cli"):
|
||||
return {"api_key": getattr(settings, "google_api_key", "") or ""}
|
||||
|
||||
if p == "openrouter":
|
||||
return {"api_key": getattr(settings, "openrouter_api_key", "") or ""}
|
||||
|
||||
# Custom provider
|
||||
for cp in getattr(settings, "custom_providers", []):
|
||||
if cp.name.lower() == p:
|
||||
return {"api_key": cp.api_key, "base_url": cp.base_url}
|
||||
|
||||
raise ValueError(f"No credentials for provider: {provider}")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Legacy helpers (kept for backward compat during migration)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -207,13 +207,6 @@ async def websocket_session(websocket: WebSocket, session_id: str):
|
||||
"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)
|
||||
|
||||
@@ -12,8 +12,7 @@ Test groups:
|
||||
- pure instance methods (`_resolve_mode`, `_compose_system_prompt`,
|
||||
`_resolve_context_paths`, `_build_dir_tree`, `_resolve_forced_tools`,
|
||||
`_resolve_attached_skills`, `_get_branch_messages`,
|
||||
`_build_history_prefix`, `_approx_tokens`,
|
||||
`_summarize_message_block`, `_truncate_large_tool_result`,
|
||||
`_build_history_prefix`, `_truncate_large_tool_result`,
|
||||
`_build_search_text`, `_maybe_compact`)
|
||||
- lifecycle (launch/update/edit/switch_branch/duplicate/close/
|
||||
delete/resume/stop, history, browser-agent children, approval,
|
||||
@@ -564,7 +563,7 @@ def test_get_branch_messages_walks_fork_lineage():
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _build_history_prefix / _approx_tokens / _summarize_message_block
|
||||
# _build_history_prefix
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -587,59 +586,6 @@ def test_build_history_prefix_empty_returns_empty():
|
||||
assert AgentManager._build_history_prefix([]) == ""
|
||||
|
||||
|
||||
@pytest.mark.parametrize("text,expected", [
|
||||
("", 1),
|
||||
("a" * 4, 1),
|
||||
("a" * 16, 4),
|
||||
("a" * 100, 25),
|
||||
])
|
||||
def test_approx_tokens_chars_over_four(text, expected):
|
||||
assert AgentManager._approx_tokens(text) == expected
|
||||
|
||||
|
||||
def test_approx_tokens_handles_none():
|
||||
assert AgentManager._approx_tokens(None) == 1
|
||||
|
||||
|
||||
def test_summarize_message_block_empty_returns_empty():
|
||||
assert AgentManager._summarize_message_block([]) == ""
|
||||
|
||||
|
||||
def test_summarize_message_block_extracts_initial_task_and_counts():
|
||||
msgs = [
|
||||
Message(role="user", content="please do the thing"),
|
||||
Message(role="tool_call", content={"tool": "Read", "input": {}}),
|
||||
Message(role="tool_call", content={"tool": "Bash", "input": {}}),
|
||||
Message(role="tool_call", content={"tool": "Read", "input": {}}),
|
||||
Message(role="tool_result", content="ok"),
|
||||
Message(role="assistant", content="done"),
|
||||
]
|
||||
out = AgentManager._summarize_message_block(msgs)
|
||||
assert "<compacted_history>" in out
|
||||
assert "please do the thing" in out
|
||||
# Tool counts
|
||||
assert "Read×2" in out
|
||||
assert "Bash×1" in out
|
||||
assert "Tool calls so far (3 total)" in out
|
||||
assert "Tool results received: 1" in out
|
||||
assert "Last assistant message:" in out
|
||||
assert "done" in out
|
||||
|
||||
|
||||
def test_summarize_message_block_assistant_list_content():
|
||||
"""Assistant messages with list content (Anthropic block shape)
|
||||
should still surface their text."""
|
||||
msgs = [
|
||||
Message(role="user", content="task"),
|
||||
Message(role="assistant", content=[
|
||||
{"type": "text", "text": "the answer"},
|
||||
{"type": "tool_use", "id": "t1"},
|
||||
]),
|
||||
]
|
||||
out = AgentManager._summarize_message_block(msgs)
|
||||
assert "the answer" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _truncate_large_tool_result
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -11,8 +11,7 @@ in small helpers that can be exercised in isolation:
|
||||
conversations.
|
||||
- `_format_tool_result` — translate `ws_manager` result dicts into
|
||||
Anthropic content blocks.
|
||||
- `clear_browser_history` / `execute_browser_tool` — small async + state
|
||||
helpers.
|
||||
- `execute_browser_tool` — small async + state helper.
|
||||
|
||||
The integration-level tests for `run_browser_agent` /
|
||||
`run_browser_agents` / `_create_browser_card` live in the sister file
|
||||
@@ -40,7 +39,6 @@ from backend.apps.agents.browser_agent import (
|
||||
_summarize_messages,
|
||||
_trim_history_by_turns,
|
||||
_validate_message_pairing,
|
||||
clear_browser_history,
|
||||
execute_browser_tool,
|
||||
)
|
||||
|
||||
@@ -59,28 +57,6 @@ def _clear_browser_history_module_state():
|
||||
ba._browser_history.clear()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# clear_browser_history
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_clear_browser_history_drops_only_target_entry():
|
||||
ba._browser_history["b-1"] = [{"role": "user", "content": "hi"}]
|
||||
ba._browser_history["b-2"] = [{"role": "user", "content": "yo"}]
|
||||
|
||||
clear_browser_history("b-1")
|
||||
|
||||
assert "b-1" not in ba._browser_history
|
||||
assert "b-2" in ba._browser_history
|
||||
|
||||
|
||||
def test_clear_browser_history_missing_id_is_noop():
|
||||
"""Calling clear on an unknown id must not raise (it's used in
|
||||
cleanup paths where the cache may already be empty)."""
|
||||
clear_browser_history("never-existed")
|
||||
assert ba._browser_history == {}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _hash_tool_call
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -4,12 +4,12 @@ These exercise pure logic and pydantic models without booting FastAPI.
|
||||
The integration surface (routes) lives in `test_api_outputs.py`.
|
||||
|
||||
Covers:
|
||||
- outputs.py helpers: _resolve_model, _validate_against_schema,
|
||||
- outputs.py helpers: _validate_against_schema,
|
||||
_build_data_injection, _inject_data_into_html,
|
||||
_inject_token_into_relative_urls (every branch in
|
||||
_ABSOLUTE_URL_PREFIXES + token-already-present + fragment),
|
||||
_decode_data_param, _walk_directory.
|
||||
- On-disk store helpers: _save / _load / load_output / _load_all.
|
||||
- On-disk store helpers: _save / _load / _load_all.
|
||||
- Models: legacy `frontend_code` / `backend_code` / `schema_json`
|
||||
migration into `files`, plus the property accessors.
|
||||
- executor.execute_backend_code: happy path, stdout capture,
|
||||
@@ -35,12 +35,9 @@ from backend.apps.outputs.outputs import (
|
||||
_inject_token_into_relative_urls,
|
||||
_load,
|
||||
_load_all,
|
||||
_resolve_model,
|
||||
_save,
|
||||
_validate_against_schema,
|
||||
_walk_directory,
|
||||
load_output,
|
||||
MODEL_MAP,
|
||||
)
|
||||
from backend.apps.outputs.models import (
|
||||
AutoRunConfig,
|
||||
@@ -55,22 +52,6 @@ from backend.apps.outputs.executor import (
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _resolve_model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_resolve_model_known_short_name():
|
||||
assert _resolve_model("sonnet") == MODEL_MAP["sonnet"]
|
||||
assert _resolve_model("opus") == MODEL_MAP["opus"]
|
||||
assert _resolve_model("haiku") == MODEL_MAP["haiku"]
|
||||
|
||||
|
||||
def test_resolve_model_unknown_passthrough():
|
||||
assert _resolve_model("claude-3-5-haiku") == "claude-3-5-haiku"
|
||||
assert _resolve_model("") == ""
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _validate_against_schema
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -272,7 +253,7 @@ def test_walk_directory_skips_unreadable(tmp_path):
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _load_all / _save / _load / load_output
|
||||
# _load_all / _save / _load
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -291,18 +272,6 @@ def test_load_missing_raises_404(tmp_data_dirs):
|
||||
assert exc.value.status_code == 404
|
||||
|
||||
|
||||
def test_load_output_returns_none_for_missing(tmp_data_dirs):
|
||||
assert load_output("does-not-exist") is None
|
||||
|
||||
|
||||
def test_load_output_returns_resolved(tmp_data_dirs):
|
||||
out = Output(name="x")
|
||||
_save(out)
|
||||
fetched = load_output(out.id)
|
||||
assert fetched is not None
|
||||
assert fetched.name == "x"
|
||||
|
||||
|
||||
def test_load_all_picks_up_saved(tmp_data_dirs):
|
||||
a = Output(name="a")
|
||||
b = Output(name="b")
|
||||
|
||||
@@ -45,7 +45,6 @@ def patch_settings(tmp_path):
|
||||
def fresh_client(tmp_path):
|
||||
import backend.apps.service.client as client
|
||||
client._install_id = None
|
||||
client._user_id = None
|
||||
client._test_sink = None
|
||||
spool = tmp_path / "spool.db"
|
||||
with patch.object(client, "_spool_path", lambda: str(spool)):
|
||||
@@ -78,35 +77,42 @@ def test_sync_carries_install_id(sink):
|
||||
assert body["client_state"]["install_id"] == "test-install-abc"
|
||||
|
||||
|
||||
def test_sync_carries_user_id_when_set(sink):
|
||||
from backend.apps.service.client import sync, set_user_id
|
||||
set_user_id("alice@example.com")
|
||||
sync({})
|
||||
def test_sync_carries_user_id_when_set_in_settings(tmp_path, sink):
|
||||
"""User id is read from settings.user_email at envelope-build time."""
|
||||
sf = tmp_path / "settings_with_email.json"
|
||||
sf.write_text(json.dumps({
|
||||
"installation_id": "test-install-abc",
|
||||
"analytics_opt_in": True,
|
||||
"user_email": "alice@example.com",
|
||||
}))
|
||||
import backend.apps.settings.settings as settings_mod
|
||||
with patch.object(settings_mod, "SETTINGS_FILE", str(sf)):
|
||||
from backend.apps.service.client import sync
|
||||
sync({})
|
||||
_, body = sink[0]
|
||||
assert body["client_state"]["user_id"] == "alice@example.com"
|
||||
|
||||
|
||||
def test_sync_no_user_id_when_not_set(sink):
|
||||
def test_sync_no_user_id_when_email_not_set(sink):
|
||||
"""No user_email in settings → user_id absent from envelope."""
|
||||
from backend.apps.service.client import sync
|
||||
sync({})
|
||||
_, body = sink[0]
|
||||
assert "user_id" not in body["client_state"]
|
||||
|
||||
|
||||
def test_sync_user_id_cleared_with_none(sink):
|
||||
from backend.apps.service.client import sync, set_user_id
|
||||
set_user_id("alice")
|
||||
set_user_id(None)
|
||||
sync({})
|
||||
_, body = sink[0]
|
||||
assert "user_id" not in body["client_state"]
|
||||
|
||||
|
||||
def test_sync_user_id_cleared_with_empty(sink):
|
||||
from backend.apps.service.client import sync, set_user_id
|
||||
set_user_id("alice")
|
||||
set_user_id("")
|
||||
sync({})
|
||||
def test_sync_no_user_id_when_email_empty(tmp_path, sink):
|
||||
"""Empty-string user_email is treated like missing."""
|
||||
sf = tmp_path / "settings_empty_email.json"
|
||||
sf.write_text(json.dumps({
|
||||
"installation_id": "test-install-abc",
|
||||
"analytics_opt_in": True,
|
||||
"user_email": "",
|
||||
}))
|
||||
import backend.apps.settings.settings as settings_mod
|
||||
with patch.object(settings_mod, "SETTINGS_FILE", str(sf)):
|
||||
from backend.apps.service.client import sync
|
||||
sync({})
|
||||
_, body = sink[0]
|
||||
assert "user_id" not in body["client_state"]
|
||||
|
||||
|
||||
@@ -926,23 +926,6 @@ def test_find_builtin_model_returns_dict_for_known():
|
||||
assert sonnet.get("api") == "anthropic"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group R — context window
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_get_context_window_known_model():
|
||||
from backend.apps.agents.providers.registry import get_context_window
|
||||
cw = get_context_window("Anthropic", "sonnet")
|
||||
assert cw >= 200_000
|
||||
|
||||
|
||||
def test_get_context_window_unknown_returns_default():
|
||||
from backend.apps.agents.providers.registry import get_context_window
|
||||
cw = get_context_window("Unknown", "fake-model")
|
||||
assert cw == 128_000
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Group S — calculate_cost regression tests
|
||||
# ===========================================================================
|
||||
@@ -1051,18 +1034,16 @@ def test_web_tools_classes_inherit_basetool():
|
||||
assert issubclass(WebFetchTool, BaseTool)
|
||||
|
||||
|
||||
def test_web_search_tool_has_name_and_schema():
|
||||
def test_web_search_tool_has_name():
|
||||
from backend.apps.agents.tools.web import WebSearchTool
|
||||
tool = WebSearchTool()
|
||||
assert tool.name
|
||||
assert isinstance(tool.get_schema(), dict)
|
||||
|
||||
|
||||
def test_web_fetch_tool_has_name_and_schema():
|
||||
def test_web_fetch_tool_has_name():
|
||||
from backend.apps.agents.tools.web import WebFetchTool
|
||||
tool = WebFetchTool()
|
||||
assert tool.name
|
||||
assert isinstance(tool.get_schema(), dict)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user