diff --git a/astrbot/builtin_stars/astrbot/process_llm_request.py b/astrbot/builtin_stars/astrbot/process_llm_request.py index f0d7d0c51..4452181c2 100644 --- a/astrbot/builtin_stars/astrbot/process_llm_request.py +++ b/astrbot/builtin_stars/astrbot/process_llm_request.py @@ -110,12 +110,49 @@ class ProcessLLMRequest: # NOTE: subagent_orchestrator config lives at top-level now. orch_cfg = self.ctx.get_config().get("subagent_orchestrator", {}) if orch_cfg.get("main_enable", False): + policy = str(orch_cfg.get("main_tools_policy", "handoff_only")).strip() + if policy not in {"handoff_only", "unassigned_to_main"}: + # Prefer the safer default when config contains unknown values. + policy = "handoff_only" + + assigned_tools: set[str] = set() + agents = orch_cfg.get("agents", []) + if isinstance(agents, list): + for a in agents: + if not isinstance(a, dict): + continue + if a.get("enabled", True) is False: + continue + tools = a.get("tools", []) + if not isinstance(tools, list): + continue + for t in tools: + name = str(t).strip() + if name: + assigned_tools.add(name) + toolset = ToolSet() + + # Always expose handoff tools (transfer_to_*) when orchestrator is enabled. for tool in tmgr.func_list: - # Prevent recursion / confusion: in handoff-only mode, the main LLM - # should only be able to call transfer_to_* tools. if isinstance(tool, HandoffTool) and tool.active: toolset.add_tool(tool) + + # Optional mode: keep tools that are not assigned to any subagent on the main LLM. + if policy == "unassigned_to_main": + for tool in tmgr.func_list: + if not tool.active: + continue + if isinstance(tool, HandoffTool): + continue + if tool.handler_module_path == "core.subagent_orchestrator": + continue + if tool.name in assigned_tools: + continue + toolset.add_tool(tool) + + # Override any earlier tool injection (e.g. skills local env tools) to keep + # main-LLM tool visibility predictable under subagent orchestrator. req.func_tool = toolset # Encourage the model to delegate to subagents. @@ -128,6 +165,13 @@ class ProcessLLMRequest: if router_prompt: req.system_prompt += f"\n{router_prompt}\n" + if policy == "unassigned_to_main": + req.system_prompt += ( + "\n[Note: You may directly call the tools visible to the main LLM " + "if they are not assigned to any subagent; otherwise prefer delegating " + "to subagents via transfer_to_*.]\n" + ) + return # Default behavior: follow persona tool selection. diff --git a/astrbot/core/config/default.py b/astrbot/core/config/default.py index 673ab3bc1..f28cdabac 100644 --- a/astrbot/core/config/default.py +++ b/astrbot/core/config/default.py @@ -124,11 +124,15 @@ DEFAULT_CONFIG = { }, "skills": {"runtime": "sandbox"}, }, - # SubAgent orchestrator mode: the main LLM only delegates tasks to subagents - # (via transfer_to_{agent} tools). Domain tools are mounted on subagents. + # SubAgent orchestrator mode: + # - main_enable = False: disabled; main LLM mounts tools normally (persona selection). + # - main_enable = True: enabled; main LLM tool mounting is controlled by main_tools_policy. "subagent_orchestrator": { "main_enable": False, - "main_tools_policy": "handoff_only", # reserved for future; main_enable implies handoff_only + # - handoff_only: main LLM only sees transfer_to_* tools (recommended default when enabled). + # - unassigned_to_main: tools not assigned to any subagent are still mounted on main LLM. + # - disabled: UI convenience value; ignored when main_enable is False. + "main_tools_policy": "disabled", "router_system_prompt": ( "You are a task router. Your job is to chat naturally, recognize user intent, " "and delegate work to the most suitable subagent using transfer_to_* tools. " diff --git a/astrbot/dashboard/routes/subagent.py b/astrbot/dashboard/routes/subagent.py index 90a534301..54e9ad70e 100644 --- a/astrbot/dashboard/routes/subagent.py +++ b/astrbot/dashboard/routes/subagent.py @@ -35,7 +35,7 @@ class SubAgentRoute(Route): if not isinstance(data, dict): data = { "main_enable": False, - "main_tools_policy": "handoff_only", + "main_tools_policy": "disabled", "agents": [], } @@ -49,7 +49,10 @@ class SubAgentRoute(Route): # Ensure required keys exist. data.setdefault("main_enable", False) - data.setdefault("main_tools_policy", "handoff_only") + if "main_tools_policy" not in data: + data["main_tools_policy"] = ( + "handoff_only" if data.get("main_enable", False) else "disabled" + ) data.setdefault("agents", []) # Backward/forward compatibility: ensure each agent contains provider_id. diff --git a/dashboard/src/views/SubAgentPage.vue b/dashboard/src/views/SubAgentPage.vue index f2500920b..e0756e35e 100644 --- a/dashboard/src/views/SubAgentPage.vue +++ b/dashboard/src/views/SubAgentPage.vue @@ -17,23 +17,29 @@ - - +
-
- 启用:主 LLM 仅负责对话与“转交”,只会看到 transfer_to_* 这类委派工具;需要调用工具时,会把任务交给对应 SubAgent 执行。SubAgent 负责真正的工具调用与结果整理,并把结论回传给主 LLM。 +
+ 不启动:SubAgent 关闭;主 LLM 按 persona 规则挂载工具(默认全部),并直接调用。
-
- 关闭:恢复原有行为(按 persona 选择并直接注入工具)。 +
+ 启动:SubAgent 可分派;未分配给任何 SubAgent 的工具仍挂载到主 LLM 上。 +
+
+ 启动:仅 SubAgent;主 LLM 只保留 transfer_to_* 这类委派工具,不挂载其他工具。
@@ -239,9 +245,10 @@ type SubAgentItem = { __tool_group_selected?: string[] } +type MainMode = 'disabled' | 'unassigned_to_main' | 'handoff_only' + type SubAgentConfig = { - main_enable: boolean - main_tools_policy: 'handoff_only' | 'persona' + main_mode: MainMode agents: SubAgentItem[] } @@ -259,14 +266,14 @@ function toast(message: string, color: 'success' | 'error' | 'warning' = 'succes snackbar.value = { show: true, message, color } } -const mainToolPolicies = [ - { label: 'handoff_only(主 LLM 仅 transfer_to_*)', value: 'handoff_only' }, - { label: 'persona(仍按 persona 选择工具)', value: 'persona' } +const mainModes: Array<{ label: string; value: MainMode }> = [ + { label: '不启动:SubAgent 关闭(主 LLM 按 persona 挂载工具)', value: 'disabled' }, + { label: '启动:未分配工具仍挂载到主 LLM', value: 'unassigned_to_main' }, + { label: '启动:仅 SubAgent(主 LLM 仅 transfer_to_*)', value: 'handoff_only' } ] const cfg = ref({ - main_enable: false, - main_tools_policy: 'handoff_only', + main_mode: 'disabled', agents: [] }) @@ -326,7 +333,10 @@ function syncGroupSelectionToAgentTools(agent: SubAgentItem) { function normalizeConfig(raw: any): SubAgentConfig { const main_enable = !!raw?.main_enable - const main_tools_policy = (raw?.main_tools_policy === 'persona' ? 'persona' : 'handoff_only') + const policy = (raw?.main_tools_policy ?? '').toString().trim() + const main_mode: MainMode = !main_enable + ? 'disabled' + : (policy === 'unassigned_to_main' ? 'unassigned_to_main' : 'handoff_only') const agentsRaw = Array.isArray(raw?.agents) ? raw.agents : [] const agents: SubAgentItem[] = agentsRaw.map((a: any, i: number) => { @@ -351,7 +361,7 @@ function normalizeConfig(raw: any): SubAgentConfig { } }) - return { main_enable, main_tools_policy, agents } + return { main_mode, agents } } async function loadConfig() { @@ -478,10 +488,10 @@ async function save() { saving.value = true try { // Strip UI-only fields + const mode = cfg.value.main_mode const payload = { - main_enable: cfg.value.main_enable, - // Reserved for future; backend treats main_enable as handoff-only. - main_tools_policy: 'handoff_only', + main_enable: mode !== 'disabled', + main_tools_policy: mode, agents: cfg.value.agents.map(a => ({ name: a.name, public_description: a.public_description,