refactor(agents): evict mac-only tools/middleware from shared kernel

These were never shared with anonymous_chat (nor podcaster/video_presentation)
-- only multi_agent_chat (subagents/main agent) and the boundary use them:

  shared/tools/mcp/             -> multi_agent_chat/shared/tools/mcp/
  shared/tools/hitl.py          -> multi_agent_chat/shared/tools/hitl.py
  shared/tools/catalog.py       -> multi_agent_chat/shared/tools/catalog.py
  shared/middleware/dedup_tool_calls.py
                                -> multi_agent_chat/shared/middleware/dedup_tool_calls.py

app/agents/shared/ now holds only the genuine anon<->mac kernel:
context, middleware/{compaction,retry_after}, tools/web_search.
This commit is contained in:
CREDO23 2026-06-05 12:50:46 +02:00
parent b7ea829371
commit d59bb2b5aa
21 changed files with 50 additions and 40 deletions

View file

@ -29,7 +29,7 @@ from langchain.agents.middleware import AgentMiddleware, AgentState
from langchain_core.tools import BaseTool from langchain_core.tools import BaseTool
from langgraph.runtime import Runtime from langgraph.runtime import Runtime
from app.agents.shared.middleware.dedup_tool_calls import ( from app.agents.multi_agent_chat.shared.middleware.dedup_tool_calls import (
DedupResolver, DedupResolver,
wrap_dedup_key_by_arg_name, wrap_dedup_key_by_arg_name,
) )

View file

@ -9,7 +9,7 @@ factories for those few tools and nothing else, so the main agent's tool
surface stays self-contained and connector-free. surface stays self-contained and connector-free.
Tool *display* metadata for the whole app (the ``/agent/tools`` listing Tool *display* metadata for the whole app (the ``/agent/tools`` listing
endpoint) lives separately in :mod:`app.agents.shared.tools.catalog`, a endpoint) lives separately in :mod:`app.agents.multi_agent_chat.shared.tools.catalog`, a
pure-data module that imports no connectors. This registry only governs what pure-data module that imports no connectors. This registry only governs what
the main agent actually builds and binds. the main agent actually builds and binds.
""" """

View file

@ -0,0 +1 @@
"""Tools shared across multi_agent_chat (main agent + subagents + boundary)."""

View file

