add 3-mode selector and main tool mounting policy

This commit is contained in:
advent259141
2026-01-29 11:27:50 +08:00
parent 60492d46ee
commit 738e69a8af
4 changed files with 90 additions and 29 deletions

View File

@@ -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.

View File

@@ -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. "

View File

@@ -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.

View File

@@ -17,23 +17,29 @@
<v-card class="rounded-lg" variant="flat">
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<v-switch
v-model="cfg.main_enable"
inset
color="primary"
label="启用 SubAgent 分派模式(主 LLM 仅通过 transfer_to_* 委派)"
<v-col cols="12" md="8">
<v-select
v-model="cfg.main_mode"
:items="mainModes"
item-title="label"
item-value="value"
label="SubAgent 模式"
variant="outlined"
density="comfortable"
hide-details
/>
</v-col>
</v-row>
<div class="text-caption text-medium-emphasis mt-1">
<div>
启用 LLM 仅负责对话与转交只会看到 transfer_to_* 这类委派工具需要调用工具时会把任务交给对应 SubAgent 执行SubAgent 负责真正的工具调用与结果整理并把结论回传给主 LLM
<div v-if="cfg.main_mode === 'disabled'">
不启动SubAgent 关闭 LLM persona 规则挂载工具默认全部并直接调用
</div>
<div>
关闭恢复原有行为 persona 选择并直接注入工具
<div v-else-if="cfg.main_mode === 'unassigned_to_main'">
启动SubAgent 可分派未分配给任何 SubAgent 的工具仍挂载到主 LLM
</div>
<div v-else>
启动 SubAgent LLM 只保留 transfer_to_* 这类委派工具不挂载其他工具
</div>
</div>
@@ -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<SubAgentConfig>({
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,