feat: expose master agent evolution dashboard

This commit is contained in:
kris
2026-04-16 05:46:45 +08:00
parent 504d112218
commit f0490de180
12 changed files with 445 additions and 23 deletions

View File

@@ -155,6 +155,7 @@
- `版本迭代记录` 只读,由主 Agent 汇总
- `我的` 根页当前保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于`
- `我的 > 主 Agent 提示词 / 记忆` 当前可编辑管理员全局主提示词、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 项目记忆
- `我的 > 主 Agent 自动进化` 当前可查看进化信号、待审批提案、已生效规则,并切换 `controlled / autonomous`
- `我的 > AI 账号` 必须可查看和切换 `主 GPT / 备用 GPT / API 容灾`
- `我的 > 技能` 必须按绑定设备展示 Skill并支持一键复制调用语句
- `设备` 页当前只允许出现生产设备,旧演示脏数据不能回流到正式视图

View File

@@ -135,6 +135,7 @@ cd /Users/kris/code/boss
- 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。``master-agent` 会话
- 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有预设不适用时才需要填写自定义模型
- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已接通:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆都可以在 Web 端查看和编辑;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖
- 当前 `我的 > 主 Agent 自动进化` 页面已接通Web `/me/master-agent/evolution` 可查看最近信号、待审批提案和已生效规则,并允许管理员切换 `controlled / autonomous`、批准或拒绝提案
- 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新`
- 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果存在新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口
- 当前如果主控身份还是 `Master Codex Node`,但该节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API / 阿里百炼 Qwen` 备用账号,不再把失败日志直接原样回给用户
@@ -165,6 +166,7 @@ cd /Users/kris/code/boss
- 当前对话级 `agentControls` 已经生效:`master-agent` 会话支持 `modelOverride / reasoningEffortOverride` 强制覆盖,也支持 `fastModelOverride / fastReasoningEffortOverride / smartModelOverride / smartReasoningEffortOverride` 这组策略默认值;主 Agent 普通对话默认按 fast 档选模型,深度任务可按 smart 档选模型,手动强制覆盖仍然优先级最高
- 当前主 Agent 自动进化引擎已接入第一阶段共享内核:`masterAgentEvolutionConfig / Signals / Proposals / Rules / RunLogs` 已进入状态模型,`GET /api/v1/master-agent/evolution` 可查看进化状态,`POST /api/v1/master-agent/evolution/config` 可在 `controlled / autonomous` 间切换,提案支持 approve / reject`controlled` 只生成待审批提案,`autonomous` 可自动采纳低风险 `memory_patch / routing_preference_patch / fast_path_rule`
- 当前 `codex/master-agent-autonomous-evolution` 分支把默认 evolution mode 改成 `autonomous`,新初始化状态会默认开启低风险自动采纳;`codex/master-agent-controlled-evolution` 保持 `controlled` 默认值
- 当前主 Agent 会在 fast path 判断前捕获状态类问题信号;重复短问句会沉淀为 `repeated_question`,显式后端回退会附带 `fallbackToBackendId`,在 autonomous 模式下可自动写回 `backendOverride`
- 当前 `group_dispatch_plan / device_import_resolution / attachment_analysis` 三类深度任务已经会把 `smart*` 策略下发到任务队列,并随任务持久化 `executionModel / executionReasoningEffort`local-agent 执行这类任务时会优先吃任务级模型,不再只依赖本机固定默认模型
- 当前对话级 `agentControls` 也已支持 `backendOverride``master-agent` 会话可在 `Claw Runtime``Hermes Runtime` 可用时显式选择 `claw-runtime / hermes-runtime`,普通单线程会话当前只开放 `hermes-runtime`;不可用时保存接口会直接拒绝,并返回人类可读原因
- 原生 Android 当前会把 `master-agent` 的等待态保留在消息流里:发送后常驻显示“主 Agent 思考中”,超时后改成“主 Agent 回复超时 + 重试等待”,收到新回复后会自动清掉,不再只靠 toast 提示

View File

