From 3f77c74daf52be006c8e615f9caf599854f03dac Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Tue, 12 May 2026 12:00:59 +0200 Subject: [PATCH] multi_agent_chat: drop general_purpose subagent and dead permission plumbing --- .../builder/sections/registry_subagents.py | 1 - .../markdown/main_agent_tool_routing.md | 5 +- .../middleware/shared/file_intent.py | 11 -- .../middleware/shared/permissions/__init__.py | 12 -- .../middleware/shared/permissions/context.py | 107 ------------------ .../shared/permissions/middleware.py | 10 -- .../middleware/shared/resilience/__init__.py | 2 +- .../multi_agent_chat/middleware/stack.py | 35 +----- .../middleware/subagent/extras.py | 10 +- .../builtins/general_purpose/__init__.py | 0 .../builtins/general_purpose/agent.py | 105 ----------------- .../builtins/knowledge_base/agent.py | 18 +-- 12 files changed, 9 insertions(+), 307 deletions(-) delete mode 100644 surfsense_backend/app/agents/multi_agent_chat/middleware/shared/file_intent.py delete mode 100644 surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/__init__.py delete mode 100644 surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/context.py delete mode 100644 surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/middleware.py delete mode 100644 surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/general_purpose/__init__.py delete mode 100644 surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/general_purpose/agent.py diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/registry_subagents.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/registry_subagents.py index 90f4cc2d6..191e86d33 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/registry_subagents.py +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/registry_subagents.py @@ -21,7 +21,6 @@ def build_registry_subagents_section( "\n\n" "These specialists are registered for **task** (routes without a matching connector are omitted).\n" f"{bullets}\n" - "The runtime may also offer a general-purpose **task** helper with your tools in a separate context.\n" "Pick the specialist by **name**. Put full instructions in the task prompt; they do not see this thread.\n" "\n" ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/markdown/main_agent_tool_routing.md b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/markdown/main_agent_tool_routing.md index 5b0fbea89..a3f0f7305 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/markdown/main_agent_tool_routing.md +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/markdown/main_agent_tool_routing.md @@ -1,12 +1,11 @@ -Use **task** for any work beyond your direct SurfSense tools. Two builtin -specialists are always available: +Use **task** for any work beyond your direct SurfSense tools. The +**knowledge_base** specialist is always available: - **knowledge_base** — owns the user's workspace (documents and folders). Route here whenever the user wants to create, read, edit, search, organise, or remove a document or folder (e.g. *"save these notes to my KB"*, *"find my Q2 roadmap"*, *"rename this folder"*). -- **general_purpose** — ad-hoc multi-step work that doesn't fit any specialist. The connector specialists listed in `` (later in this prompt) cover calendar, mail, chat, tickets, third-party documents, diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/file_intent.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/file_intent.py deleted file mode 100644 index 5ff65aa12..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/file_intent.py +++ /dev/null @@ -1,11 +0,0 @@ -"""File-intent classifier that gates strict write contracts.""" - -from __future__ import annotations - -from langchain_core.language_models import BaseChatModel - -from app.agents.new_chat.middleware import FileIntentMiddleware - - -def build_file_intent_mw(llm: BaseChatModel) -> FileIntentMiddleware: - return FileIntentMiddleware(llm=llm) diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/__init__.py deleted file mode 100644 index 4f2228170..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -"""Permission rulesets fanned out to parent / general-purpose / subagent stacks.""" - -from __future__ import annotations - -from .context import PermissionContext, build_permission_context -from .middleware import build_full_permission_mw - -__all__ = [ - "PermissionContext", - "build_full_permission_mw", - "build_permission_context", -] diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/context.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/context.py deleted file mode 100644 index e121421a0..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/context.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Derive shared permission context once; fan out to all three stack layers. - -The context carries: -- ``rulesets``: full ask/deny/allow rules for the main-agent permission middleware. -- ``general_purpose_interrupt_on``: ``ask`` rules mirrored as deepagents - ``interrupt_on`` so HITL still triggers from inside ``task`` runs (subagents - bypass the main-agent permission middleware). -- ``subagent_deny_mw``: a deny-only ``PermissionMiddleware`` instance shared - across the general-purpose and registry subagent stacks. -""" - -from __future__ import annotations - -from collections.abc import Sequence -from dataclasses import dataclass - -from langchain_core.tools import BaseTool - -from app.agents.new_chat.feature_flags import AgentFeatureFlags -from app.agents.new_chat.filesystem_selection import FilesystemMode -from app.agents.new_chat.middleware import PermissionMiddleware -from app.agents.new_chat.permissions import Rule, Ruleset -from app.agents.new_chat.tools.registry import BUILTIN_TOOLS - -from ..flags import enabled - - -@dataclass(frozen=True) -class PermissionContext: - rulesets: list[Ruleset] - general_purpose_interrupt_on: dict[str, bool] - subagent_deny_mw: PermissionMiddleware | None - - -def build_permission_context( - *, - flags: AgentFeatureFlags, - filesystem_mode: FilesystemMode, - tools: Sequence[BaseTool], - available_connectors: list[str] | None, -) -> PermissionContext: - is_desktop_fs = filesystem_mode == FilesystemMode.DESKTOP_LOCAL_FOLDER - permission_enabled = enabled(flags, "enable_permission") - - rulesets: list[Ruleset] = [] - if permission_enabled or is_desktop_fs: - rulesets.append( - Ruleset( - rules=[Rule(permission="*", pattern="*", action="allow")], - origin="surfsense_defaults", - ) - ) - if is_desktop_fs: - rulesets.append( - Ruleset( - rules=[ - Rule(permission="rm", pattern="*", action="ask"), - Rule(permission="rmdir", pattern="*", action="ask"), - Rule(permission="move_file", pattern="*", action="ask"), - Rule(permission="edit_file", pattern="*", action="ask"), - Rule(permission="write_file", pattern="*", action="ask"), - ], - origin="desktop_safety", - ) - ) - - tool_names_in_use = {t.name for t in tools} - - if permission_enabled: - available_set = set(available_connectors or []) - synthesized: list[Rule] = [] - for tool_def in BUILTIN_TOOLS: - if tool_def.name not in tool_names_in_use: - continue - rc = tool_def.required_connector - if rc and rc not in available_set: - synthesized.append( - Rule(permission=tool_def.name, pattern="*", action="deny") - ) - if synthesized: - rulesets.append(Ruleset(rules=synthesized, origin="connector_synthesized")) - - general_purpose_interrupt_on: dict[str, bool] = { - rule.permission: True - for rs in rulesets - for rule in rs.rules - if rule.action == "ask" and rule.permission in tool_names_in_use - } - - deny_rulesets = [ - Ruleset( - rules=[r for r in rs.rules if r.action == "deny"], - origin=rs.origin, - ) - for rs in rulesets - ] - deny_rulesets = [rs for rs in deny_rulesets if rs.rules] - - subagent_deny_mw: PermissionMiddleware | None = ( - PermissionMiddleware(rulesets=deny_rulesets) if deny_rulesets else None - ) - - return PermissionContext( - rulesets=rulesets, - general_purpose_interrupt_on=general_purpose_interrupt_on, - subagent_deny_mw=subagent_deny_mw, - ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/middleware.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/middleware.py deleted file mode 100644 index 704a26fb3..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/permissions/middleware.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Main-agent permission middleware (full ask/deny/allow rules).""" - -from __future__ import annotations - -from app.agents.new_chat.middleware import PermissionMiddleware -from app.agents.new_chat.permissions import Ruleset - - -def build_full_permission_mw(rulesets: list[Ruleset]) -> PermissionMiddleware | None: - return PermissionMiddleware(rulesets=rulesets) if rulesets else None diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/resilience/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/resilience/__init__.py index 92596b771..377f93964 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/resilience/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/resilience/__init__.py @@ -1,4 +1,4 @@ -"""Resilience middleware shared as the same instances across parent / general-purpose / registry.""" +"""Resilience middleware shared as the same instances across parent / registry.""" from __future__ import annotations diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/stack.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/stack.py index 754f4d1b8..dc9c27b68 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/stack.py +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/stack.py @@ -23,9 +23,6 @@ from app.agents.multi_agent_chat.subagents import ( build_subagents, get_subagents_to_exclude, ) -from app.agents.multi_agent_chat.subagents.builtins.general_purpose.agent import ( - build_subagent as build_general_purpose_subagent, -) from app.agents.multi_agent_chat.subagents.builtins.knowledge_base.agent import ( build_subagent as build_knowledge_base_subagent, ) @@ -56,10 +53,6 @@ from .shared.compaction import build_compaction_mw from .shared.kb_context_projection import build_kb_context_projection_mw from .shared.memory import build_memory_mw from .shared.patch_tool_calls import build_patch_tool_calls_mw -from .shared.permissions import ( - build_full_permission_mw, - build_permission_context, -) from .shared.resilience import build_resilience_bundle from .shared.todos import build_todos_mw from .subagent.extras import build_subagent_extras @@ -87,34 +80,14 @@ def build_main_agent_deepagent_middleware( disabled_tools: list[str] | None = None, ) -> list[Any]: """Ordered middleware for ``create_agent`` (None entries already stripped).""" - permissions = build_permission_context( - flags=flags, - filesystem_mode=filesystem_mode, - tools=tools, - available_connectors=available_connectors, - ) resilience = build_resilience_bundle(flags) - # Single instance threaded into both the main-agent stack and the general-purpose subagent. memory_mw = build_memory_mw( user_id=user_id, search_space_id=search_space_id, visibility=visibility, ) - general_purpose_subagent = build_general_purpose_subagent( - llm=llm, - tools=tools, - backend_resolver=backend_resolver, - filesystem_mode=filesystem_mode, - search_space_id=search_space_id, - user_id=user_id, - thread_id=thread_id, - permissions=permissions, - resilience=resilience, - memory_mw=memory_mw, - ) - knowledge_base_subagent = build_knowledge_base_subagent( llm=llm, backend_resolver=backend_resolver, @@ -122,14 +95,12 @@ def build_main_agent_deepagent_middleware( search_space_id=search_space_id, user_id=user_id, thread_id=thread_id, - permissions=permissions, resilience=resilience, ) subagents_registry: list[SubAgent] = [] try: subagent_extras = build_subagent_extras( - permissions=permissions, resilience=resilience, ) subagents_registry = build_subagents( @@ -145,15 +116,14 @@ def build_main_agent_deepagent_middleware( [s["name"] for s in subagents_registry], ) except Exception: - # Degrade to general-purpose-only rather than aborting the turn: + # Degrade to KB-only rather than aborting the turn: # one bad subagent dep should not deny the user a response. logging.exception( - "Subagents registry build failed; falling back to general-purpose only" + "Subagents registry build failed; falling back to knowledge_base only" ) subagents_registry = [] subagents: list[SubAgent] = [ - general_purpose_subagent, knowledge_base_subagent, *subagents_registry, ] @@ -209,7 +179,6 @@ def build_main_agent_deepagent_middleware( resilience.retry, resilience.fallback, build_repair_mw(flags=flags, tools=tools), - build_full_permission_mw(permissions.rulesets), build_doom_loop_mw(flags), build_action_log_mw( flags=flags, diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/subagent/extras.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/subagent/extras.py index 46dca8a81..687f7d36c 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/subagent/extras.py +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/subagent/extras.py @@ -2,27 +2,23 @@ Registry subagents are scoped to one domain (deliverables, research, memory, connectors, MCP) and never read or write the SurfSense filesystem — that -capability belongs to the main agent and is delegated to the general-purpose -subagent as an escape hatch. Keeping FS off the registry stacks avoids -polluting their tool surface with FS tools they never act on. +capability belongs to the ``knowledge_base`` subagent. Keeping FS off the +registry stacks avoids polluting their tool surface with FS tools they +never act on. """ from __future__ import annotations from typing import Any -from ..shared.permissions import PermissionContext from ..shared.resilience import ResilienceBundle from ..shared.todos import build_todos_mw def build_subagent_extras( *, - permissions: PermissionContext, resilience: ResilienceBundle, ) -> list[Any]: extras: list[Any] = [build_todos_mw()] - if permissions.subagent_deny_mw is not None: - extras.append(permissions.subagent_deny_mw) extras.extend(resilience.as_list()) return extras diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/general_purpose/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/general_purpose/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/general_purpose/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/general_purpose/agent.py deleted file mode 100644 index 1c3c44f12..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/general_purpose/agent.py +++ /dev/null @@ -1,105 +0,0 @@ -"""General-purpose subagent for the multi-agent main agent.""" - -from __future__ import annotations - -from collections.abc import Sequence -from typing import Any, cast - -from deepagents import SubAgent -from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware -from deepagents.middleware.subagents import GENERAL_PURPOSE_SUBAGENT -from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware -from langchain_core.language_models import BaseChatModel -from langchain_core.tools import BaseTool - -from app.agents.multi_agent_chat.middleware.shared.anthropic_cache import ( - build_anthropic_cache_mw, -) -from app.agents.multi_agent_chat.middleware.shared.compaction import ( - build_compaction_mw, -) -from app.agents.multi_agent_chat.middleware.shared.file_intent import ( - build_file_intent_mw, -) -from app.agents.multi_agent_chat.middleware.shared.filesystem import ( - build_filesystem_mw, -) -from app.agents.multi_agent_chat.middleware.shared.patch_tool_calls import ( - build_patch_tool_calls_mw, -) -from app.agents.multi_agent_chat.middleware.shared.permissions import ( - PermissionContext, -) -from app.agents.multi_agent_chat.middleware.shared.resilience import ( - ResilienceBundle, -) -from app.agents.multi_agent_chat.middleware.shared.todos import build_todos_mw -from app.agents.new_chat.filesystem_selection import FilesystemMode -from app.agents.new_chat.middleware import MemoryInjectionMiddleware - -NAME = "general-purpose" - - -def build_subagent( - *, - llm: BaseChatModel, - tools: Sequence[BaseTool], - backend_resolver: Any, - filesystem_mode: FilesystemMode, - search_space_id: int, - user_id: str | None, - thread_id: int | None, - permissions: PermissionContext, - resilience: ResilienceBundle, - memory_mw: MemoryInjectionMiddleware, -) -> SubAgent: - """Deny + resilience inserts encapsulated here so the orchestrator never mutates the list.""" - middleware: list[Any] = [ - build_todos_mw(), - memory_mw, - build_file_intent_mw(llm), - build_filesystem_mw( - backend_resolver=backend_resolver, - filesystem_mode=filesystem_mode, - search_space_id=search_space_id, - user_id=user_id, - thread_id=thread_id, - ), - build_compaction_mw(llm), - build_patch_tool_calls_mw(), - build_anthropic_cache_mw(), - ] - - if permissions.subagent_deny_mw is not None: - patch_idx = next( - ( - i - for i, m in enumerate(middleware) - if isinstance(m, PatchToolCallsMiddleware) - ), - len(middleware), - ) - middleware.insert(patch_idx, permissions.subagent_deny_mw) - - resilience_mws = resilience.as_list() - if resilience_mws: - cache_idx = next( - ( - i - for i, m in enumerate(middleware) - if isinstance(m, AnthropicPromptCachingMiddleware) - ), - len(middleware), - ) - for offset, mw in enumerate(resilience_mws): - middleware.insert(cache_idx + offset, mw) - - spec: dict[str, Any] = { - **GENERAL_PURPOSE_SUBAGENT, - "model": llm, - "tools": tools, - "middleware": middleware, - } - if permissions.general_purpose_interrupt_on: - spec["interrupt_on"] = permissions.general_purpose_interrupt_on - return cast(SubAgent, spec) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py index 52b2c97c4..bf6ec6753 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/knowledge_base/agent.py @@ -11,7 +11,6 @@ from __future__ import annotations from typing import Any, cast from deepagents import SubAgent -from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware from langchain_core.language_models import BaseChatModel @@ -30,9 +29,6 @@ from app.agents.multi_agent_chat.middleware.shared.kb_context_projection import from app.agents.multi_agent_chat.middleware.shared.patch_tool_calls import ( build_patch_tool_calls_mw, ) -from app.agents.multi_agent_chat.middleware.shared.permissions import ( - PermissionContext, -) from app.agents.multi_agent_chat.middleware.shared.resilience import ( ResilienceBundle, ) @@ -55,10 +51,9 @@ def build_subagent( search_space_id: int, user_id: str | None, thread_id: int | None, - permissions: PermissionContext, resilience: ResilienceBundle, ) -> SubAgent: - """Deny + resilience inserts encapsulated here so the orchestrator never mutates the list.""" + """Resilience inserts encapsulated here so the orchestrator never mutates the list.""" description = read_md_file(__package__, "description").strip() if not description: description = ( @@ -86,17 +81,6 @@ def build_subagent( build_anthropic_cache_mw(), ] - if permissions.subagent_deny_mw is not None: - patch_idx = next( - ( - i - for i, m in enumerate(middleware) - if isinstance(m, PatchToolCallsMiddleware) - ), - len(middleware), - ) - middleware.insert(patch_idx, permissions.subagent_deny_mw) - resilience_mws = resilience.as_list() if resilience_mws: cache_idx = next(