diff --git a/surfsense_backend/app/agents/multi_agent_chat/constants.py b/surfsense_backend/app/agents/multi_agent_chat/constants.py index 972677502..7e4061813 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/constants.py +++ b/surfsense_backend/app/agents/multi_agent_chat/constants.py @@ -25,6 +25,7 @@ CONNECTOR_TYPE_TO_CONNECTOR_AGENT_MAPS: dict[str, str] = { SUBAGENT_TO_REQUIRED_CONNECTOR_MAP: dict[str, frozenset[str]] = { "deliverables": frozenset(), + "knowledge_base": frozenset(), "airtable": frozenset({"AIRTABLE_CONNECTOR"}), "calendar": frozenset({"GOOGLE_CALENDAR_CONNECTOR"}), "clickup": frozenset({"CLICKUP_CONNECTOR"}), diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/graph/compile_graph_sync.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/graph/compile_graph_sync.py index 4ed94bf7b..86c2ac9e8 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/graph/compile_graph_sync.py +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/graph/compile_graph_sync.py @@ -11,7 +11,7 @@ from langchain_core.language_models import BaseChatModel from langchain_core.tools import BaseTool from langgraph.types import Checkpointer -from app.agents.multi_agent_chat.middleware import ( +from app.agents.multi_agent_chat.middleware.stack import ( build_main_agent_deepagent_middleware, ) from app.agents.multi_agent_chat.subagents.shared.permissions import ( diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/runtime/factory.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/runtime/factory.py index cb6410acb..8988f0296 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/runtime/factory.py +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/runtime/factory.py @@ -7,7 +7,6 @@ import time from collections.abc import Sequence from typing import Any -from deepagents.graph import BASE_AGENT_PROMPT from langchain_core.language_models import BaseChatModel from langchain_core.tools import BaseTool from langgraph.types import Checkpointer @@ -218,7 +217,7 @@ async def create_multi_agent_chat_deep_agent( "[create_agent] System prompt built in %.3fs", time.perf_counter() - _t0 ) - final_system_prompt = system_prompt + "\n\n" + BASE_AGENT_PROMPT + final_system_prompt = system_prompt config_id = agent_config.config_id if agent_config is not None else None diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/compose.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/compose.py index cfac0092e..c21e69fcb 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/compose.py +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/compose.py @@ -41,6 +41,7 @@ from .sections.tools import build_tools_section def build_main_agent_system_prompt( *, + registry_subagent_prompt_lines: list[tuple[str, str]], today: datetime | None = None, thread_visibility: ChatVisibility | None = None, enabled_tool_names: set[str] | None = None, @@ -49,7 +50,6 @@ def build_main_agent_system_prompt( use_default_system_instructions: bool = True, citations_enabled: bool = True, model_name: str | None = None, # noqa: ARG001 — kept for caller compatibility - registry_subagent_prompt_lines: list[tuple[str, str]] | None = None, ) -> str: resolved_today = (today or datetime.now(UTC)).astimezone(UTC).date().isoformat() visibility = thread_visibility or ChatVisibility.PRIVATE diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/specialists.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/specialists.py index 7bc106e1e..a3455bd83 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/specialists.py +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/specialists.py @@ -1,18 +1,15 @@ -"""```` section — live ``task`` roster for this workspace.""" +"""```` section — live ``task`` roster for this workspace. + +The roster is non-empty by contract: ``deliverables`` and ``knowledge_base`` +both declare ``frozenset()`` in ``SUBAGENT_TO_REQUIRED_CONNECTOR_MAP``, so +they survive every connector-based exclusion pass. +""" from __future__ import annotations def build_specialists_section( - specialist_lines: list[tuple[str, str]] | None, + specialist_lines: list[tuple[str, str]], ) -> str: - if specialist_lines is None: - return "" - if not specialist_lines: - return ( - "\n\n" - "No specialists are available for `task` in this workspace.\n" - "\n" - ) bullets = "\n".join(f"- **{name}** — {desc}" for name, desc in specialist_lines) return f"\n\n{bullets}\n\n" diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/tools.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/tools.py index bc4d48ef5..caf741d45 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/tools.py +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/sections/tools.py @@ -1,4 +1,4 @@ -"""Main-agent ```` block (memory + research builtins only; see ``main_agent.tools``).""" +"""Main-agent ```` block (memory + research builtins + ``task``).""" from __future__ import annotations diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/tool_instruction_block.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/tool_instruction_block.py index be789140d..cbc8728ca 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/tool_instruction_block.py +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/builder/tool_instruction_block.py @@ -1,8 +1,7 @@ """Compose the ```` block from per-tool vertical-slice folders. Each tool lives in ``prompts/tools//`` with ``description.md`` and an -inline-rendered ``example.md``. Visibility variants (currently only -``update_memory``) live in ``prompts/tools//{private,team}/``. +``example.md``. Visibility variants live in ``{private,team}/`` subfolders. """ from __future__ import annotations @@ -31,6 +30,8 @@ def build_tools_instruction_block( enabled_tool_names: set[str] | None, disabled_tool_names: set[str] | None, ) -> str: + """Render ````. ``task`` is always included: at least ``deliverables`` + and ``knowledge_base`` are always in ```` (see constants).""" variant = "team" if visibility == ChatVisibility.SEARCH_SPACE else "private" parts: list[str] = ["\n\n"] @@ -51,6 +52,14 @@ def build_tools_instruction_block( parts.append("\n" + example + "\n") parts.append("\n") + task_description = read_prompt_md("tools/task/description.md") + task_example = read_prompt_md("tools/task/example.md") + if task_description: + parts.append(task_description + "\n") + if task_example: + parts.append("\n" + task_example + "\n") + parts.append("\n") + known_disabled = ( set(disabled_tool_names) & set(MAIN_AGENT_SURFSENSE_TOOL_NAMES_ORDERED) if disabled_tool_names diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/__init__.py new file mode 100644 index 000000000..5eb371b75 --- /dev/null +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/__init__.py @@ -0,0 +1 @@ +"""``task`` — description + few-shot examples for the specialist-delegation tool.""" diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/description.md b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/description.md new file mode 100644 index 000000000..f559b1828 --- /dev/null +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/description.md @@ -0,0 +1,19 @@ +- `task` — Invoke a specialist subagent. + - Specialists own workspace knowledge-base operations and connected + third-party services (Slack, Notion, Jira, Gmail, etc.). See + `` for the live roster. + - Each subagent runs in isolation with its own tool stack and context, + and returns a single synthesized result. + - Args: + - `subagent_type` — name of the specialist to invoke (must match an + entry in ``). + - `description` — the FULL task prompt. The specialist cannot see this + thread, so include all context and constraints, plus what you need + back. The specialist will respond in its own format — don't dictate + one. + - Rules: + - One `task` call per turn. Bundle related work for the same specialist + into one invocation; the parent graph cannot coordinate human + approvals across parallel subagents. + - Don't claim to already know what a specialist's source contains; + invoke it and use what it returns. diff --git a/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/example.md b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/example.md new file mode 100644 index 000000000..87e5e1b6d --- /dev/null +++ b/surfsense_backend/app/agents/multi_agent_chat/main_agent/system_prompt/prompts/tools/task/example.md @@ -0,0 +1,20 @@ + +user: "Save these meeting notes to my KB: …" +→ task(subagent_type="knowledge_base", description="Save the notes below to + a new document under /documents/notes/. Pick a sensible title and folder; + tell me the path you used.\n\n") + + + +user: "What did Maya say about the Q2 roadmap in Slack last week?" +→ task(subagent_type="slack", description="Find messages from Maya about + the Q2 roadmap from the past week. Return the most relevant quotes with + channel and timestamp.") + + + +user: "Find my Q2 roadmap and summarise the milestones." +→ task(subagent_type="knowledge_base", description="Locate the Q2 roadmap + document under /documents and summarise its milestones. Use glob or grep + if the path isn't obvious from the workspace tree.") + diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/__init__.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/__init__.py index e6eed9fbe..e69de29bb 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/__init__.py +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/__init__.py @@ -1,7 +0,0 @@ -"""Multi-agent middleware stack assembly.""" - -from __future__ import annotations - -from .stack import build_main_agent_deepagent_middleware - -__all__ = ["build_main_agent_deepagent_middleware"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/checkpointed_subagent_middleware/task_description.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/checkpointed_subagent_middleware/task_description.py new file mode 100644 index 000000000..73afa6823 --- /dev/null +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/main_agent/checkpointed_subagent_middleware/task_description.py @@ -0,0 +1,15 @@ +"""Schema-level description for the ``task`` tool. + +Loaded from ``prompts/tools/task/description.md`` so the tool-schema text +and the ```` block render from the same source. +""" + +from __future__ import annotations + +from app.agents.multi_agent_chat.main_agent.system_prompt.builder.load_md import ( + read_prompt_md, +) + +TASK_TOOL_DESCRIPTION: str = read_prompt_md("tools/task/description.md") + +__all__ = ["TASK_TOOL_DESCRIPTION"] 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 377f93964..9f26ffe49 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 @@ -2,6 +2,6 @@ from __future__ import annotations -from .bundle import ResilienceBundle, build_resilience_bundle +from .bundle import ResilienceMiddlewares, build_resilience_middlewares -__all__ = ["ResilienceBundle", "build_resilience_bundle"] +__all__ = ["ResilienceMiddlewares", "build_resilience_middlewares"] diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/resilience/bundle.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/resilience/bundle.py index 45f76a6f3..111244784 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/resilience/bundle.py +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/shared/resilience/bundle.py @@ -23,7 +23,9 @@ from .tool_call_limit import build_tool_call_limit_mw @dataclass(frozen=True) -class ResilienceBundle: +class ResilienceMiddlewares: + """The four resilience middleware instances, any of which may be ``None`` when disabled by flags.""" + retry: RetryAfterMiddleware | None fallback: ScopedModelFallbackMiddleware | None model_call_limit: ModelCallLimitMiddleware | None @@ -42,8 +44,8 @@ class ResilienceBundle: ] -def build_resilience_bundle(flags: AgentFeatureFlags) -> ResilienceBundle: - return ResilienceBundle( +def build_resilience_middlewares(flags: AgentFeatureFlags) -> ResilienceMiddlewares: + return ResilienceMiddlewares( retry=build_retry_mw(flags), fallback=build_fallback_mw(flags), model_call_limit=build_model_call_limit_mw(flags), 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 dc9c27b68..db50abffb 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.knowledge_base.agent import ( - build_subagent as build_knowledge_base_subagent, -) from app.agents.multi_agent_chat.subagents.shared.permissions import ToolsPermissions from app.agents.new_chat.feature_flags import AgentFeatureFlags from app.agents.new_chat.filesystem_selection import FilesystemMode @@ -37,6 +34,9 @@ from .main_agent.busy_mutex import build_busy_mutex_mw from .main_agent.checkpointed_subagent_middleware import ( SurfSenseCheckpointedSubAgentMiddleware, ) +from .main_agent.checkpointed_subagent_middleware.task_description import ( + TASK_TOOL_DESCRIPTION, +) from .main_agent.context_editing import build_context_editing_mw from .main_agent.dedup_hitl import build_dedup_hitl_mw from .main_agent.doom_loop import build_doom_loop_mw @@ -53,9 +53,9 @@ 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.resilience import build_resilience_bundle +from .shared.resilience import build_resilience_middlewares from .shared.todos import build_todos_mw -from .subagent.extras import build_subagent_extras +from .subagent.middleware_stack import build_subagent_middleware_stack def build_main_agent_deepagent_middleware( @@ -80,7 +80,7 @@ def build_main_agent_deepagent_middleware( disabled_tools: list[str] | None = None, ) -> list[Any]: """Ordered middleware for ``create_agent`` (None entries already stripped).""" - resilience = build_resilience_bundle(flags) + resilience = build_resilience_middlewares(flags) memory_mw = build_memory_mw( user_id=user_id, @@ -88,45 +88,21 @@ def build_main_agent_deepagent_middleware( visibility=visibility, ) - knowledge_base_subagent = build_knowledge_base_subagent( - llm=llm, - backend_resolver=backend_resolver, - filesystem_mode=filesystem_mode, - search_space_id=search_space_id, - user_id=user_id, - thread_id=thread_id, - resilience=resilience, + subagent_dependencies = { + **subagent_dependencies, + "backend_resolver": backend_resolver, + "filesystem_mode": filesystem_mode, + } + + subagents: list[SubAgent] = build_subagents( + dependencies=subagent_dependencies, + model=llm, + middleware_stack=build_subagent_middleware_stack(resilience=resilience), + mcp_tools_by_agent=mcp_tools_by_agent or {}, + exclude=get_subagents_to_exclude(available_connectors), + disabled_tools=disabled_tools, ) - - subagents_registry: list[SubAgent] = [] - try: - subagent_extras = build_subagent_extras( - resilience=resilience, - ) - subagents_registry = build_subagents( - dependencies=subagent_dependencies, - model=llm, - extra_middleware=subagent_extras, - mcp_tools_by_agent=mcp_tools_by_agent or {}, - exclude=get_subagents_to_exclude(available_connectors), - disabled_tools=disabled_tools, - ) - logging.debug( - "Subagents registry: %s", - [s["name"] for s in subagents_registry], - ) - except Exception: - # 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 knowledge_base only" - ) - subagents_registry = [] - - subagents: list[SubAgent] = [ - knowledge_base_subagent, - *subagents_registry, - ] + logging.debug("Subagents registry: %s", [s["name"] for s in subagents]) stack: list[Any] = [ build_busy_mutex_mw(flags), @@ -165,6 +141,8 @@ def build_main_agent_deepagent_middleware( checkpointer=checkpointer, backend=StateBackend, subagents=subagents, + system_prompt=None, + task_description=TASK_TOOL_DESCRIPTION, ), resilience.model_call_limit, resilience.tool_call_limit, 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 deleted file mode 100644 index 687f7d36c..000000000 --- a/surfsense_backend/app/agents/multi_agent_chat/middleware/subagent/extras.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Extra middleware threaded into every registry subagent's stack. - -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 ``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.resilience import ResilienceBundle -from ..shared.todos import build_todos_mw - - -def build_subagent_extras( - *, - resilience: ResilienceBundle, -) -> list[Any]: - extras: list[Any] = [build_todos_mw()] - extras.extend(resilience.as_list()) - return extras diff --git a/surfsense_backend/app/agents/multi_agent_chat/middleware/subagent/middleware_stack.py b/surfsense_backend/app/agents/multi_agent_chat/middleware/subagent/middleware_stack.py new file mode 100644 index 000000000..9889e629a --- /dev/null +++ b/surfsense_backend/app/agents/multi_agent_chat/middleware/subagent/middleware_stack.py @@ -0,0 +1,30 @@ +"""Shared middleware stack threaded into every subagent. + +Mirrors ``middleware/stack.py`` (the orchestrator's middleware stack) but +exposes its contents as a dict keyed by purpose so specialists can pick +the entries they need and decide ordering. The default consumer +(``pack_subagent``) prepends every non-``None`` value in insertion order. + +Registry subagents never touch the SurfSense filesystem — that capability +belongs to ``knowledge_base`` — so no FS middleware is exposed here. +""" + +from __future__ import annotations + +from typing import Any + +from ..shared.resilience import ResilienceMiddlewares +from ..shared.todos import build_todos_mw + + +def build_subagent_middleware_stack( + *, + resilience: ResilienceMiddlewares, +) -> dict[str, Any]: + return { + "todos": build_todos_mw(), + "retry": resilience.retry, + "fallback": resilience.fallback, + "model_call_limit": resilience.model_call_limit, + "tool_call_limit": resilience.tool_call_limit, + } diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/agent.py index 0f7070645..0baa6714f 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/deliverables/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) 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 bf6ec6753..9f8775284 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 @@ -1,9 +1,12 @@ """`knowledge_base` route: ``SubAgent`` spec for the SurfSense KB specialist. -The KB subagent owns the `/documents/` workspace: reading, writing, editing, -searching, and organising user documents. It shares the orchestrator's -``workspace_tree_text`` and ``kb_priority`` via state and re-emits them as -SystemMessages through the projection middleware (no extra DB / LLM calls). +Owns the ``/documents/`` workspace (read, write, edit, search, organise) +and shares the orchestrator's ``workspace_tree_text`` and ``kb_priority`` +via state. KB conforms to :class:`SubagentBuilder` but composes its +middleware list itself: it picks individual entries from +``middleware_stack`` by key so resilience lands just outside the +Anthropic cache (inside the filesystem and projection middlewares), +which a flat prepend can't satisfy. """ from __future__ import annotations @@ -11,7 +14,6 @@ from __future__ import annotations from typing import Any, cast from deepagents import SubAgent -from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware from langchain_core.language_models import BaseChatModel from app.agents.multi_agent_chat.middleware.shared.anthropic_cache import ( @@ -29,13 +31,12 @@ 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.resilience import ( - ResilienceBundle, -) -from app.agents.multi_agent_chat.middleware.shared.todos import build_todos_mw from app.agents.multi_agent_chat.subagents.shared.md_file_reader import ( read_md_file, ) +from app.agents.multi_agent_chat.subagents.shared.permissions import ( + ToolsPermissions, +) from app.agents.new_chat.filesystem_selection import FilesystemMode from .tools.index import destructive_fs_interrupt_on @@ -45,20 +46,19 @@ NAME = "knowledge_base" def build_subagent( *, - llm: BaseChatModel, - backend_resolver: Any, - filesystem_mode: FilesystemMode, - search_space_id: int, - user_id: str | None, - thread_id: int | None, - resilience: ResilienceBundle, + dependencies: dict[str, Any], + model: BaseChatModel | None = None, + middleware_stack: dict[str, Any] | None = None, + extra_tools_bucket: ToolsPermissions | None = None, # noqa: ARG001 — KB ships fixed tools ) -> SubAgent: - """Resilience inserts encapsulated here so the orchestrator never mutates the list.""" - description = read_md_file(__package__, "description").strip() - if not description: - description = ( - "Handles knowledge-base reads, writes, edits, and organisation." - ) + """Conforms to :class:`SubagentBuilder`; KB splices the shared stack itself.""" + llm = model if model is not None else dependencies["llm"] + filesystem_mode: FilesystemMode = dependencies["filesystem_mode"] + mws = middleware_stack or {} + + description = read_md_file(__package__, "description").strip() or ( + "Handles knowledge-base reads, writes, edits, and organisation." + ) prompt_stem = ( "system_prompt_cloud" if filesystem_mode == FilesystemMode.CLOUD @@ -66,40 +66,39 @@ def build_subagent( ) system_prompt = read_md_file(__package__, prompt_stem).strip() + resilience_mws = [ + m + for m in ( + mws.get("retry"), + mws.get("fallback"), + mws.get("model_call_limit"), + mws.get("tool_call_limit"), + ) + if m is not None + ] + middleware: list[Any] = [ - build_todos_mw(), + mws["todos"], build_kb_context_projection_mw(), build_filesystem_mw( - backend_resolver=backend_resolver, + backend_resolver=dependencies["backend_resolver"], filesystem_mode=filesystem_mode, - search_space_id=search_space_id, - user_id=user_id, - thread_id=thread_id, + search_space_id=dependencies["search_space_id"], + user_id=dependencies.get("user_id"), + thread_id=dependencies.get("thread_id"), ), build_compaction_mw(llm), build_patch_tool_calls_mw(), + *resilience_mws, build_anthropic_cache_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] = { "name": NAME, "description": description, "system_prompt": system_prompt, "model": llm, - "tools": [], + "tools": [], # KB virtual FS tools are injected at runtime by SurfSenseFilesystemMiddleware "middleware": middleware, "interrupt_on": destructive_fs_interrupt_on(), } diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/agent.py index 0afe207ce..2cd9e70a1 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/memory/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/agent.py index 1b7998153..d38ab2af3 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/builtins/research/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/agent.py index 7b78f4565..c186684ab 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/airtable/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/agent.py index 42ccba213..0f00c68e8 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/calendar/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/agent.py index 057351c77..fb34aa938 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/clickup/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/agent.py index 3b021ee70..044fd7dc1 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/confluence/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/agent.py index feacecd78..d2cb3a9b1 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/discord/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/agent.py index 9ff9bc1f3..b9743c9d6 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/dropbox/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/agent.py index 5edf37b85..bd4bbc929 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/gmail/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/agent.py index 4b4269e2b..31d270b22 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/google_drive/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/agent.py index b381c6bcf..ae6573e4b 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/jira/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/agent.py index 4c3d1d3a5..f93d15b3c 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/linear/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/agent.py index 343874c33..afd5787ef 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/luma/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/agent.py index 8c8a80ab5..7910eb450 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/notion/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/agent.py index 551388d34..521c45958 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/onedrive/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/agent.py index b72f82dab..552070961 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/slack/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/agent.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/agent.py index aa6f34935..0f7f7e2bc 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/agent.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/connectors/teams/agent.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any from deepagents import SubAgent @@ -29,7 +28,7 @@ def build_subagent( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: buckets = load_tools(dependencies=dependencies) @@ -51,5 +50,5 @@ def build_subagent( tools=tools, interrupt_on=interrupt_on, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, ) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/registry.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/registry.py index 1b7a19ad7..58a971c0b 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/registry.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/registry.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any, Protocol from deepagents import SubAgent @@ -14,6 +13,9 @@ from app.agents.multi_agent_chat.constants import ( from app.agents.multi_agent_chat.subagents.builtins.deliverables.agent import ( build_subagent as build_deliverables_subagent, ) +from app.agents.multi_agent_chat.subagents.builtins.knowledge_base.agent import ( + build_subagent as build_knowledge_base_subagent, +) from app.agents.multi_agent_chat.subagents.builtins.memory.agent import ( build_subagent as build_memory_subagent, ) @@ -79,7 +81,7 @@ class SubagentBuilder(Protocol): *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, extra_tools_bucket: ToolsPermissions | None = None, ) -> SubAgent: ... @@ -95,6 +97,7 @@ SUBAGENT_BUILDERS_BY_NAME: dict[str, SubagentBuilder] = { "gmail": build_gmail_subagent, "google_drive": build_google_drive_subagent, "jira": build_jira_subagent, + "knowledge_base": build_knowledge_base_subagent, "linear": build_linear_subagent, "luma": build_luma_subagent, "memory": build_memory_subagent, @@ -169,7 +172,7 @@ def build_subagents( *, dependencies: dict[str, Any], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, mcp_tools_by_agent: dict[str, ToolsPermissions] | None = None, exclude: list[str] | None = None, disabled_tools: list[str] | None = None, @@ -188,7 +191,7 @@ def build_subagents( spec = builder( dependencies=dependencies, model=model, - extra_middleware=extra_middleware, + middleware_stack=middleware_stack, extra_tools_bucket=mcp.get(name), ) _filter_disabled_tools_in_place(spec, disabled_names) diff --git a/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/subagent_builder.py b/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/subagent_builder.py index b6614afa9..a4a1f84d4 100644 --- a/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/subagent_builder.py +++ b/surfsense_backend/app/agents/multi_agent_chat/subagents/shared/subagent_builder.py @@ -2,7 +2,6 @@ from __future__ import annotations -from collections.abc import Sequence from typing import Any, cast from deepagents import SubAgent @@ -20,16 +19,22 @@ def pack_subagent( system_prompt: str, tools: list[BaseTool], model: BaseChatModel | None = None, - extra_middleware: Sequence[Any] | None = None, + middleware_stack: dict[str, Any] | None = None, interrupt_on: dict[str, bool] | None = None, ) -> SubAgent: - """Pack the route-local pieces passed in into one sub-agent spec.""" + """Pack the route-local pieces passed in into one sub-agent spec. + + ``middleware_stack`` is the shared subagent middleware stack (see + ``build_subagent_middleware_stack``). Every non-``None`` value is + prepended to this subagent's middleware list in insertion order. + """ if not system_prompt.strip(): msg = f"Subagent {name!r}: system_prompt is empty" raise ValueError(msg) + prepended = [m for m in (middleware_stack or {}).values() if m is not None] middleware: list[Any] = [ - *(extra_middleware or []), + *prepended, PatchToolCallsMiddleware(), DedupHITLToolCallsMiddleware(agent_tools=tools), ] diff --git a/surfsense_backend/tests/unit/agents/multi_agent_chat/subagents/shared/test_subagent_builder.py b/surfsense_backend/tests/unit/agents/multi_agent_chat/subagents/shared/test_subagent_builder.py index 648e52115..123bdc09f 100644 --- a/surfsense_backend/tests/unit/agents/multi_agent_chat/subagents/shared/test_subagent_builder.py +++ b/surfsense_backend/tests/unit/agents/multi_agent_chat/subagents/shared/test_subagent_builder.py @@ -1,4 +1,4 @@ -"""Subagent resilience contract: ``extra_middleware`` reaches the agent chain.""" +"""Subagent resilience contract: ``middleware_stack`` reaches the agent chain.""" from __future__ import annotations @@ -67,7 +67,7 @@ class _AlwaysFailingChatModel(BaseChatModel): @pytest.mark.asyncio async def test_subagent_recovers_when_primary_llm_fails(): - """Fallback in ``extra_middleware`` must finish the turn when primary raises.""" + """Fallback in ``middleware_stack`` must finish the turn when primary raises.""" primary = _AlwaysFailingChatModel() fallback = FakeMessagesListChatModel( responses=[AIMessage(content="recovered via fallback")] @@ -79,7 +79,7 @@ async def test_subagent_recovers_when_primary_llm_fails(): system_prompt="be helpful", tools=[], model=primary, - extra_middleware=[ModelFallbackMiddleware(fallback)], + middleware_stack={"fallback": ModelFallbackMiddleware(fallback)}, ) agent = create_agent(