mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
add 3-mode selector and main tool mounting policy
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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. "
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user