@@ -0,0 +1,36 @@
import { RealtimeRefresh } from "@/components/app-runtime";
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
import { MasterAgentEvolutionClient } from "@/components/master-agent-evolution-client";
import { requirePageSession } from "@/lib/boss-auth";
import { getMasterAgentEvolutionDashboard } from "@/lib/master-agent-evolution";
export const dynamic = "force-dynamic";
export default async function MasterAgentEvolutionPage() {
const session = await requirePageSession();
const dashboard = await getMasterAgentEvolutionDashboard();
return (
<AppShell bottomNav={false}>
<RealtimeRefresh events={["master_agent.settings.updated"]} />
<StatusBar />
<PageNav title="主 Agent 自动进化" backHref="/me" />
<div className="px-[18px] pb-3">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
Agent
<br />
{session.role === "highest_admin"
? "你是管理员,可以在这里切换受控/全自动模式,并直接审批提案。"
: "你当前是只读视角,可以查看主 Agent 正在学什么。"}
</div>
</div>
<MasterAgentEvolutionClient
isAdmin={session.role === "highest_admin"}
config={dashboard.config}
signals={dashboard.signals}
proposals={dashboard.proposals}
rules={dashboard.rules}
/>
</AppShell>
);
}

View File

@@ -28,6 +28,11 @@ export default async function MePage() {
title="主 Agent 提示词 / 记忆"
description="配置全局主提示词、当前主提示词和用户记忆"
/>
<MenuRow
href="/me/master-agent/evolution"
title="主 Agent 自动进化"
description="查看进化信号、提案、规则,并切换受控 / 全自动模式"
/>
<MenuRow
href="/me/storage"
title="附件与存储"

View File

@@ -0,0 +1,237 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import clsx from "clsx";
type EvolutionConfig = {
mode: "controlled" | "autonomous";
autoApplyLowRiskRules: boolean;
};
type EvolutionSignal = {
signalId: string;
kind: string;
requestText: string;
createdAt: string;
};
type EvolutionProposal = {
proposalId: string;
proposalType: string;
title: string;
summary: string;
status: string;
confidence: number;
riskLevel: string;
createdAt: string;
};
type EvolutionRule = {
ruleId: string;
ruleType: string;
createdAt: string;
sourceProposalId?: string;
};
function formatTime(value?: string) {
if (!value) {
return "-";
}
return new Date(value).toLocaleString("zh-CN", { hour12: false });
}
async function readJson<T>(response: Response): Promise<T> {
return (await response.json()) as T;
}
export function MasterAgentEvolutionClient({
isAdmin,
config,
signals,
proposals,
rules,
}: {
isAdmin: boolean;
config: EvolutionConfig;
signals: EvolutionSignal[];
proposals: EvolutionProposal[];
rules: EvolutionRule[];
}) {
const router = useRouter();
const [busyKey, setBusyKey] = useState<string | null>(null);
const [message, setMessage] = useState("");
async function switchMode(mode: "controlled" | "autonomous") {
setBusyKey(`mode:${mode}`);
const response = await fetch("/api/v1/master-agent/evolution/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode }),
});
const result = await readJson<{ ok: boolean; message?: string }>(response);
setBusyKey(null);
setMessage(result.ok ? `已切到 ${mode === "autonomous" ? "完全自我进化" : "受控自动进化"}` : result.message ?? "切换失败。");
if (result.ok) {
router.refresh();
}
}
async function reviewProposal(proposalId: string, action: "approve" | "reject") {
setBusyKey(`${action}:${proposalId}`);
const response = await fetch(`/api/v1/master-agent/evolution/proposals/${proposalId}/${action}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const result = await readJson<{ ok: boolean; message?: string }>(response);
setBusyKey(null);
setMessage(result.ok ? (action === "approve" ? "提案已批准。" : "提案已拒绝。") : result.message ?? "提交失败。");
if (result.ok) {
router.refresh();
}
}
const pendingProposals = proposals.filter((item) => item.status === "pending_review");
return (
<div className="flex flex-col gap-4 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] leading-6 text-[#8C8C8C]">
{config.mode === "autonomous" ? "完全自我进化" : "受控自动进化"}
{config.autoApplyLowRiskRules ? "低风险提案会自动采纳。" : "所有提案都需要人工确认。"}
</div>
</div>
<span
className={clsx(
"rounded-full px-3 py-1 text-[11px] font-semibold",
config.mode === "autonomous" ? "bg-[#EEF8F1] text-[#117A37]" : "bg-[#FFF5E8] text-[#B54708]",
)}
>
{config.mode === "autonomous" ? "全自动" : "受控"}
</span>
</div>
{isAdmin ? (
<div className="mt-4 grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void switchMode("controlled")}
disabled={busyKey === "mode:controlled" || config.mode === "controlled"}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === "mode:controlled" ? "切换中" : "切到受控模式"}
</button>
<button
type="button"
onClick={() => void switchMode("autonomous")}
disabled={busyKey === "mode:autonomous" || config.mode === "autonomous"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "mode:autonomous" ? "切换中" : "切到全自动模式"}
</button>
</div>
) : null}
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{pendingProposals.length}
</span>
</div>
<div className="mt-3 space-y-3">
{pendingProposals.length === 0 ? (
<div className="rounded-xl bg-[#F7F8FA] px-3 py-3 text-[13px] text-[#8C8C8C]"></div>
) : (
pendingProposals.map((proposal) => (
<div key={proposal.proposalId} className="rounded-xl border border-[#EAECEF] px-3 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[14px] font-semibold text-[#111111]">{proposal.title}</div>
<div className="mt-1 text-[12px] leading-6 text-[#57606A]">{proposal.summary}</div>
</div>
<span className="rounded-full bg-[#FFF5E8] px-2 py-1 text-[11px] font-semibold text-[#B54708]">
{proposal.riskLevel} / {Math.round(proposal.confidence * 100)}%
</span>
</div>
<div className="mt-2 text-[12px] text-[#8C8C8C]">
{proposal.proposalType} · {formatTime(proposal.createdAt)}
</div>
{isAdmin ? (
<div className="mt-3 grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void reviewProposal(proposal.proposalId, "reject")}
disabled={busyKey === `reject:${proposal.proposalId}`}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === `reject:${proposal.proposalId}` ? "处理中" : "拒绝"}
</button>
<button
type="button"
onClick={() => void reviewProposal(proposal.proposalId, "approve")}
disabled={busyKey === `approve:${proposal.proposalId}`}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === `approve:${proposal.proposalId}` ? "处理中" : "批准"}
</button>
</div>
) : null}
</div>
))
)}
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{signals.length}
</span>
</div>
<div className="mt-3 space-y-3">
{signals.slice(0, 8).map((signal) => (
<div key={signal.signalId} className="rounded-xl bg-[#F7F8FA] px-3 py-3">
<div className="text-[13px] font-semibold text-[#111111]">{signal.kind}</div>
<div className="mt-1 text-[12px] leading-6 text-[#57606A]">{signal.requestText}</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">{formatTime(signal.createdAt)}</div>
</div>
))}
</div>
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{rules.length}
</span>
</div>
<div className="mt-3 space-y-3">
{rules.slice(0, 8).map((rule) => (
<div key={rule.ruleId} className="rounded-xl bg-[#F7F8FA] px-3 py-3">
<div className="text-[13px] font-semibold text-[#111111]">{rule.ruleType}</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">
{formatTime(rule.createdAt)}
{rule.sourceProposalId ? ` · ${rule.sourceProposalId}` : ""}
</div>
</div>
))}
</div>
</div>
</div>
{message ? (
<div className="rounded-2xl border border-[#D8E8FF] bg-[#F4F8FF] px-4 py-3 text-[13px] text-[#2457C5]">
{message}
</div>
) : null}
</div>
);
}

View File

@@ -1726,10 +1726,6 @@ type MasterAgentFastIntentContext = {
effectiveDeepTaskPolicy: ReturnType<typeof resolveMasterAgentModelPolicy>;
};
function buildMasterAgentRuntimeBackendLabel(context: MasterAgentFastIntentContext) {
return context.agentControls?.backendOverride?.trim() || "master-codex-node";
}
function getMasterAgentRuntimeDevice(context: MasterAgentFastIntentContext) {
const deviceId =
context.runtime.account.nodeId?.trim() ||
@@ -1952,7 +1948,7 @@ async function appendFastPathError(
}
async function tryRecordMasterAgentEvolutionSignal(input: {
kind: "fast_path_candidate" | "user_correction" | "backend_fallback";
kind: "fast_path_candidate" | "repeated_question" | "user_correction" | "backend_fallback";
account: string;
requestText: string;
replyText?: string;
@@ -1972,6 +1968,47 @@ async function tryRecordMasterAgentEvolutionSignal(input: {
}
}
function isMasterAgentEvolutionStatusLikeRequest(requestText: string) {
return /(当前|现在|有没有|是否|哪个|什么|在线吗|状态)/i.test(requestText);
}
function normalizeMasterAgentEvolutionQuestion(requestText: string) {
return normalizeLexicalText(requestText)
.replace(/当前|现在|目前|一下|帮我|请问|请/g, "")
.replace(/吗|呢|呀|啊/g, "")
.trim();
}
async function isRepeatedMasterAgentStatusQuestion(params: {
requestText: string;
requestMessageId?: string;
}) {
const normalizedRequest = normalizeMasterAgentEvolutionQuestion(params.requestText);
if (!normalizedRequest) {
return false;
}
const state = await readState();
const masterProject = state.projects.find((project) => project.id === "master-agent");
if (!masterProject) {
return false;
}
const priorUserMessages = [...masterProject.messages]
.reverse()
.filter((message) => message.sender === "user" && message.id !== params.requestMessageId)
.slice(0, 8);
return priorUserMessages.some((message) => {
const normalizedBody = normalizeMasterAgentEvolutionQuestion(message.body);
if (!normalizedBody) {
return false;
}
return (
normalizedBody === normalizedRequest ||
normalizedBody.includes(normalizedRequest) ||
normalizedRequest.includes(normalizedBody)
);
});
}
function buildModelSummaryReply(context: MasterAgentFastIntentContext, requestText: string) {
const normalized = normalizeLexicalText(requestText);
const manualModel = context.agentControls?.modelOverride?.trim() || "";
@@ -3250,6 +3287,22 @@ export async function replyToMasterAgentUserMessage(params: {
currentSessionExpiresAt?: string;
mode?: "wait" | "enqueue";
}) {
if (isMasterAgentEvolutionStatusLikeRequest(params.requestText)) {
const repeatedQuestion = await isRepeatedMasterAgentStatusQuestion({
requestText: params.requestText,
requestMessageId: params.requestMessageId,
});
await tryRecordMasterAgentEvolutionSignal({
kind: repeatedQuestion ? "repeated_question" : "fast_path_candidate",
account: params.requestedByAccount,
requestText: params.requestText,
metadataJson: {
source: "replyToMasterAgentUserMessage.pre_fast_intent",
repeatedQuestion,
},
});
}
const fastIntentResult = await tryHandleMasterAgentFastIntent({
requestText: params.requestText,
requestedByAccount: params.requestedByAccount,
@@ -3258,17 +3311,6 @@ export async function replyToMasterAgentUserMessage(params: {
return fastIntentResult;
}
if (/(当前|现在|有没有|是否|哪个|什么|在线吗|状态)/i.test(params.requestText)) {
await tryRecordMasterAgentEvolutionSignal({
kind: "fast_path_candidate",
account: params.requestedByAccount,
requestText: params.requestText,
metadataJson: {
source: "replyToMasterAgentUserMessage.pre_slow_path",
},
});
}
const runtime = await getMasterAgentRuntimeAccount();
if (!runtime?.account) {
@@ -3311,6 +3353,22 @@ export async function replyToMasterAgentUserMessage(params: {
};
const selectedBackend = await selectExecutionBackend(backendSelectionInput);
const backendChoices = listExecutionBackendChoices(backendSelectionInput);
const requestedBackendId = executionConfig.agentControls?.backendOverride?.trim() || "";
if (
requestedBackendId &&
selectedBackend.backendId !== requestedBackendId &&
(selectedBackend.backendId === CLAW_BACKEND_ID || selectedBackend.backendId === HERMES_BACKEND_ID)
) {
await tryRecordMasterAgentEvolutionSignal({
kind: "backend_fallback",
account: params.requestedByAccount,
requestText: params.requestText,
metadataJson: {
requestedBackendId,
fallbackToBackendId: selectedBackend.backendId,
},
});
}
const agentControls = executionConfig.agentControls;
const masterExecutionPrompt = buildMasterCodexNodePrompt(
state,

View File

@@ -6,11 +6,12 @@ export const MASTER_AGENT_CHAT_PAGE_ANCHORS = {
} as const;
export const MASTER_AGENT_TAKEOVER_PAGE_HREF = "/me/master-agent/takeover";
export const MASTER_AGENT_EVOLUTION_PAGE_HREF = "/me/master-agent/evolution";
export type MasterAgentChatPageAnchors = typeof MASTER_AGENT_CHAT_PAGE_ANCHORS;
export type MasterAgentChatMenuItem = {
key: "prompt" | "model" | "reasoning_effort" | "takeover" | "memory" | "refresh";
key: "prompt" | "model" | "reasoning_effort" | "takeover" | "evolution" | "memory" | "refresh";
label: string;
href?: string;
action?: "refresh";
@@ -37,6 +38,11 @@ export function getMasterAgentChatMenuItems(projectId: string): MasterAgentChatM
label: "全局接管",
href: MASTER_AGENT_TAKEOVER_PAGE_HREF,
},
{
key: "evolution",
label: "自动进化",
href: MASTER_AGENT_EVOLUTION_PAGE_HREF,
},
{
key: "prompt",
label: "提示词",

View File

@@ -31,6 +31,7 @@ function inferProposalFromSignal(input: {
projectId: string;
requestText: string;
signalId: string;
metadataJson?: Record<string, unknown>;
}): Omit<
Parameters<typeof createMasterAgentEvolutionProposalInState>[0],
"status"
@@ -76,6 +77,11 @@ function inferProposalFromSignal(input: {
}
if (input.kind === "backend_fallback") {
const fallbackToBackendId =
input.metadataJson?.fallbackToBackendId === "hermes-runtime" ||
input.metadataJson?.fallbackToBackendId === "claw-runtime"
? input.metadataJson.fallbackToBackendId
: undefined;
return {
proposalType: "routing_preference_patch",
account: input.account,
@@ -84,9 +90,10 @@ function inferProposalFromSignal(input: {
summary: "检测到后端回退,建议后续优先选择最近稳定的可用后端。",
patchJson: {
backendPreference: "prefer_available_runtime",
backendOverride: fallbackToBackendId,
},
sourceSignalIds: [input.signalId],
confidence: 0.72,
confidence: fallbackToBackendId ? 0.8 : 0.72,
riskLevel: "low",
};
}
@@ -193,6 +200,7 @@ export async function recordMasterAgentEvolutionSignal(input: {
projectId: signal.projectId,
requestText: signal.requestText,
signalId: signal.signalId,
metadataJson: signal.metadataJson,
});
if (!proposalInput) {
return { signal, proposal: null };

View File

@@ -34,6 +34,7 @@ test("master agent settings pages refresh when master agent config changes", asy
for (const relativePath of [
"src/app/me/master-agent/page.tsx",
"src/app/me/master-agent/takeover/page.tsx",
"src/app/me/master-agent/evolution/page.tsx",
]) {
const source = await readSource(relativePath);
assert.match(source, /import \{ RealtimeRefresh \}/, `expected ${relativePath} to import RealtimeRefresh`);
@@ -45,3 +46,9 @@ test("master agent settings pages refresh when master agent config changes", asy
);
}
});
test("me page exposes master agent evolution entry", async () => {
const source = await readSource("src/app/me/page.tsx");
assert.match(source, /href="\/me\/master-agent\/evolution"/, "expected me page to link evolution page");
assert.match(source, /title="主 Agent 自动进化"/, "expected me page to show evolution menu title");
});

View File

@@ -2,18 +2,19 @@ import test from "node:test";
import assert from "node:assert/strict";
import { getMasterAgentChatMenuItems } from "../src/lib/master-agent-chat-menu";
test("master-agent 聊天页菜单包含全局接管、提示词、模型、推理强度、记忆和刷新", () => {
test("master-agent 聊天页菜单包含全局接管、进化、提示词、模型、推理强度、记忆和刷新", () => {
const items = getMasterAgentChatMenuItems("master-agent");
assert.deepEqual(
items.map((item) => item.key),
["model", "reasoning_effort", "takeover", "prompt", "memory", "refresh"],
["model", "reasoning_effort", "takeover", "evolution", "prompt", "memory", "refresh"],
);
assert.equal(items[0]?.href, "/me/master-agent#model-section");
assert.equal(items[1]?.href, "/me/master-agent#reasoning-effort-section");
assert.equal(items[2]?.href, "/me/master-agent/takeover");
assert.equal(items[3]?.href, "/me/master-agent#prompt-section");
assert.equal(items[4]?.href, "/me/master-agent#memory-section");
assert.equal(items[5]?.action, "refresh");
assert.equal(items[3]?.href, "/me/master-agent/evolution");
assert.equal(items[4]?.href, "/me/master-agent#prompt-section");
assert.equal(items[5]?.href, "/me/master-agent#memory-section");
assert.equal(items[6]?.action, "refresh");
});
test("普通会话不返回主 Agent 专属菜单", () => {

View File

@@ -6,6 +6,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
let runtimeRoot = "";
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let getProjectAgentControls: (typeof import("../src/lib/boss-data"))["getProjectAgentControls"];
let recordMasterAgentEvolutionSignal: (typeof import("../src/lib/master-agent-evolution"))["recordMasterAgentEvolutionSignal"];
let listMasterAgentEvolutionProposals: (typeof import("../src/lib/master-agent-evolution"))["listMasterAgentEvolutionProposals"];
let setMasterAgentEvolutionMode: (typeof import("../src/lib/master-agent-evolution"))["setMasterAgentEvolutionMode"];
@@ -22,6 +23,7 @@ async function setup() {
import("../src/lib/master-agent-evolution.ts"),
]);
readState = data.readState;
getProjectAgentControls = data.getProjectAgentControls;
recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal;
listMasterAgentEvolutionProposals = evolution.listMasterAgentEvolutionProposals;
setMasterAgentEvolutionMode = evolution.setMasterAgentEvolutionMode;
@@ -73,3 +75,25 @@ test("autonomous mode auto applies low risk fast path proposals as evolution rul
assert.equal(state.masterAgentEvolutionRules.length, 1);
assert.equal(state.masterAgentEvolutionRules[0]?.ruleType, "fast_path_rule");
});
test("autonomous mode auto applies backend fallback proposals into master-agent backend override", async () => {
await setMasterAgentEvolutionMode("autonomous");
const result = await recordMasterAgentEvolutionSignal({
kind: "backend_fallback",
projectId: "master-agent",
account: "17600003315",
requestText: "Hermes 不可用时自动回退到 Claw",
metadataJson: {
failedBackendId: "hermes-runtime",
fallbackToBackendId: "claw-runtime",
},
});
assert.equal(result.proposal?.status, "auto_applied");
const state = await readState();
const controls = await getProjectAgentControls("master-agent", "17600003315");
assert.equal(state.masterAgentEvolutionRules.length, 1);
assert.equal(state.masterAgentEvolutionRules[0]?.ruleType, "routing_preference_patch");
assert.equal(controls?.backendOverride ?? null, "claw-runtime");
});

View File

@@ -359,6 +359,43 @@ test("master-agent 查询当前后端时直接走 fast path 返回后端摘要",
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4");
});
test("master-agent 重复追问状态类问题时会记录 repeated_question 进化信号", async () => {
await saveAiAccount({
accountId: "openai-repeat-status",
label: "OpenAI 主模型",
role: "primary",
provider: "openai_api",
displayName: "OpenAI 主模型",
model: "gpt-5.4",
apiKey: "sk-openai-repeat-status",
enabled: true,
setActive: true,
loginStatusNote: "用于重复状态问题测试。",
});
const firstResponse = await POST(
await createAuthedRequest("master-agent", {
body: "当前主节点在线吗",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(firstResponse.status, 200);
const secondResponse = await POST(
await createAuthedRequest("master-agent", {
body: "现在主节点在线吗",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(secondResponse.status, 200);
const state = await readState();
assert.ok(
state.masterAgentEvolutionSignals.some((signal) => signal.kind === "repeated_question"),
"expected at least one repeated_question signal",
);
});
test("master-agent 查询全局接管状态时直接走 fast path 返回当前状态", async () => {
await saveAiAccount({
accountId: "openai-takeover-status",