diff --git a/docs/superpowers/specs/2026-04-16-master-agent-fast-path-design.md b/docs/superpowers/specs/2026-04-16-master-agent-fast-path-design.md index 4cf370f..b73b341 100644 --- a/docs/superpowers/specs/2026-04-16-master-agent-fast-path-design.md +++ b/docs/superpowers/specs/2026-04-16-master-agent-fast-path-design.md @@ -85,11 +85,33 @@ Android 主 Agent 会话页顶部标题同步显示: 下一批适合继续接入 Fast Path 的问题: -- 当前绑定设备 / 在线设备查询 - 当前会话 / 当前线程运行状态查询 -- GUI / CLI 默认执行模式查询 -- 当前接管开关状态查询 - 最近活跃项目 / 最近活跃线程查询 +- 更细粒度的线程级接管控制与作用域切换 + +## 6.1 第二批已接入意图 + +本次继续把主 Agent 的“控制面”高频问题接入快路径: + +- 全局接管状态查询 + - 例:“当前有没有开启主agent接管” +- 全局接管开关 + - 例:“帮我开启全局接管” + - 例:“关闭全局接管” +- 默认后端切换 + - 例:“把默认后端切到 Hermes” + - 例:“切到 Claw 后端” +- 默认执行模式查询 + - 例:“现在默认走 GUI 还是 CLI” +- 当前主节点 / 绑定设备在线状态查询 + - 例:“当前主节点在线吗” + +这批实现原则是: + +- 只处理确定性、纯本地状态可回答的问题 +- 不进入异步任务队列 +- 先命中接管 / 后端 / 执行模式,再命中模型切换,避免“切到 Hermes 后端”被模型切换规则误判 +- `GUI 还是 CLI` 与 `Hermes 还是 Claw` 分属不同意图,不再混用一个正则 ## 7. 验证基线 @@ -99,3 +121,11 @@ Android 主 Agent 会话页顶部标题同步显示: - `npm run build` - `./gradlew :app:compileDebugJavaWithJavac :app:assembleDebug` - 真机安装并验证主 Agent 名称与模型查询行为 + +第二批补充验证点: + +- `npx tsx --test tests/master-agent-message-queue.test.ts` +- 覆盖全局接管查询 / 切换 +- 覆盖默认后端切换 +- 覆盖 GUI/CLI 执行模式查询 +- 覆盖主节点设备在线状态查询 diff --git a/src/lib/boss-master-agent.ts b/src/lib/boss-master-agent.ts index bf350c7..a954b51 100644 --- a/src/lib/boss-master-agent.ts +++ b/src/lib/boss-master-agent.ts @@ -43,6 +43,8 @@ import { createHermesBackend, getHermesBackendSelectionState, } from "@/lib/execution/backends/hermes-backend"; +import { getClawBackendAvailability } from "@/lib/execution/backends/claw-config"; +import { getHermesBackendAvailability } from "@/lib/execution/backends/hermes-config"; import { getOmxTeamBackendSelectionState } from "@/lib/execution/backends/omx-team-backend"; import type { OrchestrationBackendId } from "@/lib/execution/orchestration-backend"; import { listExecutionBackendChoices, selectExecutionBackend } from "@/lib/execution/backend-selector"; @@ -1723,6 +1725,21 @@ type MasterAgentFastIntentContext = { effectiveDeepTaskPolicy: ReturnType; }; +function buildMasterAgentRuntimeBackendLabel(context: MasterAgentFastIntentContext) { + return context.agentControls?.backendOverride?.trim() || "master-codex-node"; +} + +function getMasterAgentRuntimeDevice(context: MasterAgentFastIntentContext) { + const deviceId = + context.runtime.account.nodeId?.trim() || + context.state.user.boundDeviceId?.trim() || + ""; + if (!deviceId) { + return null; + } + return context.state.devices.find((device) => device.id === deviceId) ?? null; +} + function detectMasterAgentModelCommandScope(requestText: string): MasterAgentModelCommandScope { const normalized = normalizeLexicalText(requestText); if ( @@ -1791,11 +1808,76 @@ function isBackendStatusRequest(requestText: string) { if (!normalized || isModelSwitchRequest(requestText)) { return false; } - return /(什么后端|哪个后端|当前后端|现在后端|正在用什么后端|当前用什么后端|gui还是cli|hermes还是claw|claw还是hermes)/i.test( + return /(什么后端|哪个后端|当前后端|现在后端|正在用什么后端|当前用什么后端|hermes还是claw|claw还是hermes)/i.test( normalized, ); } +function isBackendSwitchRequest(requestText: string) { + const normalized = normalizeLexicalText(requestText); + if (!normalized) { + return false; + } + return /(后端|runtime)/i.test(normalized) && /(切到|切换到|换成|改成|改为|用|使用)/i.test(normalized); +} + +function detectRequestedBackend(requestText: string) { + const normalized = normalizeLexicalText(requestText); + if (normalized.includes("hermes")) { + return HERMES_BACKEND_ID; + } + if (normalized.includes("claw")) { + return CLAW_BACKEND_ID; + } + return ""; +} + +function isTakeoverStatusRequest(requestText: string) { + const normalized = normalizeLexicalText(requestText); + if (!normalized) { + return false; + } + return /(接管|协同接管)/i.test(normalized) && /(有没有开启|是否开启|开了吗|状态|现在开着吗|当前开着吗)/i.test(normalized); +} + +function isTakeoverSwitchRequest(requestText: string) { + const normalized = normalizeLexicalText(requestText); + if (!normalized) { + return false; + } + if (/(有没有开启|是否开启|开了吗|状态|现在开着吗|当前开着吗)/i.test(normalized)) { + return false; + } + return /(接管|协同接管)/i.test(normalized) && /(开启|打开|关闭|关掉|停用|禁用)/i.test(normalized); +} + +function detectRequestedTakeoverEnabled(requestText: string) { + const normalized = normalizeLexicalText(requestText); + if (/(关闭|关掉|停用|禁用)/i.test(normalized)) { + return false; + } + if (/(开启|打开)/i.test(normalized)) { + return true; + } + return null; +} + +function isExecutionModeStatusRequest(requestText: string) { + const normalized = normalizeLexicalText(requestText); + if (!normalized) { + return false; + } + return /(gui\s*还是\s*cli|默认走.*gui.*cli|默认执行模式|执行模式|gui\s*cli)/i.test(normalized); +} + +function isBoundDeviceStatusRequest(requestText: string) { + const normalized = normalizeLexicalText(requestText); + if (!normalized) { + return false; + } + return /(绑定设备|主节点|master节点|codex节点)/i.test(normalized) && /(在线吗|在不在线|是否在线|状态)/i.test(normalized); +} + async function buildMasterAgentFastIntentContext( requestedByAccount: string, ): Promise { @@ -1934,6 +2016,140 @@ async function tryHandleMasterAgentBackendStatusQuery(params: { ); } +async function tryHandleMasterAgentBackendSwitchCommand(params: { + requestText: string; + requestedByAccount: string; + context: MasterAgentFastIntentContext; +}) { + if (!isBackendSwitchRequest(params.requestText)) { + return null; + } + const requestedBackend = detectRequestedBackend(params.requestText); + if (!requestedBackend) { + return appendFastPathError( + "我收到的是后端切换请求,但没有识别到具体后端。你可以直接说“切到 Hermes 后端”或“切到 Claw 后端”。", + "BACKEND_NAME_REQUIRED", + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); + } + + if (requestedBackend === HERMES_BACKEND_ID) { + const availability = await getHermesBackendAvailability(); + if (!availability.selectable) { + return appendFastPathError( + `Hermes Runtime 当前不可切换:${availability.reasonLabel}`, + "BACKEND_NOT_AVAILABLE", + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); + } + } + + if (requestedBackend === CLAW_BACKEND_ID) { + const availability = await getClawBackendAvailability(); + if (!availability.selectable) { + return appendFastPathError( + `Claw Runtime 当前不可切换:${availability.reasonLabel}`, + "BACKEND_NOT_AVAILABLE", + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); + } + } + + await updateProjectAgentControls( + "master-agent", + { backendOverride: requestedBackend }, + params.requestedByAccount, + ); + return appendFastPathReply( + `已把默认后端切到 ${requestedBackend}。`, + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); +} + +async function tryHandleMasterAgentTakeoverCommand(params: { + requestText: string; + requestedByAccount: string; + context: MasterAgentFastIntentContext; +}) { + const isStatus = isTakeoverStatusRequest(params.requestText); + const isSwitch = isTakeoverSwitchRequest(params.requestText); + if (!isStatus && !isSwitch) { + return null; + } + + const currentEnabled = Boolean(params.context.agentControls?.globalTakeoverEnabled); + if (isStatus && !isSwitch) { + return appendFastPathReply( + `全局接管:${currentEnabled ? "开启" : "关闭"}。`, + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); + } + + const nextEnabled = detectRequestedTakeoverEnabled(params.requestText); + if (nextEnabled === null) { + return appendFastPathError( + "我收到的是接管切换请求,但没有识别到要开启还是关闭。你可以直接说“开启全局接管”或“关闭全局接管”。", + "TAKEOVER_ACTION_REQUIRED", + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); + } + + await updateProjectAgentControls( + "master-agent", + { globalTakeoverEnabled: nextEnabled }, + params.requestedByAccount, + ); + return appendFastPathReply( + nextEnabled ? "已开启全局接管。" : "已关闭全局接管。", + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); +} + +async function tryHandleMasterAgentExecutionModeStatusQuery(params: { + requestText: string; + context: MasterAgentFastIntentContext; +}) { + if (!isExecutionModeStatusRequest(params.requestText)) { + return null; + } + const device = getMasterAgentRuntimeDevice(params.context); + if (!device) { + return appendFastPathReply( + "当前没有绑定可识别的主节点设备,所以还不能判断默认执行模式。", + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); + } + const preferredMode = device.preferredExecutionMode || "unknown"; + const guiStatus = device.capabilities?.gui?.connected ? "在线" : "离线"; + const cliStatus = device.capabilities?.cli?.connected ? "在线" : "离线"; + return appendFastPathReply( + `默认执行模式:${preferredMode}。设备:${device.name || device.id}。GUI:${guiStatus}。CLI:${cliStatus}。`, + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); +} + +async function tryHandleMasterAgentBoundDeviceStatusQuery(params: { + requestText: string; + context: MasterAgentFastIntentContext; +}) { + if (!isBoundDeviceStatusRequest(params.requestText)) { + return null; + } + const device = getMasterAgentRuntimeDevice(params.context); + if (!device) { + return appendFastPathReply( + "当前没有找到绑定的主节点设备记录。", + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); + } + const guiStatus = device.capabilities?.gui?.connected ? "在线" : "离线"; + const cliStatus = device.capabilities?.cli?.connected ? "在线" : "离线"; + return appendFastPathReply( + `当前主节点设备:${device.name || device.id}。设备状态:${device.status}。GUI:${guiStatus}。CLI:${cliStatus}。`, + buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), + ); +} + function isModelListRequest(requestText: string) { return /(哪些模型|什么模型|模型清单|可用模型|有哪些可用|available models?)/i.test(requestText); } @@ -2024,6 +2240,10 @@ async function tryHandleMasterAgentFastIntent(params: { return null; } const handlers = [ + () => tryHandleMasterAgentTakeoverCommand({ ...params, context }), + () => tryHandleMasterAgentBackendSwitchCommand({ ...params, context }), + () => tryHandleMasterAgentExecutionModeStatusQuery({ requestText: params.requestText, context }), + () => tryHandleMasterAgentBoundDeviceStatusQuery({ requestText: params.requestText, context }), () => tryHandleMasterAgentModelCommand({ ...params, context }), () => tryHandleMasterAgentStatusQuery({ requestText: params.requestText, context }), () => tryHandleMasterAgentBackendStatusQuery({ requestText: params.requestText, context }), diff --git a/tests/master-agent-message-queue.test.ts b/tests/master-agent-message-queue.test.ts index a676304..6c74842 100644 --- a/tests/master-agent-message-queue.test.ts +++ b/tests/master-agent-message-queue.test.ts @@ -10,6 +10,7 @@ let POST: (typeof import("../src/app/api/v1/projects/[projectId]/messages/route" 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 updateDevice: (typeof import("../src/lib/boss-data"))["updateDevice"]; let readState: (typeof import("../src/lib/boss-data"))["readState"]; let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"]; let AUTH_SESSION_COOKIE = ""; @@ -33,6 +34,7 @@ async function setup() { saveAiAccount = data.saveAiAccount; getProjectAgentControls = data.getProjectAgentControls; updateProjectAgentControls = data.updateProjectAgentControls; + updateDevice = data.updateDevice; readState = data.readState; createAuthSession = data.createAuthSession; AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE; @@ -357,6 +359,252 @@ test("master-agent 查询当前后端时直接走 fast path 返回后端摘要", assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4"); }); +test("master-agent 查询全局接管状态时直接走 fast path 返回当前状态", async () => { + await saveAiAccount({ + accountId: "openai-takeover-status", + label: "OpenAI 主模型", + role: "primary", + provider: "openai_api", + displayName: "OpenAI 主模型", + model: "gpt-5.4-mini", + apiKey: "sk-openai-takeover-status", + enabled: true, + setActive: true, + loginStatusNote: "用于全局接管状态查询测试。", + }); + await updateProjectAgentControls( + "master-agent", + { + globalTakeoverEnabled: true, + }, + "17600003315", + ); + + 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); + assert.match(reply?.body ?? "", /全局接管:开启/); + assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4-mini"); +}); + +test("master-agent 可以直接通过 fast path 开启全局接管", async () => { + await saveAiAccount({ + accountId: "openai-takeover-switch", + label: "OpenAI 主模型", + role: "primary", + provider: "openai_api", + displayName: "OpenAI 主模型", + model: "gpt-5.4-mini", + apiKey: "sk-openai-takeover-switch", + enabled: true, + setActive: true, + loginStatusNote: "用于全局接管切换测试。", + }); + + 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 controls = await getProjectAgentControls("master-agent", "17600003315"); + assert.equal(controls?.globalTakeoverEnabled ?? null, true); + + const state = await readState(); + const masterProject = state.projects.find((project) => project.id === "master-agent"); + const reply = masterProject?.messages.at(-1); + assert.ok(reply); + assert.match(reply?.body ?? "", /已开启全局接管/); +}); + +test("master-agent 可以直接通过 fast path 切换默认后端到 Hermes", async () => { + const previousEnv = { + BOSS_HERMES_ENABLED: process.env.BOSS_HERMES_ENABLED, + BOSS_HERMES_COMMAND: process.env.BOSS_HERMES_COMMAND, + }; + process.env.BOSS_HERMES_ENABLED = "true"; + process.env.BOSS_HERMES_COMMAND = process.execPath; + + await saveAiAccount({ + accountId: "openai-backend-switch", + label: "OpenAI 主模型", + role: "primary", + provider: "openai_api", + displayName: "OpenAI 主模型", + model: "gpt-5.4", + apiKey: "sk-openai-backend-switch", + enabled: true, + setActive: true, + loginStatusNote: "用于后端切换测试。", + }); + + try { + const response = await POST( + await createAuthedRequest("master-agent", { + body: "把默认后端切到 Hermes", + }), + { 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?.backendOverride ?? null, "hermes-runtime"); + + const state = await readState(); + const masterProject = state.projects.find((project) => project.id === "master-agent"); + const reply = masterProject?.messages.at(-1); + assert.ok(reply); + assert.match(reply?.body ?? "", /已把默认后端切到 hermes-runtime/i); + } finally { + process.env.BOSS_HERMES_ENABLED = previousEnv.BOSS_HERMES_ENABLED; + process.env.BOSS_HERMES_COMMAND = previousEnv.BOSS_HERMES_COMMAND; + } +}); + +test("master-agent 查询默认执行模式时直接返回 GUI CLI 状态", async () => { + await saveAiAccount({ + accountId: "master-codex-execution-mode", + label: "主 GPT", + role: "primary", + provider: "master_codex_node", + displayName: "Mac 上的 Master Codex Node", + nodeId: "mac-studio", + nodeLabel: "Mac Studio", + model: "gpt-5.4", + enabled: true, + setActive: true, + loginStatusNote: "用于执行模式查询测试。", + }); + await updateDevice("mac-studio", { + status: "online", + preferredExecutionMode: "gui", + capabilities: { + gui: { + connected: true, + }, + cli: { + connected: true, + }, + }, + }); + + const response = await POST( + await createAuthedRequest("master-agent", { + body: "现在默认走 GUI 还是 CLI", + }), + { 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); + assert.match(reply?.body ?? "", /默认执行模式:gui/i); + assert.match(reply?.body ?? "", /GUI:在线/); + assert.match(reply?.body ?? "", /CLI:在线/); +}); + +test("master-agent 查询当前主节点设备状态时直接返回绑定设备在线信息", async () => { + await saveAiAccount({ + accountId: "master-codex-device-status", + label: "主 GPT", + role: "primary", + provider: "master_codex_node", + displayName: "Mac 上的 Master Codex Node", + nodeId: "mac-studio", + nodeLabel: "Mac Studio", + model: "gpt-5.4", + enabled: true, + setActive: true, + loginStatusNote: "用于绑定设备状态查询测试。", + }); + await updateDevice("mac-studio", { + status: "online", + capabilities: { + gui: { + connected: true, + }, + cli: { + connected: false, + }, + }, + }); + + 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); + assert.match(reply?.body ?? "", /当前主节点设备:/); + assert.match(reply?.body ?? "", /设备状态:online/); + assert.match(reply?.body ?? "", /GUI:在线/); + assert.match(reply?.body ?? "", /CLI:离线/); +}); + test("POST /api/v1/projects/master-agent/messages 快速返回队列态并在异步实际回复时继承当前会话覆盖", async () => { await saveAiAccount({ accountId: "openai-master-agent-queue",