SurfSense/surfsense_backend/app/agents/new_chat/feature_flags.py
2026-04-28 23:52:37 -07:00

199 lines
7.2 KiB
Python

"""
Feature flags for the SurfSense new_chat agent stack.
These flags gate the newer agent middleware (some ported from OpenCode,
some sourced from ``langchain.agents.middleware`` / ``deepagents``, some
SurfSense-native). They follow a "default-OFF for risky things,
default-ON for safe upgrades, master kill-switch for everything new" model.
All new middleware checks its flag at agent build time. If the master
kill-switch ``SURFSENSE_DISABLE_NEW_AGENT_STACK`` is set, every new
middleware is disabled regardless of its individual flag. This gives
operators a single switch to revert to pre-port behavior.
Examples
--------
Local development (recommended for trying everything except doom-loop / selector):
SURFSENSE_ENABLE_CONTEXT_EDITING=true
SURFSENSE_ENABLE_COMPACTION_V2=true
SURFSENSE_ENABLE_RETRY_AFTER=true
SURFSENSE_ENABLE_TOOL_CALL_REPAIR=true
SURFSENSE_ENABLE_PERMISSION=false # default off, opt-in per deploy
SURFSENSE_ENABLE_DOOM_LOOP=false # default off until UI ships
SURFSENSE_ENABLE_LLM_TOOL_SELECTOR=false
Master kill-switch (overrides everything else):
SURFSENSE_DISABLE_NEW_AGENT_STACK=true
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
logger = logging.getLogger(__name__)
def _env_bool(name: str, default: bool) -> bool:
"""Parse a boolean env var. Accepts ``1``/``true``/``yes``/``on`` (case-insensitive)."""
raw = os.environ.get(name)
if raw is None:
return default
return raw.strip().lower() in ("1", "true", "yes", "on")
@dataclass(frozen=True)
class AgentFeatureFlags:
"""Resolved feature-flag state for one agent build.
Constructed via :meth:`from_env`. The dataclass is frozen so it can be
safely shared across coroutines.
"""
# Master kill-switch — when true, every flag below resolves to False
# regardless of its env value. Used for rapid rollback.
disable_new_agent_stack: bool = False
# Agent quality — context budget, retry/limits, name-repair, doom-loop
enable_context_editing: bool = False
enable_compaction_v2: bool = False
enable_retry_after: bool = False
enable_model_fallback: bool = False
enable_model_call_limit: bool = False
enable_tool_call_limit: bool = False
enable_tool_call_repair: bool = False
enable_doom_loop: bool = (
False # Default OFF until UI handles permission='doom_loop'
)
# Safety — permissions, concurrency, tool-set narrowing
enable_permission: bool = False # Default OFF for first deploy
enable_busy_mutex: bool = False
enable_llm_tool_selector: bool = False # Default OFF — adds per-turn LLM cost
# Skills + subagents
enable_skills: bool = False
enable_specialized_subagents: bool = False
enable_kb_planner_runnable: bool = False
# Snapshot / revert
enable_action_log: bool = False
enable_revert_route: bool = (
False # Backend ships before UI; route returns 503 until this flips
)
# Plugins
enable_plugin_loader: bool = False
# Observability — OTel (orthogonal; also requires OTEL_EXPORTER_OTLP_ENDPOINT)
enable_otel: bool = False
@classmethod
def from_env(cls) -> AgentFeatureFlags:
"""Read flags from environment.
Master kill-switch is evaluated first; when set, all other flags
force to False.
"""
master_off = _env_bool("SURFSENSE_DISABLE_NEW_AGENT_STACK", False)
if master_off:
logger.info(
"SURFSENSE_DISABLE_NEW_AGENT_STACK is set: every new agent "
"middleware is forced OFF for this build."
)
return cls(disable_new_agent_stack=True)
return cls(
disable_new_agent_stack=False,
# Agent quality
enable_context_editing=_env_bool("SURFSENSE_ENABLE_CONTEXT_EDITING", False),
enable_compaction_v2=_env_bool("SURFSENSE_ENABLE_COMPACTION_V2", False),
enable_retry_after=_env_bool("SURFSENSE_ENABLE_RETRY_AFTER", False),
enable_model_fallback=_env_bool("SURFSENSE_ENABLE_MODEL_FALLBACK", False),
enable_model_call_limit=_env_bool(
"SURFSENSE_ENABLE_MODEL_CALL_LIMIT", False
),
enable_tool_call_limit=_env_bool("SURFSENSE_ENABLE_TOOL_CALL_LIMIT", False),
enable_tool_call_repair=_env_bool(
"SURFSENSE_ENABLE_TOOL_CALL_REPAIR", False
),
enable_doom_loop=_env_bool("SURFSENSE_ENABLE_DOOM_LOOP", False),
# Safety
enable_permission=_env_bool("SURFSENSE_ENABLE_PERMISSION", False),
enable_busy_mutex=_env_bool("SURFSENSE_ENABLE_BUSY_MUTEX", False),
enable_llm_tool_selector=_env_bool(
"SURFSENSE_ENABLE_LLM_TOOL_SELECTOR", False
),
# Skills + subagents
enable_skills=_env_bool("SURFSENSE_ENABLE_SKILLS", False),
enable_specialized_subagents=_env_bool(
"SURFSENSE_ENABLE_SPECIALIZED_SUBAGENTS", False
),
enable_kb_planner_runnable=_env_bool(
"SURFSENSE_ENABLE_KB_PLANNER_RUNNABLE", False
),
# Snapshot / revert
enable_action_log=_env_bool("SURFSENSE_ENABLE_ACTION_LOG", False),
enable_revert_route=_env_bool("SURFSENSE_ENABLE_REVERT_ROUTE", False),
# Plugins
enable_plugin_loader=_env_bool("SURFSENSE_ENABLE_PLUGIN_LOADER", False),
# Observability
enable_otel=_env_bool("SURFSENSE_ENABLE_OTEL", False),
)
def any_new_middleware_enabled(self) -> bool:
"""Return True if any new middleware flag is on."""
if self.disable_new_agent_stack:
return False
return any(
(
self.enable_context_editing,
self.enable_compaction_v2,
self.enable_retry_after,
self.enable_model_fallback,
self.enable_model_call_limit,
self.enable_tool_call_limit,
self.enable_tool_call_repair,
self.enable_doom_loop,
self.enable_permission,
self.enable_busy_mutex,
self.enable_llm_tool_selector,
self.enable_skills,
self.enable_specialized_subagents,
self.enable_kb_planner_runnable,
self.enable_action_log,
self.enable_revert_route,
self.enable_plugin_loader,
)
)
# Module-level cache. Read once at import time so the values are consistent
# across the process lifetime. Use ``reload_for_tests`` to reset in tests.
_FLAGS: AgentFeatureFlags | None = None
def get_flags() -> AgentFeatureFlags:
"""Return the resolved feature-flag state, caching on first call."""
global _FLAGS
if _FLAGS is None:
_FLAGS = AgentFeatureFlags.from_env()
return _FLAGS
def reload_for_tests() -> AgentFeatureFlags:
"""Force a fresh read from env. Tests should call this after monkeypatching env."""
global _FLAGS
_FLAGS = AgentFeatureFlags.from_env()
return _FLAGS
__all__ = [
"AgentFeatureFlags",
"get_flags",
"reload_for_tests",
]