mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-03 21:02:40 +02:00
198 lines
7 KiB
Python
198 lines
7 KiB
Python
"""
|
|
Feature flags for the SurfSense new_chat agent stack.
|
|
|
|
These flags control rollout of OpenCode-pattern middleware ported into
|
|
SurfSense. 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
|
|
|
|
# Tier 1 — Agent quality
|
|
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'
|
|
)
|
|
|
|
# Tier 2 — Safety
|
|
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
|
|
|
|
# Tier 4 — Skills + subagents
|
|
enable_skills: bool = False
|
|
enable_specialized_subagents: bool = False
|
|
enable_kb_planner_runnable: bool = False
|
|
|
|
# Tier 5 — Snapshot / revert
|
|
enable_action_log: bool = False
|
|
enable_revert_route: bool = (
|
|
False # Backend ships before UI; route returns 503 until this flips
|
|
)
|
|
|
|
# Tier 6 — Plugins
|
|
enable_plugin_loader: bool = False
|
|
|
|
# Tier 3b — 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,
|
|
# Tier 1
|
|
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),
|
|
# Tier 2
|
|
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
|
|
),
|
|
# Tier 4
|
|
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
|
|
),
|
|
# Tier 5
|
|
enable_action_log=_env_bool("SURFSENSE_ENABLE_ACTION_LOG", False),
|
|
enable_revert_route=_env_bool("SURFSENSE_ENABLE_REVERT_ROUTE", False),
|
|
# Tier 6
|
|
enable_plugin_loader=_env_bool("SURFSENSE_ENABLE_PLUGIN_LOADER", False),
|
|
# Tier 3b
|
|
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",
|
|
]
|