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

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,15 @@
"""``<specialists>`` section — live ``task`` roster for this workspace."""
"""``<specialists>`` 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<specialists>\n"
"No specialists are available for `task` in this workspace.\n"
"</specialists>\n"
)
bullets = "\n".join(f"- **{name}** — {desc}" for name, desc in specialist_lines)
return f"\n<specialists>\n{bullets}\n</specialists>\n"

View file

@ -1,4 +1,4 @@
"""Main-agent ``<tools>`` block (memory + research builtins only; see ``main_agent.tools``)."""
"""Main-agent ``<tools>`` block (memory + research builtins + ``task``)."""
from __future__ import annotations

View file

@ -1,8 +1,7 @@
"""Compose the ``<tools>`` block from per-tool vertical-slice folders.
Each tool lives in ``prompts/tools/<name>/`` with ``description.md`` and an
inline-rendered ``example.md``. Visibility variants (currently only
``update_memory``) live in ``prompts/tools/<name>/{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 ``<tools>``. ``task`` is always included: at least ``deliverables``
and ``knowledge_base`` are always in ``<specialists>`` (see constants)."""
variant = "team" if visibility == ChatVisibility.SEARCH_SPACE else "private"
parts: list[str] = ["\n<tools>\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

View file

@ -0,0 +1 @@
"""``task`` — description + few-shot examples for the specialist-delegation tool."""

View file

@ -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
`<specialists>` 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 `<specialists>`).
- `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.

View file

@ -0,0 +1,20 @@
<example>
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<notes></notes>")
</example>
<example>
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.")
</example>
<example>
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.")
</example>

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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