mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-17 18:35:19 +02:00
multi_agent_chat: drop general_purpose subagent and dead permission plumbing
This commit is contained in:
parent
3fb1976886
commit
3f77c74daf
12 changed files with 9 additions and 307 deletions
|
|
@ -21,7 +21,6 @@ def build_registry_subagents_section(
|
|||
"\n<registry_subagents>\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"
|
||||
"</registry_subagents>\n"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
<tool_routing>
|
||||
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 `<registry_subagents>` (later in this
|
||||
prompt) cover calendar, mail, chat, tickets, third-party documents,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue