feat(sub-agent): 子智能体最大轮次支持个人空间自定义

- 原 task.py 硬编码 max_turns=50,改为从任务记录读取(None=默认50、0=无上限、N=N轮)

- 个人空间-子智能体新增「最大执行轮次」设置项,PUT /api/multiagent/settings 支持 sub_agent_max_turns(null 删键恢复默认,负数 400)

- 多智能体与传统后台两条创建链路均接入该配置
This commit is contained in:
JOJO 2026-07-31 22:40:20 +08:00
parent 2c2c5aba4d
commit 2da6c87245
5 changed files with 97 additions and 8 deletions

View File

@ -1955,12 +1955,16 @@ class MainTerminalToolsExecutionMixin:
task_message = build_master_dispatch_text(arguments.get("task", ""))
summary_text = (arguments.get("summary") or f"{role.name}作业")[:80]
thinking_mode = arguments.get("thinking_mode") or role.thinking_mode or "fast"
# 读取子智能体压缩阈值配置
# 读取子智能体压缩阈值与最大轮次配置
_compress_threshold = 150_000
_max_turns = None
try:
from modules.personalization_manager import load_personalization_config
_prefs = load_personalization_config(data_dir) or {}
_compress_threshold = int(_prefs.get("sub_agent_compress_threshold_tokens", 150_000))
_raw_max_turns = _prefs.get("sub_agent_max_turns")
if _raw_max_turns is not None:
_max_turns = int(_raw_max_turns)
except Exception:
pass
# 走原行 发事件创建(避免后期重建提供重复工能重费,直接使用 multi_agent_mode=True 调用)
@ -1978,6 +1982,7 @@ class MainTerminalToolsExecutionMixin:
system_prompt=system_prompt,
task_message=task_message,
compress_threshold_tokens=_compress_threshold,
max_turns=_max_turns,
)
# 在多智能体模式下,子智能体是团队协作成员,不是传统后台任务。
# run_in_background=False 避免触发后台完成通知轮询,保持主对话输入区可用。
@ -1985,6 +1990,16 @@ class MainTerminalToolsExecutionMixin:
logger.exception("[multi_agent] create_sub_agent failed")
result = {"success": False, "error": str(exc)}
else:
# 读取子智能体最大轮次配置None=默认50、0=无上限、N=N轮
_max_turns = None
try:
from modules.personalization_manager import load_personalization_config
_prefs = load_personalization_config(str(getattr(self, "data_dir", ""))) or {}
_raw_max_turns = _prefs.get("sub_agent_max_turns")
if _raw_max_turns is not None:
_max_turns = int(_raw_max_turns)
except Exception:
pass
result = self.sub_agent_manager.create_sub_agent(
agent_id=arguments.get("agent_id"),
summary=arguments.get("summary", ""),
@ -1993,7 +2008,8 @@ class MainTerminalToolsExecutionMixin:
run_in_background=arguments.get("run_in_background", False),
timeout_seconds=arguments.get("timeout_seconds"),
thinking_mode=arguments.get("thinking_mode"),
conversation_id=self.context_manager.current_conversation_id
conversation_id=self.context_manager.current_conversation_id,
max_turns=_max_turns,
)
# 如果不是后台运行,阻塞等待完成

View File