@ -6,7 +6,7 @@ shared by every sensitive tool (native connectors and MCP tools alike).
Usage inside a tool:: Usage inside a tool::
from app.agents.shared.tools.hitl import request_approval from app.agents.multi_agent_chat.shared.tools.hitl import request_approval
result = request_approval( result = request_approval(
action_type="gmail_email_send", action_type="gmail_email_send",

View file

@ -112,7 +112,9 @@ def refresh_mcp_tools_cache_for_connector(
when an event loop is available. Neither path raises. when an event loop is available. Neither path raises.
""" """
try: try:
from app.agents.shared.tools.mcp.tool import invalidate_mcp_tools_cache from app.agents.multi_agent_chat.shared.tools.mcp.tool import (
invalidate_mcp_tools_cache,
)
invalidate_mcp_tools_cache(search_space_id) invalidate_mcp_tools_cache(search_space_id)
except Exception: except Exception:
@ -133,7 +135,9 @@ def refresh_mcp_tools_cache_for_connector(
async def _run_connector_prefetch(connector_id: int) -> None: async def _run_connector_prefetch(connector_id: int) -> None:
from app.agents.shared.tools.mcp.tool import discover_single_mcp_connector from app.agents.multi_agent_chat.shared.tools.mcp.tool import (
discover_single_mcp_connector,
)
try: try:
await discover_single_mcp_connector(connector_id) await discover_single_mcp_connector(connector_id)

View file

@ -33,14 +33,16 @@ from sqlalchemy import cast, select
from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.dialects.postgresql import JSONB
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.shared.middleware.dedup_tool_calls import dedup_key_full_args from app.agents.multi_agent_chat.shared.middleware.dedup_tool_calls import (
from app.agents.shared.tools.hitl import request_approval dedup_key_full_args,
from app.agents.shared.tools.mcp.cache import ( )
from app.agents.multi_agent_chat.shared.tools.hitl import request_approval
from app.agents.multi_agent_chat.shared.tools.mcp.cache import (
CachedMCPTools, CachedMCPTools,
read_cached_tools, read_cached_tools,
write_cached_tools, write_cached_tools,
) )
from app.agents.shared.tools.mcp.client import MCPClient from app.agents.multi_agent_chat.shared.tools.mcp.client import MCPClient
from app.db import SearchSourceConnector from app.db import SearchSourceConnector
from app.services.mcp_oauth.registry import MCP_SERVICES, get_service_by_connector_type from app.services.mcp_oauth.registry import MCP_SERVICES, get_service_by_connector_type
from app.utils.perf import get_perf_logger from app.utils.perf import get_perf_logger

View file

@ -21,7 +21,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.agents.multi_agent_chat.constants import ( from app.agents.multi_agent_chat.constants import (
CONNECTOR_TYPE_TO_CONNECTOR_AGENT_MAPS, CONNECTOR_TYPE_TO_CONNECTOR_AGENT_MAPS,
) )
from app.agents.shared.tools.mcp.tool import load_mcp_tools from app.agents.multi_agent_chat.shared.tools.mcp.tool import load_mcp_tools
from app.db import SearchSourceConnector from app.db import SearchSourceConnector
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -1,14 +1,5 @@
"""Cross-agent shared tools and tool metadata. """Cross-agent shared tools.
Tool *implementations* live with the agents that own them (e.g. deliverable Only genuinely cross-agent tool code lives here (currently web_search, imported
generators and their knowledge-base search helper under directly from its module).
``subagents/builtins/deliverables/tools``). This package holds only the
genuinely shared piece: the display-metadata catalog.
""" """
from .catalog import TOOL_CATALOG, ToolMetadata
__all__ = [
"TOOL_CATALOG",
"ToolMetadata",
]

View file

@ -665,7 +665,7 @@ def _refresh_mcp_cache(connector_id: int, space_id: int) -> None:
isolated from the OAuth response flow. isolated from the OAuth response flow.
""" """
try: try:
from app.agents.shared.tools.mcp.cache import ( from app.agents.multi_agent_chat.shared.tools.mcp.cache import (
refresh_mcp_tools_cache_for_connector, refresh_mcp_tools_cache_for_connector,
) )

View file

@ -1668,7 +1668,7 @@ async def list_agent_tools(
Hidden (WIP) tools are excluded from the response. Hidden (WIP) tools are excluded from the response.
""" """
from app.agents.shared.tools.catalog import TOOL_CATALOG from app.agents.multi_agent_chat.shared.tools.catalog import TOOL_CATALOG
return [ return [
AgentToolInfo( AgentToolInfo(

View file

@ -43,6 +43,7 @@ from app.db import (
async_session_maker, async_session_maker,
get_async_session, get_async_session,
) )
from app.notifications.service import NotificationService
from app.observability import metrics as ot_metrics, otel as ot from app.observability import metrics as ot_metrics, otel as ot
from app.schemas import ( from app.schemas import (
GoogleDriveIndexRequest, GoogleDriveIndexRequest,
@ -55,7 +56,6 @@ from app.schemas import (
SearchSourceConnectorUpdate, SearchSourceConnectorUpdate,
) )
from app.services.composio_service import ComposioService, get_composio_service from app.services.composio_service import ComposioService, get_composio_service
from app.notifications.service import NotificationService
from app.users import current_active_user from app.users import current_active_user
# NOTE: connector indexer functions are imported lazily inside each # NOTE: connector indexer functions are imported lazily inside each
@ -675,7 +675,9 @@ async def delete_search_source_connector(
await session.commit() await session.commit()
if is_mcp: if is_mcp:
from app.agents.shared.tools.mcp.tool import invalidate_mcp_tools_cache from app.agents.multi_agent_chat.shared.tools.mcp.tool import (
invalidate_mcp_tools_cache,
)
invalidate_mcp_tools_cache(search_space_id) invalidate_mcp_tools_cache(search_space_id)
@ -2687,7 +2689,7 @@ async def create_mcp_connector(
f"for user {user.id} in search space {search_space_id}" f"for user {user.id} in search space {search_space_id}"
) )
from app.agents.shared.tools.mcp.cache import ( from app.agents.multi_agent_chat.shared.tools.mcp.cache import (
refresh_mcp_tools_cache_for_connector, refresh_mcp_tools_cache_for_connector,
) )
@ -2867,7 +2869,7 @@ async def update_mcp_connector(
logger.info(f"Updated MCP connector {connector_id}") logger.info(f"Updated MCP connector {connector_id}")
from app.agents.shared.tools.mcp.cache import ( from app.agents.multi_agent_chat.shared.tools.mcp.cache import (
refresh_mcp_tools_cache_for_connector, refresh_mcp_tools_cache_for_connector,
) )
@ -2927,7 +2929,9 @@ async def delete_mcp_connector(
await session.delete(connector) await session.delete(connector)
await session.commit() await session.commit()
from app.agents.shared.tools.mcp.tool import invalidate_mcp_tools_cache from app.agents.multi_agent_chat.shared.tools.mcp.tool import (
invalidate_mcp_tools_cache,
)
invalidate_mcp_tools_cache(search_space_id) invalidate_mcp_tools_cache(search_space_id)
@ -2966,7 +2970,7 @@ async def test_mcp_server_connection(
Connection status and list of available tools Connection status and list of available tools
""" """
try: try:
from app.agents.shared.tools.mcp.client import ( from app.agents.multi_agent_chat.shared.tools.mcp.client import (
test_mcp_connection, test_mcp_connection,
test_mcp_http_connection, test_mcp_http_connection,
) )
@ -3157,7 +3161,9 @@ async def trust_mcp_tool(
connectors (``LINEAR_CONNECTOR``, ``JIRA_CONNECTOR``, ...) the connectors (``LINEAR_CONNECTOR``, ``JIRA_CONNECTOR``, ...) the
storage primitive is the same JSON list under ``config.trusted_tools``. storage primitive is the same JSON list under ``config.trusted_tools``.
""" """
from app.agents.shared.tools.mcp.tool import invalidate_mcp_tools_cache from app.agents.multi_agent_chat.shared.tools.mcp.tool import (
invalidate_mcp_tools_cache,
)
from app.services.user_tool_allowlist import add_user_trust from app.services.user_tool_allowlist import add_user_trust
try: try:
@ -3197,7 +3203,9 @@ async def untrust_mcp_tool(
The tool will require HITL approval again on subsequent calls. The tool will require HITL approval again on subsequent calls.
""" """
from app.agents.shared.tools.mcp.tool import invalidate_mcp_tools_cache from app.agents.multi_agent_chat.shared.tools.mcp.tool import (
invalidate_mcp_tools_cache,
)
from app.services.user_tool_allowlist import remove_user_trust from app.services.user_tool_allowlist import remove_user_trust
try: try:

View file

@ -137,10 +137,10 @@ def install(active_patches: list[Any]) -> None:
"""Patch production MCP streamable-HTTP boundaries exactly once.""" """Patch production MCP streamable-HTTP boundaries exactly once."""
targets = [ targets = [
( (
"app.agents.shared.tools.mcp.tool.streamablehttp_client", "app.agents.multi_agent_chat.shared.tools.mcp.tool.streamablehttp_client",
_fake_streamablehttp_client, _fake_streamablehttp_client,
), ),
("app.agents.shared.tools.mcp.tool.ClientSession", _FakeClientSession), ("app.agents.multi_agent_chat.shared.tools.mcp.tool.ClientSession", _FakeClientSession),
] ]
for target, replacement in targets: for target, replacement in targets:
p = patch(target, replacement) p = patch(target, replacement)

View file

@ -115,7 +115,9 @@ def test_full_args_dedup_keeps_distinct_calls_sharing_a_field() -> None:
With :func:`dedup_key_full_args` only fully identical arg dicts dedup. With :func:`dedup_key_full_args` only fully identical arg dicts dedup.
""" """
from app.agents.shared.middleware.dedup_tool_calls import dedup_key_full_args from app.agents.multi_agent_chat.shared.middleware.dedup_tool_calls import (
dedup_key_full_args,
)
tool = _make_tool("createJiraIssue", dedup_key=dedup_key_full_args) tool = _make_tool("createJiraIssue", dedup_key=dedup_key_full_args)
mw = DedupHITLToolCallsMiddleware(agent_tools=[tool]) mw = DedupHITLToolCallsMiddleware(agent_tools=[tool])
@ -157,7 +159,9 @@ def test_full_args_dedup_keeps_distinct_calls_sharing_a_field() -> None:
def test_full_args_dedup_drops_only_exact_duplicates() -> None: def test_full_args_dedup_drops_only_exact_duplicates() -> None:
from app.agents.shared.middleware.dedup_tool_calls import dedup_key_full_args from app.agents.multi_agent_chat.shared.middleware.dedup_tool_calls import (
dedup_key_full_args,
)
tool = _make_tool("createJiraIssue", dedup_key=dedup_key_full_args) tool = _make_tool("createJiraIssue", dedup_key=dedup_key_full_args)
mw = DedupHITLToolCallsMiddleware(agent_tools=[tool]) mw = DedupHITLToolCallsMiddleware(agent_tools=[tool])

View file

@ -17,7 +17,7 @@ caused two production-painful behaviors:
read-only tool calls, raising ``RejectedError("ls")``. read-only tool calls, raising ``RejectedError("ls")``.
* Mutating connector tools got *double* prompted once via the * Mutating connector tools got *double* prompted once via the
middleware ``ask`` and again via the per-tool ``interrupt()`` in middleware ``ask`` and again via the per-tool ``interrupt()`` in
``app.agents.shared.tools.hitl``. ``app.agents.multi_agent_chat.shared.tools.hitl``.
These tests pin the layering so a refactor that drops the default These tests pin the layering so a refactor that drops the default
ruleset fails loud. ruleset fails loud.

View file

@ -10,7 +10,7 @@ from __future__ import annotations
import pytest import pytest
from app.agents.shared.tools.hitl import ( from app.agents.multi_agent_chat.shared.tools.hitl import (
DEFAULT_AUTO_APPROVED_TOOLS, DEFAULT_AUTO_APPROVED_TOOLS,
HITLResult, HITLResult,
request_approval, request_approval,

View file

@ -7,7 +7,7 @@ from types import SimpleNamespace
import pytest import pytest
from app.agents.shared.tools.mcp.cache import ( from app.agents.multi_agent_chat.shared.tools.mcp.cache import (
CachedMCPToolDef, CachedMCPToolDef,
CachedMCPTools, CachedMCPTools,
read_cached_tools, read_cached_tools,

View file

@ -5,7 +5,7 @@ from langchain_core.tools import StructuredTool
from app.agents.multi_agent_chat.main_agent.middleware.dedup_hitl import ( from app.agents.multi_agent_chat.main_agent.middleware.dedup_hitl import (
DedupHITLToolCallsMiddleware, DedupHITLToolCallsMiddleware,
) )
from app.agents.shared.middleware.dedup_tool_calls import ( from app.agents.multi_agent_chat.shared.middleware.dedup_tool_calls import (
wrap_dedup_key_by_arg_name, wrap_dedup_key_by_arg_name,
) )