Integrate master agent runtime orchestration updates
This commit is contained in:
@@ -8,6 +8,7 @@ import { NextRequest } from "next/server";
|
||||
let runtimeRoot = "";
|
||||
let POST: (typeof import("../src/app/api/v1/projects/[projectId]/messages/route"))["POST"];
|
||||
let saveAiAccount: (typeof import("../src/lib/boss-data"))["saveAiAccount"];
|
||||
let getProjectAgentControls: (typeof import("../src/lib/boss-data"))["getProjectAgentControls"];
|
||||
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
||||
@@ -30,6 +31,7 @@ async function setup() {
|
||||
|
||||
POST = messageRoute.POST;
|
||||
saveAiAccount = data.saveAiAccount;
|
||||
getProjectAgentControls = data.getProjectAgentControls;
|
||||
updateProjectAgentControls = data.updateProjectAgentControls;
|
||||
readState = data.readState;
|
||||
createAuthSession = data.createAuthSession;
|
||||
@@ -77,6 +79,240 @@ test.beforeEach(async () => {
|
||||
await mkdir(runtimeRoot, { recursive: true });
|
||||
});
|
||||
|
||||
test("master-agent 明确查询可用模型时直接本地返回模型清单而不进入异步队列", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-model-list",
|
||||
label: "OpenAI 主账号",
|
||||
role: "primary",
|
||||
provider: "openai_api",
|
||||
displayName: "OpenAI 主账号",
|
||||
model: "gpt-5.4",
|
||||
apiKey: "sk-openai-model-list",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "用于模型清单测试。",
|
||||
});
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "qwen-model-list",
|
||||
label: "Qwen 备用",
|
||||
role: "backup",
|
||||
provider: "aliyun_qwen_api",
|
||||
displayName: "阿里百炼",
|
||||
model: "qwen3.5-plus",
|
||||
apiKey: "sk-qwen-model-list",
|
||||
enabled: true,
|
||||
setActive: false,
|
||||
loginStatusNote: "用于模型清单测试。",
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "主 Agent,现在有哪些模型可以用?",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
|
||||
const state = await readState();
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
const reply = masterProject?.messages.at(-1);
|
||||
assert.ok(reply, "expected the master-agent model list reply to be persisted");
|
||||
assert.match(reply?.body ?? "", /当前可用模型/);
|
||||
assert.match(reply?.body ?? "", /gpt-5\.4/);
|
||||
assert.match(reply?.body ?? "", /qwen3\.5-plus/);
|
||||
});
|
||||
|
||||
test("master-agent 明确要求切快模型时直接更新 controls 并返回完成态", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-fast-switch",
|
||||
label: "OpenAI 快模型",
|
||||
role: "primary",
|
||||
provider: "openai_api",
|
||||
displayName: "OpenAI 快模型",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "sk-openai-fast-switch",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "用于快模型切换测试。",
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "帮我把快模型切到 gpt-5.4-mini",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
|
||||
const controls = await getProjectAgentControls("master-agent", "17600003315");
|
||||
assert.equal(controls?.fastModelOverride ?? null, "gpt-5.4-mini");
|
||||
|
||||
const state = await readState();
|
||||
assert.equal(state.masterAgentTasks.length, 0);
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
const reply = masterProject?.messages.at(-1);
|
||||
assert.ok(reply, "expected the master-agent model switch reply to be persisted");
|
||||
assert.match(reply?.body ?? "", /快模型/);
|
||||
assert.match(reply?.body ?? "", /gpt-5\.4-mini/);
|
||||
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4-mini");
|
||||
});
|
||||
|
||||
test("master-agent 识别自然写法的模型名并切当前主模型", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-main-switch",
|
||||
label: "OpenAI 主模型",
|
||||
role: "primary",
|
||||
provider: "openai_api",
|
||||
displayName: "OpenAI 主模型",
|
||||
model: "gpt-5.4",
|
||||
apiKey: "sk-openai-main-switch",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "用于主模型自然写法切换测试。",
|
||||
});
|
||||
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "把主agent模型换成gpt5.4",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
|
||||
const controls = await getProjectAgentControls("master-agent", "17600003315");
|
||||
assert.equal(controls?.modelOverride ?? null, "gpt-5.4");
|
||||
|
||||
const state = await readState();
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
const reply = masterProject?.messages.at(-1);
|
||||
assert.ok(reply, "expected the master-agent natural model switch reply to be persisted");
|
||||
assert.match(reply?.body ?? "", /当前主模型/);
|
||||
assert.match(reply?.body ?? "", /gpt-5\.4/);
|
||||
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4");
|
||||
});
|
||||
|
||||
test("master-agent 查询当前是什么大模型时直接走 fast path 返回当前模型摘要", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-fast-query",
|
||||
label: "OpenAI 主模型",
|
||||
role: "primary",
|
||||
provider: "openai_api",
|
||||
displayName: "OpenAI 主模型",
|
||||
model: "gpt-5.4",
|
||||
apiKey: "sk-openai-fast-query",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "用于当前模型查询测试。",
|
||||
});
|
||||
await updateProjectAgentControls(
|
||||
"master-agent",
|
||||
{
|
||||
fastModelOverride: "gpt-5.4-mini",
|
||||
smartModelOverride: "gpt-5.4",
|
||||
},
|
||||
"17600003315",
|
||||
);
|
||||
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "你现在是什么大模型",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
|
||||
const state = await readState();
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
const reply = masterProject?.messages.at(-1);
|
||||
assert.ok(reply, "expected the master-agent fast model summary reply to be persisted");
|
||||
assert.match(reply?.body ?? "", /当前聊天模型:gpt-5\.4-mini/);
|
||||
assert.match(reply?.body ?? "", /强模型:gpt-5\.4/);
|
||||
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4-mini");
|
||||
});
|
||||
|
||||
test("master-agent 查询当前后端时直接走 fast path 返回后端摘要", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-backend-query",
|
||||
label: "OpenAI 主模型",
|
||||
role: "primary",
|
||||
provider: "openai_api",
|
||||
displayName: "OpenAI 主模型",
|
||||
model: "gpt-5.4",
|
||||
apiKey: "sk-openai-backend-query",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "用于后端查询测试。",
|
||||
});
|
||||
await updateProjectAgentControls(
|
||||
"master-agent",
|
||||
{
|
||||
backendOverride: "hermes-runtime",
|
||||
},
|
||||
"17600003315",
|
||||
);
|
||||
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "当前后端是什么",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
|
||||
const state = await readState();
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
const reply = masterProject?.messages.at(-1);
|
||||
assert.ok(reply, "expected the master-agent backend summary reply to be persisted");
|
||||
assert.match(reply?.body ?? "", /当前后端:hermes-runtime/);
|
||||
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4");
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/master-agent/messages 快速返回队列态并在异步实际回复时继承当前会话覆盖", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-master-agent-queue",
|
||||
@@ -122,6 +358,7 @@ test("POST /api/v1/projects/master-agent/messages 快速返回队列态并在异
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
message: { id: string };
|
||||
task?: { taskId: string; taskType: string; status: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed";
|
||||
masterReply?: { accountId?: string } | null;
|
||||
@@ -134,6 +371,7 @@ test("POST /api/v1/projects/master-agent/messages 快速返回队列态并在异
|
||||
assert.equal(payload.task?.taskType, "conversation_reply");
|
||||
assert.equal(payload.task?.status, "queued");
|
||||
assert.ok(payload.task?.taskId, "expected a stable taskId in the response");
|
||||
assert.equal((payload.task as { requestMessageId?: string } | null)?.requestMessageId, payload.message.id);
|
||||
|
||||
await waitFor(async () => {
|
||||
const state = await readState();
|
||||
@@ -333,6 +571,96 @@ test("master-agent enqueue 在显式选择 claw-runtime 时会通过 Claw 异步
|
||||
}
|
||||
});
|
||||
|
||||
test("master-agent enqueue 在显式选择 hermes-runtime 时会通过 Hermes 异步回写回复", async () => {
|
||||
const hermesDir = await mkdtemp(path.join(os.tmpdir(), "boss-hermes-queue-"));
|
||||
const hermesScriptPath = path.join(hermesDir, "hermes-runtime.mjs");
|
||||
await writeFile(
|
||||
hermesScriptPath,
|
||||
`
|
||||
const args = process.argv.slice(2);
|
||||
const queryIndex = args.findIndex((item) => item === "-q" || item === "--query");
|
||||
const query = queryIndex >= 0 ? args[queryIndex + 1] ?? "" : "";
|
||||
process.stdout.write("Hermes 已接管当前主 Agent 会话:" + query + "\\n\\n");
|
||||
process.stdout.write("session_id: hermes-session-123\\n");
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const previousEnv = {
|
||||
BOSS_HERMES_ENABLED: process.env.BOSS_HERMES_ENABLED,
|
||||
BOSS_HERMES_COMMAND: process.env.BOSS_HERMES_COMMAND,
|
||||
BOSS_HERMES_ARGS: process.env.BOSS_HERMES_ARGS,
|
||||
BOSS_HERMES_TIMEOUT_MS: process.env.BOSS_HERMES_TIMEOUT_MS,
|
||||
};
|
||||
process.env.BOSS_HERMES_ENABLED = "true";
|
||||
process.env.BOSS_HERMES_COMMAND = process.execPath;
|
||||
process.env.BOSS_HERMES_ARGS = hermesScriptPath;
|
||||
process.env.BOSS_HERMES_TIMEOUT_MS = "1000";
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-hermes",
|
||||
label: "主 GPT",
|
||||
role: "primary",
|
||||
provider: "master_codex_node",
|
||||
displayName: "Mac 上的 Master Codex Node",
|
||||
nodeId: "local-codex-node",
|
||||
nodeLabel: "本机 Codex",
|
||||
model: "gpt-5.4",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "用于 Hermes backend 队列测试。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
backendOverride: "hermes-runtime",
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "请走 Hermes runtime",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string; status: string } | null;
|
||||
masterReply?: { accountId?: string } | null;
|
||||
masterReplyState?: string | null;
|
||||
};
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.masterReply?.accountId, "hermes-runtime");
|
||||
assert.equal(payload.masterReplyState, "queued");
|
||||
assert.ok(payload.task?.taskId);
|
||||
|
||||
await waitFor(async () => {
|
||||
const state = await readState();
|
||||
const task = state.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
|
||||
return task?.status === "completed";
|
||||
});
|
||||
|
||||
const nextState = await readState();
|
||||
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
|
||||
assert.equal(task?.status, "completed");
|
||||
assert.match(task?.replyBody ?? "", /Hermes 已接管当前主 Agent 会话:/);
|
||||
assert.match(task?.replyBody ?? "", /请走 Hermes runtime/);
|
||||
assert.equal(task?.sessionId, "hermes-session-123");
|
||||
|
||||
const masterProject = nextState.projects.find((project) => project.id === "master-agent");
|
||||
const mirroredReply = masterProject?.messages.at(-1);
|
||||
assert.match(mirroredReply?.body ?? "", /Hermes 已接管当前主 Agent 会话/);
|
||||
} finally {
|
||||
process.env.BOSS_HERMES_ENABLED = previousEnv.BOSS_HERMES_ENABLED;
|
||||
process.env.BOSS_HERMES_COMMAND = previousEnv.BOSS_HERMES_COMMAND;
|
||||
process.env.BOSS_HERMES_ARGS = previousEnv.BOSS_HERMES_ARGS;
|
||||
process.env.BOSS_HERMES_TIMEOUT_MS = previousEnv.BOSS_HERMES_TIMEOUT_MS;
|
||||
await rm(hermesDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("master-agent enqueue 在首选主节点离线时会回退到可用的备用主节点并返回实际账号", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-offline",
|
||||
|
||||
Reference in New Issue
Block a user