@ -239,6 +239,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
system_prompt: Optional[str] = None,
task_message: Optional[str] = None,
compress_threshold_tokens: Optional[int] = None,
max_turns: Optional[int] = None,
) -> Dict:
"""创建子智能体任务并启动协程。
@ -329,6 +330,7 @@ class SubAgentManager(SubAgentStateMixin, SubAgentStatsMixin, SubAgentCreationMi
"display_name": display_name,
"execution_mode": "in_process",
"compress_threshold_tokens": compress_threshold_tokens,
"max_turns": max_turns,
"container_name": None,
}
# 多智能体模式:为该会话创建或复用 MultiAgentState

View File

@ -79,6 +79,14 @@ class SubAgentTask:
# 上下文压缩配置
# 默认阈值 150k tokens可由外部覆盖如个人空间子智能体管理配置
self.compress_threshold_tokens: int = int(task_record.get("compress_threshold_tokens") or 150_000)
# 最大执行轮次(对应个人空间子智能体设置项 sub_agent_max_turns
# 未设置 → 默认 500或负数→ None 表示无上限;正整数 → 该值
_raw_max_turns = task_record.get("max_turns")
if _raw_max_turns is None:
self.max_turns: Optional[int] = 50
else:
_max_turns_int = int(_raw_max_turns)
self.max_turns = _max_turns_int if _max_turns_int > 0 else None
self.current_context_tokens: int = 0
self._compress_round: int = 0
@ -191,7 +199,7 @@ class SubAgentTask:
tools.append(FINISH_TOOL)
start_time = time.time()
max_turns = 50
max_turns = self.max_turns # None 表示无上限(个人空间可配置,默认 50
turn = 0
while not self._cancelled:
@ -262,7 +270,7 @@ class SubAgentTask:
continue
turn += 1
if turn > max_turns:
if max_turns is not None and turn > max_turns:
await self._write_failure("任务执行超过最大轮次限制", max_turns_exceeded=True)
return

View File

@ -205,14 +205,17 @@ def get_multi_agent_settings_api():
_, workspace = get_user_resources(username)
if not workspace:
return jsonify({"success": False, "error": "工作区未就绪"}), 503
# 从个人化配置中读取子智能体压缩阈值
# 从个人化配置中读取子智能体设置
from modules.personalization_manager import load_personalization_config
prefs = load_personalization_config(workspace.data_dir) or {}
compress_threshold = prefs.get("sub_agent_compress_threshold_tokens", 150000)
# 最大执行轮次None未设置表示默认 500 表示无上限;正整数为该值
max_turns = prefs.get("sub_agent_max_turns")
return jsonify({
"success": True,
"settings": {
"sub_agent_compress_threshold_tokens": compress_threshold,
"sub_agent_max_turns": max_turns,
},
})
except Exception as exc:
@ -232,15 +235,33 @@ def update_multi_agent_settings_api():
return jsonify({"success": False, "error": "工作区未就绪"}), 503
data = request.get_json() or {}
settings = data.get("settings") or {}
from modules.personalization_manager import load_personalization_config, save_personalization_config
prefs = load_personalization_config(workspace.data_dir) or {}
dirty = False
# 更新子智能体压缩阈值
threshold = settings.get("sub_agent_compress_threshold_tokens")
if threshold is not None:
threshold = int(threshold)
if threshold < 10000:
return jsonify({"success": False, "error": "压缩阈值不能小于 10000"}), 400
from modules.personalization_manager import load_personalization_config, save_personalization_config
prefs = load_personalization_config(workspace.data_dir) or {}
prefs["sub_agent_compress_threshold_tokens"] = threshold
dirty = True
# 更新子智能体最大轮次键存在才处理None → 删除键恢复默认 500 → 无上限;正整数 → 该值)
if "sub_agent_max_turns" in settings:
max_turns_raw = settings.get("sub_agent_max_turns")
if max_turns_raw is None:
if prefs.pop("sub_agent_max_turns", None) is not None:
dirty = True
else:
try:
max_turns = int(max_turns_raw)
except (TypeError, ValueError):
return jsonify({"success": False, "error": "最大轮次必须是整数"}), 400
if max_turns < 0:
return jsonify({"success": False, "error": "最大轮次不能为负数0 表示无上限)"}), 400
prefs["sub_agent_max_turns"] = max_turns
dirty = True
if dirty:
save_personalization_config(workspace.data_dir, prefs)
return jsonify({"success": True})
except Exception as exc:

View File

@ -1772,6 +1772,35 @@
</div>
</div>
<!-- 最大执行轮次配置 -->
<div class="settings-action-row" style="margin-bottom: 16px">
<span class="settings-row-copy">
<span class="settings-row-title">最大执行轮次</span>
<span class="settings-row-desc">
子智能体单次任务的最大执行轮次一轮 = 一次模型调用留空默认 50 0 表示无上限慎用失控任务会持续消耗 API 额度
</span>
</span>
<div style="display: flex; gap: 6px; align-items: center">
<input
type="number"
class="settings-number-input"
v-model.number="subAgentMaxTurns"
:min="0"
:step="10"
placeholder="50"
style="width: 120px"
/>
<button
type="button"
class="settings-secondary-button"
:disabled="subAgentSettingsSaving"
@click="saveSubAgentSettings"
>
{{ subAgentSettingsSaving ? '保存中...' : '保存' }}
</button>
</div>
</div>
<!-- 角色列表 -->
<div class="settings-section-header" style="margin-bottom: 8px">
<span class="settings-section-title">角色列表</span>
@ -2732,6 +2761,8 @@ watch(
const subAgentRoles = ref<any[]>([]);
const subAgentRolesLoading = ref(false);
const subAgentCompressThreshold = ref(150000);
/** 子智能体最大执行轮次null/'' = 默认 500 = 无上限;正整数 = 该值 */
const subAgentMaxTurns = ref<number | null>(null);
const subAgentSettingsSaving = ref(false);
const subAgentModels = ref<any[]>([]);
const roleEditorOpen = ref(false);
@ -2779,6 +2810,8 @@ const loadSubAgentSettings = async () => {
const data = await resp.json();
if (data.success && data.settings) {
subAgentCompressThreshold.value = data.settings.sub_agent_compress_threshold_tokens || 150000;
// nullplaceholder 50
subAgentMaxTurns.value = data.settings.sub_agent_max_turns ?? null;
}
} catch (e) {
//
@ -2792,7 +2825,16 @@ const saveSubAgentSettings = async () => {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ settings: { sub_agent_compress_threshold_tokens: subAgentCompressThreshold.value } })
body: JSON.stringify({
settings: {
sub_agent_compress_threshold_tokens: subAgentCompressThreshold.value,
// null/'' null 500
sub_agent_max_turns:
subAgentMaxTurns.value === null || (subAgentMaxTurns.value as unknown) === ''
? null
: Number(subAgentMaxTurns.value)
}
})
});
} finally {
subAgentSettingsSaving.value = false;