diff --git a/backend/apps/agents/manager/prompt/prompt_context.py b/backend/apps/agents/manager/prompt/prompt_context.py index 629b88c4..23a1ff6a 100644 --- a/backend/apps/agents/manager/prompt/prompt_context.py +++ b/backend/apps/agents/manager/prompt/prompt_context.py @@ -7,7 +7,7 @@ from backend.apps.tools_lib.tools_lib import ( _load_all as load_all_tools, _sanitize_server_name as sanitize_server_name, ) -from backend.apps.agents.manager.prompt.tool_catalog import get_denied_tool_names, is_fully_denied +from backend.apps.agents.manager.prompt.tool_catalog import is_fully_denied @typechecked @@ -20,88 +20,6 @@ def resolve_mode(mode_id: str, get_all_tool_names: Callable[[], List[str]]) -> T return get_all_tool_names(), None, None -@typechecked -def build_connected_tools_context(allowed_tools: List[str], get_all_tool_names: Callable[[], List[str]]) -> Optional[str]: - """Build a context block describing connected MCP tools and their accounts. - - Tools set to 'deny' and fully-denied servers are excluded. - """ - all_tools = load_all_tools() - mcp_tools = [t for t in all_tools if t.mcp_config and t.enabled and t.auth_status in ("configured", "connected")] - - sections = [] - for tool in mcp_tools: - tool_ref = f"mcp:{tool.name}" - if tool_ref not in allowed_tools and allowed_tools != get_all_tool_names(): - continue - - if is_fully_denied(tool): - continue - - server_name = sanitize_server_name(tool.name) - denied = get_denied_tool_names(tool) - tool_descs = { - k: v for k, v in tool.tool_permissions.get("_tool_descriptions", {}).items() - if k not in denied - } - if not tool_descs: - continue - - lines = [f"MCP Server: {server_name}"] - lines.append(f" Status: {tool.auth_status}") - - if tool.connected_account_email: - lines.append(f" Connected account: {tool.connected_account_email}") - lines.append( - f" IMPORTANT: When calling tools from this server that require an email " - f"parameter (e.g. user_google_email, user_email), always use " - f"\"{tool.connected_account_email}\" automatically, do NOT ask the user." - ) - - # Discord guild scoping, hard restriction. The bot may technically - # be in other servers (across other OpenSwarm users), but this - # specific user only authorized these guild IDs. - if tool.name.lower() == "discord": - guilds = tool.oauth_tokens.get("guilds") or [] - if guilds: - guild_descriptions = ", ".join( - f"{g.get('name', 'Unknown')} ({g.get('id', '')})" for g in guilds - ) - allowed_ids = [g.get("id", "") for g in guilds if g.get("id")] - lines.append( - f" AUTHORIZED DISCORD SERVERS (guild_ids): {guild_descriptions}" - ) - lines.append( - f" HARD RESTRICTION: You MUST only call Discord tools that operate on " - f"these guild_ids: {allowed_ids}. NEVER call Discord tools on any other " - f"guild_id even if the bot has access to it. NEVER list, search, or " - f"enumerate servers outside this list. If a user asks about a server " - f"not in this list, refuse and tell them to authorize it via the Connect " - f"Discord button. This is a security boundary, not a preference." - ) - else: - lines.append( - " No Discord servers authorized yet. Tell the user to click " - "'Connect Discord' to add a server before attempting any Discord actions." - ) - - tool_names = list(tool_descs.keys()) - if tool_names: - lines.append(f" Available tools ({len(tool_names)}): {', '.join(tool_names)}") - - sections.append("\n".join(lines)) - - if not sections: - return None - return ( - "\n" - "The following MCP tool servers are connected and available. " - "Use them directly when relevant to the user's request.\n\n" - + "\n\n".join(sections) - + "\n" - ) - - # A run of this many ToolSearch calls with no other tool between them is the # "looping on ToolSearch" wedge: the model hunts for a gated MCP server's tools, # which ToolSearch can never see, gets empty results, and retries. Two free @@ -421,10 +339,10 @@ AGENT_IDENTITY = ( @typechecked -def compose_system_prompt(default_prompt: Optional[str], mode_prompt: Optional[str], session_prompt: Optional[str], connected_tools_ctx: Optional[str] = None, browser_ctx: Optional[str] = None, mcp_registry_ctx: Optional[str] = None) -> Optional[str]: +def compose_system_prompt(default_prompt: Optional[str], mode_prompt: Optional[str], session_prompt: Optional[str], browser_ctx: Optional[str] = None, mcp_registry_ctx: Optional[str] = None) -> Optional[str]: # Identity always leads so it overrides the preset's Claude Code persona, even # when the user has no custom default/mode/session prompt of their own. - parts = [AGENT_IDENTITY] + [p for p in (default_prompt, mode_prompt, session_prompt, connected_tools_ctx, mcp_registry_ctx, browser_ctx) if p] + parts = [AGENT_IDENTITY] + [p for p in (default_prompt, mode_prompt, session_prompt, mcp_registry_ctx, browser_ctx) if p] return "\n\n".join(parts) diff --git a/backend/apps/agents/manager/prompt/system_prompt.py b/backend/apps/agents/manager/prompt/system_prompt.py index c4ce6f85..b0380852 100644 --- a/backend/apps/agents/manager/prompt/system_prompt.py +++ b/backend/apps/agents/manager/prompt/system_prompt.py @@ -32,16 +32,14 @@ def compose_turn_system_prompt( # MCP servers and their tool inventories are intentionally NOT injected into the system # prompt: the CLI's deferred-tool pool already exposes them by name via ToolSearch, and # eagerly listing connected MCPs (account emails, full tool enumerations) here would defeat - # the deferral and leak every integration into every turn. So connected-tools context stays - # None; the model discovers MCPs only when it actively calls ToolSearch. - connected_tools_ctx = None + # the deferral and leak every integration into every turn. The model discovers MCPs only + # when it actively calls ToolSearch; only the gated registry summary goes in. browser_ctx = build_browser_context(session.dashboard_id, selected_browser_ids=selected_browser_ids) mcp_registry_ctx = build_mcp_registry_summary(session.allowed_tools, session.active_mcps, get_all_tool_names) composed_prompt = compose_system_prompt( default_system_prompt, mode_sys_prompt, session.system_prompt, - connected_tools_ctx, browser_ctx, mcp_registry_ctx, ) diff --git a/backend/apps/agents/manager/streaming/result_message.py b/backend/apps/agents/manager/streaming/result_message.py index a428fbfd..12dbe65c 100644 --- a/backend/apps/agents/manager/streaming/result_message.py +++ b/backend/apps/agents/manager/streaming/result_message.py @@ -132,7 +132,6 @@ async def handle_result_message( turn.baseline_children_out = 0 turn.baseline_captured = False thinking.total_ms = 0 - thinking.total_chars = 0 thinking.block_starts = {} session.sdk_session_id = getattr(message, "session_id", None) diff --git a/backend/apps/agents/manager/streaming/state.py b/backend/apps/agents/manager/streaming/state.py index 937f73d5..23cf8c76 100644 --- a/backend/apps/agents/manager/streaming/state.py +++ b/backend/apps/agents/manager/streaming/state.py @@ -18,7 +18,6 @@ class ThinkingState(BaseModel): # block index -> wall-clock start (s); popped to accumulate total_ms when a block ends. block_starts: Dict[int, float] = {} total_ms: int = 0 - total_chars: int = 0 # Stable id for the turn's single thinking message (frontend dedupe replaces in place). msg_id: Optional[str] = None text_parts: List[str] = [] diff --git a/backend/apps/agents/manager/streaming/stream_event.py b/backend/apps/agents/manager/streaming/stream_event.py index d300c0a6..a3bb6e37 100644 --- a/backend/apps/agents/manager/streaming/stream_event.py +++ b/backend/apps/agents/manager/streaming/stream_event.py @@ -127,7 +127,6 @@ async def handle_stream_event( # Thinking content streams as thinking_delta # with a "thinking" field (not "text") think_chunk = delta.get("thinking", "") - thinking.total_chars += len(think_chunk) await ws_manager.send_to_session(session_id, "agent:stream_delta", { "session_id": session_id, "message_id": msg_id,