diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 5b98f75c..19d97b05 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -82,11 +82,7 @@ from backend.apps.agents.manager.session.history_compaction import ( _estimate_post_compact_input, _get_branch_messages, ) -from backend.apps.agents.manager.prompt.prompt_context import ( - _resolve_attached_skills, - _resolve_forced_tools, - _resolve_mode, -) +from backend.apps.agents.manager.prompt.prompt_context import resolve_mode from backend.apps.agents.manager.prompt.attachments import ( _build_dir_tree, _build_prompt_content, @@ -108,9 +104,6 @@ class AgentManager: # multi-second SDK teardown the cancel handler sits behind. self._live_partial: dict[str, dict] = {} - def _resolve_mode(self, mode_id: str) -> tuple[list[str], str | None, str | None]: - return _resolve_mode(mode_id, get_all_tool_names) - async def _build_mcp_servers( self, allowed_tools: list[str], @@ -188,7 +181,7 @@ class AgentManager: async def launch_agent(self, config: AgentConfig) -> AgentSession: session_id = uuid4().hex - mode_tools, _, mode_folder = self._resolve_mode(config.mode) + mode_tools, _, mode_folder = resolve_mode(config.mode, get_all_tool_names) tools = mode_tools global_settings = load_settings() @@ -291,12 +284,6 @@ class AgentManager: def _build_dir_tree(self, root: str, max_depth: int = 4, prefix: str = "") -> list[str]: return _build_dir_tree(root, max_depth, prefix) - def _resolve_forced_tools(self, forced_tools: list[str] | None) -> str: - return _resolve_forced_tools(forced_tools) - - def _resolve_attached_skills(self, attached_skills: list | None) -> str: - return _resolve_attached_skills(attached_skills) - # ------------------------------------------------------------------ # Compaction & token guard (Phase 2) # @@ -410,7 +397,7 @@ class AgentManager: return await tool_result_hook.post_tool_hook(hook_ctx, input_data, tool_use_id, context) try: - _, mode_sys_prompt, _ = self._resolve_mode(session.mode) + _, mode_sys_prompt, _ = resolve_mode(session.mode, get_all_tool_names) # Reconcile active_mcps against currently-enabled tools (Phase 3). # If the user toggled a server off in the Tools page mid-session, @@ -2527,7 +2514,7 @@ class AgentManager: session_changed = True if mode and mode != session.mode: session.mode = mode - mode_tools, _, _ = self._resolve_mode(mode) + mode_tools, _, _ = resolve_mode(mode, get_all_tool_names) session.allowed_tools = mode_tools session_changed = True if session_changed: diff --git a/backend/apps/agents/manager/prompt/attachments.py b/backend/apps/agents/manager/prompt/attachments.py index 01233a02..7877bd10 100644 --- a/backend/apps/agents/manager/prompt/attachments.py +++ b/backend/apps/agents/manager/prompt/attachments.py @@ -1,6 +1,6 @@ import os -from backend.apps.agents.manager.prompt.prompt_context import _resolve_attached_skills, _resolve_forced_tools +from backend.apps.agents.manager.prompt.prompt_context import resolve_attached_skills, resolve_forced_tools def _build_dir_tree(root: str, max_depth: int = 4, prefix: str = "") -> list[str]: @@ -45,8 +45,8 @@ def _build_prompt_content(prompt: str, images: list | None = None, context_paths context_text, native_blocks, refusals = _resolve_attachments( context_paths, api_type=api_type, model=model, ) - forced_tools_text = _resolve_forced_tools(forced_tools) - skills_text = _resolve_attached_skills(attached_skills) + forced_tools_text = resolve_forced_tools(forced_tools) + skills_text = resolve_attached_skills(attached_skills) refusal_text = "\n\n".join(refusals) parts = [p for p in (forced_tools_text, context_text, refusal_text, skills_text, prompt) if p] diff --git a/backend/apps/agents/manager/prompt/prompt_context.py b/backend/apps/agents/manager/prompt/prompt_context.py index 484cca6d..683f2d8b 100644 --- a/backend/apps/agents/manager/prompt/prompt_context.py +++ b/backend/apps/agents/manager/prompt/prompt_context.py @@ -8,7 +8,7 @@ from backend.apps.tools_lib.tools_lib import ( from backend.apps.agents.manager.prompt.tool_catalog import _get_denied_tool_names, _is_fully_denied -def _resolve_mode(mode_id: str, get_all_tool_names: Callable[[], list[str]]) -> tuple[list[str], str | None, str | None]: +def resolve_mode(mode_id: str, get_all_tool_names: Callable[[], list[str]]) -> tuple[list[str], str | None, str | None]: """Return (tools, system_prompt, default_folder) resolved from the mode store.""" mode_def = load_mode(mode_id) if mode_def: @@ -17,7 +17,7 @@ def _resolve_mode(mode_id: str, get_all_tool_names: Callable[[], list[str]]) -> return get_all_tool_names(), None, None -def _build_connected_tools_context(allowed_tools: list[str], get_all_tool_names: Callable[[], list[str]]) -> str | None: +def build_connected_tools_context(allowed_tools: list[str], get_all_tool_names: Callable[[], list[str]]) -> str | None: """Build a context block describing connected MCP tools and their accounts. Tools set to 'deny' and fully-denied servers are excluded. @@ -127,7 +127,7 @@ def toolsearch_loop_redirect(consecutive_toolsearch: int, gated_servers: list[st return reason -def _build_browser_context(dashboard_id: str | None, selected_browser_ids: list[str] | None = None) -> str | None: +def build_browser_context(dashboard_id: str | None, selected_browser_ids: list[str] | None = None) -> str | None: """Build a context block listing browser cards and delegation instructions. Only browser cards explicitly selected by the user are included. @@ -203,7 +203,7 @@ def _build_browser_context(dashboard_id: str | None, selected_browser_ids: list[ return "\n".join(lines) -def _build_selected_app_context(selected_app_output_ids: list[str] | None) -> str | None: +def build_selected_app_context(selected_app_output_ids: list[str] | None) -> str | None: """Build a context block for dashboard App cards the user selected to edit. Resolves each Output id to its on-disk workspace so the agent edits the @@ -262,7 +262,7 @@ def _build_selected_app_context(selected_app_output_ids: list[str] | None) -> st ) -def _build_selected_settings_context(selected_setting_ids: list[str] | None) -> str | None: +def build_selected_settings_context(selected_setting_ids: list[str] | None) -> str | None: """Context block when the user points the agent at specific Settings rows. A targeting aid, NOT a gate: the settings tools (SettingsRead/SettingsWrite) @@ -284,7 +284,7 @@ def _build_selected_settings_context(selected_setting_ids: list[str] | None) -> ) -def _build_mcp_registry_summary(allowed_tools: list[str], active_mcps: list[str], get_all_tool_names: Callable[[], list[str]]) -> str | None: +def build_mcp_registry_summary(allowed_tools: list[str], active_mcps: list[str], get_all_tool_names: Callable[[], list[str]]) -> str | None: """Compact registry of installed MCP servers, one line per server. This is the visible surface that drives the activation gate: the model @@ -411,14 +411,14 @@ AGENT_IDENTITY = ( ) -def _compose_system_prompt(default_prompt: str | None, mode_prompt: str | None, session_prompt: str | None, connected_tools_ctx: str | None = None, browser_ctx: str | None = None, mcp_registry_ctx: str | None = None) -> str | None: +def compose_system_prompt(default_prompt: str | None, mode_prompt: str | None, session_prompt: str | None, connected_tools_ctx: str | None = None, browser_ctx: str | None = None, mcp_registry_ctx: str | None = None) -> str | None: # 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] return "\n\n".join(parts) -def _resolve_forced_tools(forced_tools: list[str] | None) -> str: +def resolve_forced_tools(forced_tools: list[str] | None) -> str: """Build a context block describing explicitly requested tools.""" if not forced_tools: return "" @@ -458,7 +458,7 @@ def _resolve_forced_tools(forced_tools: list[str] | None) -> str: ) -def _resolve_attached_skills(attached_skills: list | None) -> str: +def resolve_attached_skills(attached_skills: list | None) -> str: """Build a context block injecting attached skill content into the prompt. For a multi-file (folder) skill we inject the SKILL.md body as text AND point diff --git a/backend/apps/agents/manager/prompt/system_prompt.py b/backend/apps/agents/manager/prompt/system_prompt.py index 559d0005..c4ce6f85 100644 --- a/backend/apps/agents/manager/prompt/system_prompt.py +++ b/backend/apps/agents/manager/prompt/system_prompt.py @@ -12,11 +12,11 @@ from typeguard import typechecked from backend.apps.agents.core.models import AgentSession from backend.apps.agents.manager.prompt.tool_catalog import get_all_tool_names from backend.apps.agents.manager.prompt.prompt_context import ( - _build_browser_context as build_browser_context, - _build_mcp_registry_summary as build_mcp_registry_summary, - _build_selected_app_context as build_selected_app_context, - _build_selected_settings_context as build_selected_settings_context, - _compose_system_prompt as compose_system_prompt, + build_browser_context, + build_mcp_registry_summary, + build_selected_app_context, + build_selected_settings_context, + compose_system_prompt, ) diff --git a/backend/tests/test_browser_orchestrator_routing.py b/backend/tests/test_browser_orchestrator_routing.py index f28a6adf..2fb4ef77 100644 --- a/backend/tests/test_browser_orchestrator_routing.py +++ b/backend/tests/test_browser_orchestrator_routing.py @@ -13,7 +13,7 @@ from backend.apps.agents.manager.prompt import prompt_context as pc def _fake_dashboard(monkeypatch): - # _build_browser_context loads the dashboard; give it a minimal one so it + # build_browser_context loads the dashboard; give it a minimal one so it # gets past the load and emits the static delegation guidance. import backend.apps.dashboards.dashboards as dash @@ -25,7 +25,7 @@ def _fake_dashboard(monkeypatch): def test_orchestrator_routes_same_flow_batches_to_one_agent(monkeypatch): _fake_dashboard(monkeypatch) - ctx = pc._build_browser_context("dash-1", selected_browser_ids=[]) + ctx = pc.build_browser_context("dash-1", selected_browser_ids=[]) assert ctx is not None # the key guidance: one agent + the whole list, not one agent per item assert "Give ONE agent the whole list" in ctx @@ -35,4 +35,4 @@ def test_orchestrator_routes_same_flow_batches_to_one_agent(monkeypatch): def test_browser_context_is_none_without_a_dashboard(): - assert pc._build_browser_context(None) is None + assert pc.build_browser_context(None) is None diff --git a/backend/tests/test_settings_select_and_send.py b/backend/tests/test_settings_select_and_send.py index 01cbaa03..d008ca7c 100644 --- a/backend/tests/test_settings_select_and_send.py +++ b/backend/tests/test_settings_select_and_send.py @@ -42,10 +42,10 @@ def test_message_endpoint_threads_selected_setting_ids(client, monkeypatch): def test_selected_settings_context_block_targets_the_fields(): - from backend.apps.agents.manager.prompt.prompt_context import _build_selected_settings_context - assert _build_selected_settings_context(None) is None - assert _build_selected_settings_context([]) is None - block = _build_selected_settings_context(["theme", "default_model"]) + from backend.apps.agents.manager.prompt.prompt_context import build_selected_settings_context + assert build_selected_settings_context(None) is None + assert build_selected_settings_context([]) is None + block = build_selected_settings_context(["theme", "default_model"]) assert "theme" in block and "default_model" in block # It tells the agent to use the always-on settings tools on exactly these fields. assert "SettingsRead" in block and "SettingsWrite" in block diff --git a/backend/tests/test_skill_registry_community.py b/backend/tests/test_skill_registry_community.py index 5bf726fe..cfa4b5ff 100644 --- a/backend/tests/test_skill_registry_community.py +++ b/backend/tests/test_skill_registry_community.py @@ -142,13 +142,13 @@ def test_install_dedups_instead_of_clobbering_existing_skill(skills_dir): def test_confirm_install_writes_folder_lists_and_injects(skills_dir, monkeypatch): """End-to-end install->usable: confirm=true through the real /install endpoint writes the folder skill, it shows up in /api/skills/list with supporting - files, and _resolve_attached_skills injects it with the folder path so an + files, and resolve_attached_skills injects it with the folder path so an agent can read its scripts. (resolve is mocked to skip the network; the live GitHub resolve is proven separately.)""" import secrets as _secrets from fastapi.testclient import TestClient from backend.main import app - from backend.apps.agents.manager.prompt.prompt_context import _resolve_attached_skills + from backend.apps.agents.manager.prompt.prompt_context import resolve_attached_skills import backend.auth as auth_mod if not auth_mod._TOKEN: auth_mod._TOKEN = _secrets.token_urlsafe(32) @@ -175,7 +175,7 @@ def test_confirm_install_writes_folder_lists_and_injects(skills_dir, monkeypatch assert (skills_dir / slug / "SKILL.md").exists() assert (skills_dir / slug / "scripts" / "extract.py").exists() # Injectable: the agent gets the body AND a pointer to the folder for on-demand reads. - block = _resolve_attached_skills([{"id": slug, "name": "PDF Tools", "content": "# PDF Tools\nRun scripts/extract.py to pull text."}]) + block = resolve_attached_skills([{"id": slug, "name": "PDF Tools", "content": "# PDF Tools\nRun scripts/extract.py to pull text."}]) assert "[Using skill: PDF Tools]" in block assert str(skills_dir / slug) in block diff --git a/backend/tests/test_skills_folders.py b/backend/tests/test_skills_folders.py index 980a6a9f..f7e6ec1f 100644 --- a/backend/tests/test_skills_folders.py +++ b/backend/tests/test_skills_folders.py @@ -14,7 +14,7 @@ import json import pytest import backend.apps.skills.skills as skills_mod -from backend.apps.agents.manager.prompt.prompt_context import _resolve_attached_skills +from backend.apps.agents.manager.prompt.prompt_context import resolve_attached_skills @pytest.fixture @@ -116,7 +116,7 @@ def test_injection_points_at_folder_for_supporting_files(skills_dir): _write(str(base / "SKILL.md"), "use the template") _write(str(base / "template.html"), "") - block = _resolve_attached_skills([{"id": "withfiles", "name": "WithFiles", "content": "use the template"}]) + block = resolve_attached_skills([{"id": "withfiles", "name": "WithFiles", "content": "use the template"}]) assert "[Using skill: WithFiles]" in block assert str(base) in block assert "Read" in block # tells the agent to read supporting files @@ -129,17 +129,17 @@ def test_skill_injection_is_provider_agnostic_by_construction(skills_dir): builtin set every provider gets. So a non-Claude agent receives byte-identical skill text and the same file-reading tools.""" import inspect - from backend.apps.agents.manager.prompt.prompt_context import _resolve_attached_skills + from backend.apps.agents.manager.prompt.prompt_context import resolve_attached_skills from backend.apps.agents.manager.prompt.tool_catalog import FULL_TOOLS # 1. No provider/api parameter -> the injected text cannot branch on the model. - assert set(inspect.signature(_resolve_attached_skills).parameters) == {"attached_skills"} + assert set(inspect.signature(resolve_attached_skills).parameters) == {"attached_skills"} # 2. A folder skill yields the body + a pointer to its folder via Read/Glob/Bash. base = skills_dir / "vid" _write(str(base / "SKILL.md"), "render it") _write(str(base / "helper.py"), "x") - block = _resolve_attached_skills([{"id": "vid", "name": "Vid", "content": "render it"}]) + block = resolve_attached_skills([{"id": "vid", "name": "Vid", "content": "render it"}]) assert "[Using skill: Vid]" in block assert str(base) in block and "Read" in block @@ -149,7 +149,7 @@ def test_skill_injection_is_provider_agnostic_by_construction(skills_dir): def test_injection_no_folder_note_for_flat_skill(skills_dir): _write(str(skills_dir / "plain.md"), "plain content") - block = _resolve_attached_skills([{"id": "plain", "name": "Plain", "content": "plain content"}]) + block = resolve_attached_skills([{"id": "plain", "name": "Plain", "content": "plain content"}]) assert "[Using skill: Plain]" in block assert "supporting files" not in block.lower()