feat: expose master agent evolution dashboard
This commit is contained in:
@@ -155,6 +155,7 @@
|
||||
- `版本迭代记录` 只读,由主 Agent 汇总
|
||||
- `我的` 根页当前保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于`
|
||||
- `我的 > 主 Agent 提示词 / 记忆` 当前可编辑管理员全局主提示词、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 项目记忆
|
||||
- `我的 > 主 Agent 自动进化` 当前可查看进化信号、待审批提案、已生效规则,并切换 `controlled / autonomous`
|
||||
- `我的 > AI 账号` 必须可查看和切换 `主 GPT / 备用 GPT / API 容灾`
|
||||
- `我的 > 技能` 必须按绑定设备展示 Skill,并支持一键复制调用语句
|
||||
- `设备` 页当前只允许出现生产设备,旧演示脏数据不能回流到正式视图
|
||||
|
||||
@@ -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 提示
|
||||
|
||||
36
src/app/me/master-agent/evolution/page.tsx
Normal file
36
src/app/me/master-agent/evolution/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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="附件与存储"
|
||||
|
||||
237
src/components/master-agent-evolution-client.tsx
Normal file
237
src/components/master-agent-evolution-client.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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: "提示词",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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 专属菜单", () => {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user