diff --git a/navi/core/agent.py b/navi/core/agent.py index 0c3b966..5b40dd0 100644 --- a/navi/core/agent.py +++ b/navi/core/agent.py @@ -182,7 +182,7 @@ raise SessionNotFound(session_id) profile = self._profiles.get(session.profile_id) - tools = self._tool_list(profile.enabled_tools, profile.mcp_servers) + tools = self._tool_list(profile.get_agent_tools()) tool_schemas = [t.schema() for t in tools] llm = self._get_backend(profile.llm_backend) @@ -399,7 +399,7 @@ if fresh and fresh.profile_id != session.profile_id: session.profile_id = fresh.profile_id profile = self._profiles.get(session.profile_id) - tools = self._tool_list(profile.enabled_tools, profile.mcp_servers) + tools = self._tool_list(profile.get_agent_tools()) tool_schemas = [t.schema() for t in tools] llm = self._get_backend(profile.llm_backend) log.info( @@ -494,10 +494,9 @@ def _tool_list( self, - enabled: list[str], - mcp_servers: dict[str, list[str]] | None = None, + scope: "ToolScopeConfig", ) -> list[Tool]: - return build_tool_list(enabled, mcp_servers, self._tools, self._mcp_manager) + return build_tool_list(scope.native, scope.mcp, self._tools, self._mcp_manager) def _get_backend(self, backend_key: str) -> LLMBackend: return self._backends.get(backend_key) diff --git a/navi/core/context_builder.py b/navi/core/context_builder.py index ca56719..2145d59 100644 --- a/navi/core/context_builder.py +++ b/navi/core/context_builder.py @@ -249,9 +249,9 @@ """ if not self._mcp_manager: return None - if profile is not None and not profile.mcp_servers: + if profile is not None and not profile.get_agent_tools().mcp: return None - server_names = set(profile.mcp_servers.keys()) if profile is not None else None + server_names = set(profile.get_agent_tools().mcp.keys()) if profile is not None else None instructions = self._mcp_manager.get_instructions(server_names) if not instructions: return None diff --git a/navi/core/subagent_runner.py b/navi/core/subagent_runner.py index f269bea..2cfc397 100644 --- a/navi/core/subagent_runner.py +++ b/navi/core/subagent_runner.py @@ -100,32 +100,10 @@ _model_var.set(profile.model) exclude = set(exclude_tools or []) - from navi.mcp.tools import build_mcp_name, is_mcp_tool - - tool_source = profile.subagent_tools if profile.subagent_tools else profile.enabled_tools - - # If subagent_tools is a strict whitelist, filter mcp_servers to only - # include MCP tools explicitly listed in the whitelist. This prevents - # a profile from accidentally granting sub-agents access to MCP servers - # that are meant for the main agent only. - mcp_servers = profile.mcp_servers - if profile.subagent_tools: - mcp_tool_names = {n for n in profile.subagent_tools if is_mcp_tool(n)} - if mcp_tool_names: - filtered_mcp_servers: dict[str, list[str]] = {} - for server_name, groups in (profile.mcp_servers or {}).items(): - for group_name in groups: - for tool_name in self._mcp_manager.resolve_group(server_name, group_name): - full_name = build_mcp_name(server_name, tool_name) - if full_name in mcp_tool_names: - filtered_mcp_servers.setdefault(server_name, []).append(group_name) - mcp_servers = filtered_mcp_servers or None - else: - mcp_servers = None - + scope = profile.get_subagent_tools() tools = [ t - for t in build_tool_list(tool_source, mcp_servers, self._tools, self._mcp_manager) + for t in build_tool_list(scope.native, scope.mcp, self._tools, self._mcp_manager) if t.name not in exclude ] tool_schemas = [t.schema() for t in tools] diff --git a/navi/profiles/base.py b/navi/profiles/base.py index fabbbc2..d030d75 100644 --- a/navi/profiles/base.py +++ b/navi/profiles/base.py @@ -1,4 +1,18 @@ -from pydantic import BaseModel, Field, field_validator +from pydantic import BaseModel, Field, field_validator, model_validator + + +class ToolScopeConfig(BaseModel): + """Tool set for a single scope (agent or subagent).""" + + native: list[str] = Field(default_factory=list) + mcp: dict[str, list[str]] = Field(default_factory=dict) + + +class ToolConfig(BaseModel): + """Explicit per-profile tool configuration.""" + + agent: ToolScopeConfig = Field(default_factory=ToolScopeConfig) + subagent: ToolScopeConfig = Field(default_factory=ToolScopeConfig) class AgentProfile(BaseModel): @@ -14,7 +28,15 @@ name: str description: str system_prompt: str - enabled_tools: list[str] # tool names; resolved by ToolRegistry at runtime + tools: ToolConfig = Field(default_factory=ToolConfig) + + # --- Deprecated: kept for auto-migration from old configs ------------------ + # These fields are automatically migrated into ``tools`` by the loader. + # Do not set them in new configs; use ``tools.agent`` / ``tools.subagent``. + enabled_tools: list[str] = Field(default_factory=list) + mcp_servers: dict[str, list[str]] = Field(default_factory=dict) + subagent_tools: list[str] = Field(default_factory=list) + llm_backend: str = "ollama" # backend key, e.g. "ollama", "openai" # Ordered list of preferred models; first available wins at runtime. # Accepts a plain string for backward compatibility (auto-wrapped in a list). @@ -91,14 +113,8 @@ adaptive_replan_enabled: bool = False # Sub-agent configuration - # subagent_tools: tool names available to sub-agents spawned from this profile. - # If empty, falls back to enabled_tools minus dangerous/irrelevant ones. - # subagent_planning_enabled: if True, sub-agents run the planning phase before their tool loop. - # subagent_think_enabled: controls extended reasoning for sub-agents. If None, - # sub-agents inherit think_enabled from the parent profile. # subagent_system_prompt: injected as an additional system message for sub-agents, # after the profile's main system_prompt. Loaded from subagent_system_prompt.txt if present. - subagent_tools: list[str] = Field(default_factory=list) subagent_planning_enabled: bool = False subagent_think_enabled: bool | None = None subagent_system_prompt: str = "" @@ -107,13 +123,45 @@ # Global providers (global_provider=True) are always injected regardless of this list. context_providers: list[str] = Field(default_factory=list) - # MCP servers referenced by this profile. - # Format: {"server_name": ["group1", "group2"]} or {"server_name": ["*"]} for all tools. - mcp_servers: dict[str, list[str]] = Field(default_factory=dict) - @field_validator("model", mode="before") @classmethod def _coerce_model(cls, v): if isinstance(v, str): return [v] if v else ["gemma4:31b-cloud"] return v + + @model_validator(mode="after") + def _migrate_tools(self): + """Auto-migrate legacy fields into the explicit ``tools`` structure.""" + if self.enabled_tools or self.mcp_servers or self.subagent_tools: + if not self.tools.agent.native: + self.tools.agent.native = list(self.enabled_tools) + if not self.tools.agent.mcp: + self.tools.agent.mcp = dict(self.mcp_servers) + if self.subagent_tools: + if not self.tools.subagent.native: + self.tools.subagent.native = list(self.subagent_tools) + if not self.tools.subagent.mcp: + # Infer MCP servers from subagent_tools if they contain mcp__ names + from navi.mcp.tools import is_mcp_tool + inferred: dict[str, list[str]] = {} + for name in self.subagent_tools: + if is_mcp_tool(name): + # e.g. mcp__navi_3d__compile_scad -> server navi_3d + parts = name.split("__", 2) + if len(parts) >= 2: + srv = parts[1] + inferred.setdefault(srv, []).append("*") + self.tools.subagent.mcp = inferred + return self + + def get_agent_tools(self) -> ToolScopeConfig: + """Return the resolved agent tool set.""" + return self.tools.agent + + def get_subagent_tools(self) -> ToolScopeConfig: + """Return the resolved subagent tool set.""" + if self.tools.subagent.native or self.tools.subagent.mcp: + return self.tools.subagent + # Fallback: if subagent is empty but agent is not, use agent + return self.tools.agent diff --git a/navi/profiles/developer/config.json b/navi/profiles/developer/config.json index 495b66f..4456d1d 100644 --- a/navi/profiles/developer/config.json +++ b/navi/profiles/developer/config.json @@ -10,6 +10,7 @@ }, "llm_backend": "ollama", "model": [ + "qwen3.5:397b-cloud", "gemma4:26b-a4b-it-q4_K_M", "gemma4:31b-cloud", "kimi-k2.6:cloud", @@ -27,39 +28,6 @@ "anti_stall_threshold": 8, "step_validation_enabled": false, "adaptive_replan_enabled": true, - "subagent_tools": [ - "todo", - "scratchpad", - "reflect", - "filesystem", - "code_exec", - "terminal", - "image_view", - "list_tools", - "share_file", - "content_publish" - ], - "enabled_tools": [ - "todo", - "scratchpad", - "reflect", - "switch_profile", - "list_profiles", - "filesystem", - "code_exec", - "terminal", - "image_view", - "memory", - "list_tools", - "tool_manual", - "ssh_exec", - "spawn_agent", - "share_file", - "content_publish", - "gmail", - "schedule_recall", - "manage_recall" - ], "planning_mandatory": false, "planning_phase1_enabled": true, "planning_phase2_enabled": true, @@ -67,11 +35,51 @@ "top_k": 40, "top_p": 0.88, "num_thread": 11, - "mcp_servers": { - "navi-web": [ - "search", - "browse", - "request" - ] + "tools": { + "agent": { + "native": [ + "todo", + "scratchpad", + "reflect", + "switch_profile", + "list_profiles", + "filesystem", + "code_exec", + "terminal", + "image_view", + "memory", + "list_tools", + "tool_manual", + "ssh_exec", + "spawn_agent", + "share_file", + "content_publish", + "gmail", + "schedule_recall", + "manage_recall" + ], + "mcp": { + "navi-web": [ + "search", + "browse", + "request" + ] + } + }, + "subagent": { + "native": [ + "todo", + "scratchpad", + "reflect", + "filesystem", + "code_exec", + "terminal", + "image_view", + "list_tools", + "share_file", + "content_publish" + ], + "mcp": {} + } } } diff --git a/navi/profiles/discuss/config.json b/navi/profiles/discuss/config.json index 3d41119..d7595b3 100644 --- a/navi/profiles/discuss/config.json +++ b/navi/profiles/discuss/config.json @@ -4,6 +4,7 @@ "description": "Creative partner for Q&A, brainstorming, and idea exploration. High creativity, free-form thinking.", "short_description": "Creative Q&A and idea discussion — best for open questions, brainstorming, and exploring concepts.", "model": [ + "qwen3.5:397b-cloud", "gemma4:26b-a4b-it-q4_K_M", "gemma4:31b-cloud", "kimi-k2.6:cloud", @@ -11,33 +12,6 @@ ], "temperature": 0.65, "max_iterations": 30, - "enabled_tools": [ - "scratchpad", - "reflect", - "memory", - "image_view", - "todo", - "get_current_datetime", - "list_tools", - "tool_manual", - "switch_profile", - "list_profiles", - "share_file", - "filesystem", - "schedule_recall", - "manage_recall" - ], - "mcp_servers": { - "gnexus-book": [ - "read" - ], - "navi-web": [ - "search", - "browse", - "request" - ] - }, - "subagent_tools": [], "planning_enabled": false, "planning_mandatory": false, "planning_phase1_enabled": true, @@ -54,5 +28,39 @@ "subagent_planning_enabled": false, "top_k": 80, "top_p": 0.95, - "num_thread": 11 + "num_thread": 11, + "tools": { + "agent": { + "native": [ + "scratchpad", + "reflect", + "memory", + "image_view", + "todo", + "get_current_datetime", + "list_tools", + "tool_manual", + "switch_profile", + "list_profiles", + "share_file", + "filesystem", + "schedule_recall", + "manage_recall" + ], + "mcp": { + "gnexus-book": [ + "read" + ], + "navi-web": [ + "search", + "browse", + "request" + ] + } + }, + "subagent": { + "native": [], + "mcp": {} + } + } } diff --git a/navi/profiles/loader.py b/navi/profiles/loader.py index a4b5c34..b988918 100644 --- a/navi/profiles/loader.py +++ b/navi/profiles/loader.py @@ -12,11 +12,11 @@ import structlog from pathlib import Path -from .base import AgentProfile +from .base import AgentProfile, ToolConfig log = structlog.get_logger() -_REQUIRED_CONFIG_KEYS = {"id", "name", "description", "enabled_tools"} +_REQUIRED_CONFIG_KEYS = {"id", "name", "description"} def _normalize_model(value: object) -> list[str]: @@ -63,12 +63,24 @@ # If only the old key is present, migrate its value transparently. phase2_default = config.get("planning_reflect_enabled", False) + # New explicit tool config or legacy migration + _tools_raw = config.get("tools") + _tools = ToolConfig.model_validate(_tools_raw) if _tools_raw else ToolConfig() + + # Legacy fields still accepted for auto-migration + _enabled_tools = config.get("enabled_tools", []) + _mcp_servers = config.get("mcp_servers", {}) + _subagent_tools = config.get("subagent_tools", []) + profiles.append(AgentProfile( id=config["id"], name=config["name"], description=config["description"], system_prompt=system_prompt, - enabled_tools=config["enabled_tools"], + tools=_tools, + enabled_tools=_enabled_tools, + mcp_servers=_mcp_servers, + subagent_tools=_subagent_tools, llm_backend=config.get("llm_backend", "ollama"), model=_normalize_model(config.get("model", ["gemma4:31b-cloud"])), temperature=config.get("temperature", 0.7), @@ -92,12 +104,10 @@ anti_stall_threshold=config.get("anti_stall_threshold", 8), step_validation_enabled=config.get("step_validation_enabled", False), adaptive_replan_enabled=config.get("adaptive_replan_enabled", False), - subagent_tools=config.get("subagent_tools", []), subagent_planning_enabled=config.get("subagent_planning_enabled", False), subagent_think_enabled=config.get("subagent_think_enabled", None), subagent_system_prompt=subagent_system_prompt, context_providers=config.get("context_providers", []), - mcp_servers=config.get("mcp_servers", {}), )) log.debug("profile.loader.loaded", profile_id=config["id"]) @@ -143,13 +153,11 @@ "anti_stall_threshold": profile.anti_stall_threshold, "step_validation_enabled": profile.step_validation_enabled, "adaptive_replan_enabled": profile.adaptive_replan_enabled, - "subagent_tools": profile.subagent_tools, "subagent_planning_enabled": profile.subagent_planning_enabled, "subagent_think_enabled": profile.subagent_think_enabled, "is_subagent_only": profile.is_subagent_only, - "enabled_tools": profile.enabled_tools, + "tools": profile.tools.model_dump(mode="json"), "context_providers": profile.context_providers, - "mcp_servers": profile.mcp_servers, } config_file = profile_dir / "config.json" diff --git a/navi/profiles/modeler_3d/config.json b/navi/profiles/modeler_3d/config.json index 94e1d6d..2c3a9f9 100644 --- a/navi/profiles/modeler_3d/config.json +++ b/navi/profiles/modeler_3d/config.json @@ -10,9 +10,12 @@ }, "llm_backend": "ollama", "model": [ + "glm-5.1:cloud", + "qwen3.5:397b-cloud", + "kimi-k2.6:cloud", "gemma4:26b-a4b-it-q4_K_M", "gemma4:31b-cloud", - "qwen3.6:27b" + "qwen3.6:35b" ], "temperature": 0.35, "max_iterations": 70, @@ -31,41 +34,48 @@ "adaptive_replan_enabled": true, "subagent_planning_enabled": false, "subagent_think_enabled": false, - "subagent_tools": [ - "filesystem", - "image_view" - ], - "enabled_tools": [ - "todo", - "scratchpad", - "reflect", - "switch_profile", - "list_profiles", - "filesystem", - "code_exec", - "terminal", - "image_view", - "memory", - "list_tools", - "tool_manual", - "spawn_agent", - "share_file", - "content_publish", - "schedule_recall", - "manage_recall" - ], - "mcp_servers": { - "navi-3d": [ - "modeling", - "analysis" - ], - "navi-web": [ - "search", - "browse", - "request" - ] - }, "top_k": 30, "top_p": 0.85, - "num_thread": 11 + "num_thread": 11, + "tools": { + "agent": { + "native": [ + "todo", + "scratchpad", + "reflect", + "switch_profile", + "list_profiles", + "filesystem", + "code_exec", + "terminal", + "image_view", + "memory", + "list_tools", + "tool_manual", + "spawn_agent", + "share_file", + "content_publish", + "schedule_recall", + "manage_recall" + ], + "mcp": { + "navi-3d": [ + "modeling", + "analysis" + ], + "navi-web": [ + "search", + "browse", + "request" + ] + } + }, + "subagent": { + "native": [ + "filesystem", + "image_view" + ], + "mcp": {} + } + } } diff --git a/navi/profiles/secretary/config.json b/navi/profiles/secretary/config.json index 65316d2..4da42e2 100644 --- a/navi/profiles/secretary/config.json +++ b/navi/profiles/secretary/config.json @@ -10,6 +10,7 @@ }, "llm_backend": "ollama", "model": [ + "qwen3.5:397b-cloud", "kimi-k2.6:cloud", "gemma4:26b-a4b-it-q4_K_M", "gemma4:31b-cloud", @@ -27,36 +28,6 @@ "anti_stall_threshold": 8, "step_validation_enabled": false, "adaptive_replan_enabled": false, - "subagent_tools": [ - "scratchpad", - "reflect", - "filesystem", - "code_exec", - "image_view", - "memory", - "share_file", - "content_publish" - ], - "enabled_tools": [ - "todo", - "scratchpad", - "reflect", - "switch_profile", - "list_profiles", - "filesystem", - "code_exec", - "image_view", - "memory", - "list_tools", - "tool_manual", - "spawn_agent", - "share_file", - "content_publish", - "weather", - "gmail", - "schedule_recall", - "manage_recall" - ], "planning_mandatory": false, "planning_phase1_enabled": true, "planning_phase2_enabled": true, @@ -64,11 +35,54 @@ "top_k": 50, "top_p": 0.9, "num_thread": 11, - "mcp_servers": { - "navi-web": [ - "search", - "browse", - "request" - ] + "tools": { + "agent": { + "native": [ + "todo", + "scratchpad", + "reflect", + "switch_profile", + "list_profiles", + "filesystem", + "code_exec", + "image_view", + "memory", + "list_tools", + "tool_manual", + "spawn_agent", + "share_file", + "content_publish", + "weather", + "gmail", + "schedule_recall", + "manage_recall" + ], + "mcp": { + "navi-web": [ + "search", + "browse", + "request" + ] + } + }, + "subagent": { + "native": [ + "scratchpad", + "reflect", + "filesystem", + "code_exec", + "image_view", + "memory", + "share_file", + "content_publish" + ], + "mcp": { + "navi-web": [ + "search", + "browse", + "request" + ] + } + } } } diff --git a/navi/profiles/server_admin/config.json b/navi/profiles/server_admin/config.json index 8087220..dd9c1ee 100644 --- a/navi/profiles/server_admin/config.json +++ b/navi/profiles/server_admin/config.json @@ -10,6 +10,7 @@ }, "llm_backend": "ollama", "model": [ + "qwen3.5:397b-cloud", "gemma4:26b-a4b-it-q4_K_M", "gemma4:31b-cloud", "kimi-k2.6:cloud", @@ -27,54 +28,61 @@ "anti_stall_threshold": 8, "step_validation_enabled": false, "adaptive_replan_enabled": false, - "subagent_tools": [ - "scratchpad", - "reflect", - "filesystem", - "code_exec", - "terminal", - "ssh_exec", - "image_view", - "share_file", - "content_publish" - ], - "enabled_tools": [ - "todo", - "scratchpad", - "reflect", - "switch_profile", - "list_profiles", - "filesystem", - "code_exec", - "terminal", - "ssh_exec", - "image_view", - "memory", - "list_tools", - "tool_manual", - "spawn_agent", - "share_file", - "content_publish", - "gmail", - "schedule_recall", - "manage_recall" - ], - "mcp_servers": { - "gnexus-book": [ - "read", - "write" - ], - "navi-web": [ - "search", - "browse", - "request" - ] - }, "planning_mandatory": false, "planning_phase1_enabled": true, "planning_phase2_enabled": true, "planning_phase3_enabled": true, "top_k": 30, "top_p": 0.8, - "num_thread": 11 + "num_thread": 11, + "tools": { + "agent": { + "native": [ + "todo", + "scratchpad", + "reflect", + "switch_profile", + "list_profiles", + "filesystem", + "code_exec", + "terminal", + "ssh_exec", + "image_view", + "memory", + "list_tools", + "tool_manual", + "spawn_agent", + "share_file", + "content_publish", + "gmail", + "schedule_recall", + "manage_recall" + ], + "mcp": { + "gnexus-book": [ + "read", + "write" + ], + "navi-web": [ + "search", + "browse", + "request" + ] + } + }, + "subagent": { + "native": [ + "scratchpad", + "reflect", + "filesystem", + "code_exec", + "terminal", + "ssh_exec", + "image_view", + "share_file", + "content_publish" + ], + "mcp": {} + } + } } diff --git a/navi/profiles/tool_developer/config.json b/navi/profiles/tool_developer/config.json index 7371fcd..5c03668 100644 --- a/navi/profiles/tool_developer/config.json +++ b/navi/profiles/tool_developer/config.json @@ -10,6 +10,7 @@ }, "llm_backend": "ollama", "model": [ + "qwen3.5:397b-cloud", "gemma4:26b-a4b-it-q4_K_M", "gemma4:31b-cloud", "kimi-k2.6:cloud", @@ -27,55 +28,62 @@ "anti_stall_threshold": 8, "step_validation_enabled": false, "adaptive_replan_enabled": true, - "subagent_tools": [ - "todo", - "scratchpad", - "reflect", - "filesystem", - "code_exec", - "terminal", - "image_view", - "list_tools", - "tool_manual", - "share_file", - "content_publish" - ], - "enabled_tools": [ - "todo", - "scratchpad", - "reflect", - "switch_profile", - "list_profiles", - "filesystem", - "code_exec", - "terminal", - "image_view", - "memory", - "reload_tools", - "list_tools", - "tool_manual", - "spawn_agent", - "share_file", - "content_publish", - "gmail", - "create_mcp_server", - "test_mcp_tool", - "mcp_status", - "schedule_recall", - "manage_recall" - ], "planning_mandatory": false, "planning_phase1_enabled": true, "planning_phase2_enabled": true, "planning_phase3_enabled": true, "top_k": 40, "top_p": 0.85, - "mcp_servers": { - "navi-web": [ - "search", - "browse", - "request" - ] - }, - "num_thread": 11 + "num_thread": 11, + "tools": { + "agent": { + "native": [ + "todo", + "scratchpad", + "reflect", + "switch_profile", + "list_profiles", + "filesystem", + "code_exec", + "terminal", + "image_view", + "memory", + "reload_tools", + "list_tools", + "tool_manual", + "spawn_agent", + "share_file", + "content_publish", + "gmail", + "create_mcp_server", + "test_mcp_tool", + "mcp_status", + "schedule_recall", + "manage_recall" + ], + "mcp": { + "navi-web": [ + "search", + "browse", + "request" + ] + } + }, + "subagent": { + "native": [ + "todo", + "scratchpad", + "reflect", + "filesystem", + "code_exec", + "terminal", + "image_view", + "list_tools", + "tool_manual", + "share_file", + "content_publish" + ], + "mcp": {} + } + } } diff --git a/navi/tools/list_tools.py b/navi/tools/list_tools.py index e2708e6..cc47f0f 100644 --- a/navi/tools/list_tools.py +++ b/navi/tools/list_tools.py @@ -57,15 +57,16 @@ except Exception: return ToolResult(success=False, output=f"Profile '{profile_id}' not found.", error="profile_not_found") - names = list(profile.enabled_tools) + scope = profile.get_agent_tools() + names = list(scope.native) extra = _load_user_enabled_tools() for name in extra: if name not in names: names.append(name) # Expand MCP server groups into concrete tool names - if profile.mcp_servers and self._mcp_manager: - for server_name, groups in profile.mcp_servers.items(): + if scope.mcp and self._mcp_manager: + for server_name, groups in scope.mcp.items(): if "*" in groups: prefix = build_mcp_name(server_name, "") for tool in self._registry.all(): diff --git a/navi/tools/spawn_agent.py b/navi/tools/spawn_agent.py index 04d7c8c..285449e 100644 --- a/navi/tools/spawn_agent.py +++ b/navi/tools/spawn_agent.py @@ -140,17 +140,13 @@ parent_sid = current_session_id.get() context_transfer = (await get_section(parent_sid, "context_transfer")) if parent_sid else "" - tool_source = ( - selected_profile.subagent_tools - if selected_profile.subagent_tools - else selected_profile.enabled_tools - ) + scope = selected_profile.get_subagent_tools() log.info("spawn_agent.start", profile_id=profile_id, max_iterations=max_iterations, task_preview=task[:80], has_briefing=bool(briefing), has_context_transfer=bool(context_transfer), has_system_prompt=bool(custom_system_prompt), - subagent_tools=len(tool_source)) + subagent_tools=len(scope.native) + sum(len(v) for v in scope.mcp.values())) agent = Agent( session_store=None, # ephemeral — no DB access