multi_agent_chat/subagents: dict-keyed middleware_stack + always-on KB

This commit is contained in:
CREDO23 2026-05-12 18:04:54 +02:00
parent eee861bb3d
commit d843468256
39 changed files with 232 additions and 203 deletions

View file

@ -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"]

View file

@ -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 ``<tools>`` 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"]

View file

@ -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"]

View file

@ -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),

View file

@ -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,

View file

@ -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

View file

@ -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,
}