From de6257a819571f7a2038f5564b0391afe871945c Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 16 Apr 2026 05:21:50 +0800 Subject: [PATCH] feat: add master agent evolution engine core --- docs/architecture/ai_handoff_index_cn.md | 2 + .../current_runtime_and_deploy_status_cn.md | 1 + ...026-04-16-master-agent-evolution-engine.md | 287 ++++++++++++ ...16-master-agent-evolution-engine-design.md | 198 +++++++++ .../v1/master-agent/evolution/config/route.ts | 44 ++ .../proposals/[proposalId]/approve/route.ts | 29 ++ .../proposals/[proposalId]/reject/route.ts | 29 ++ .../api/v1/master-agent/evolution/route.ts | 16 + src/lib/boss-data.ts | 414 ++++++++++++++++++ src/lib/boss-master-agent.ts | 66 ++- src/lib/master-agent-evolution.ts | 226 ++++++++++ tests/master-agent-evolution-engine.test.ts | 73 +++ tests/master-agent-evolution-routes.test.ts | 125 ++++++ tests/master-agent-evolution-state.test.ts | 49 +++ 14 files changed, 1551 insertions(+), 8 deletions(-) create mode 100644 docs/superpowers/plans/2026-04-16-master-agent-evolution-engine.md create mode 100644 docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md create mode 100644 src/app/api/v1/master-agent/evolution/config/route.ts create mode 100644 src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route.ts create mode 100644 src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route.ts create mode 100644 src/app/api/v1/master-agent/evolution/route.ts create mode 100644 src/lib/master-agent-evolution.ts create mode 100644 tests/master-agent-evolution-engine.test.ts create mode 100644 tests/master-agent-evolution-routes.test.ts create mode 100644 tests/master-agent-evolution-state.test.ts diff --git a/docs/architecture/ai_handoff_index_cn.md b/docs/architecture/ai_handoff_index_cn.md index b65a026..1f23bb0 100644 --- a/docs/architecture/ai_handoff_index_cn.md +++ b/docs/architecture/ai_handoff_index_cn.md @@ -24,6 +24,7 @@ 7. `docs/architecture/thread_context_budget_and_handoff_protocol_cn.md` 8. `prompts/codex_fullstack_build_and_deploy_prompt_cn.md` 9. `docs/superpowers/specs/2026-04-16-master-agent-fast-path-design.md` +10. `docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md` ## 3. 当前有效实现边界 @@ -36,6 +37,7 @@ - `src/lib/boss-device-auth.ts`:设备 token / 登录会话混合鉴权辅助 - `src/lib/boss-events.ts`:SSE 事件总线 - `src/lib/boss-master-agent.ts`:主 Agent 真实回复链路、Master Codex Node relay 与 API 容灾逻辑 +- `src/lib/master-agent-evolution.ts`:主 Agent 自动进化引擎,负责 signal、proposal、rule 与 controlled / autonomous 模式 - `src/lib/boss-attachments.ts`:附件类型识别、分析状态决策和下载头 - `src/lib/boss-storage.ts`:附件存储抽象、配置校验和脱敏输出 - `src/lib/boss-storage-server-file.ts`:服务器文件存储上传 / 读取 diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index b90ad7c..0e70c19 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -163,6 +163,7 @@ cd /Users/kris/code/boss - 主 Agent 当前真实对话链路已验证通过:`Boss Web -> /api/v1/projects/master-agent/messages -> master-agent task queue -> local-agent -> codex exec -> /complete -> 项目消息账本` - 主 Agent 单聊当前已改成“快速入队 + 异步回流”:`POST /api/v1/projects/master-agent/messages` 会先返回 `masterReplyState + task`,真实回复随后再回写消息账本 - 当前对话级 `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` - 当前 `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 提示 diff --git a/docs/superpowers/plans/2026-04-16-master-agent-evolution-engine.md b/docs/superpowers/plans/2026-04-16-master-agent-evolution-engine.md new file mode 100644 index 0000000..cfe3761 --- /dev/null +++ b/docs/superpowers/plans/2026-04-16-master-agent-evolution-engine.md @@ -0,0 +1,287 @@ +# Master Agent Evolution Engine Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** 给 Boss 主Agent 增加一套统一的自动进化引擎,并支持 `controlled` 与 `autonomous` 两种模式以及双分支交付。 + +**Architecture:** 在现有 `boss-data` 与 `boss-master-agent` 上新增 evolution state、engine 和 API。共享内核一次实现,分支差异只保留在默认模式与自动采纳开关上,避免维护两份逻辑。 + +**Tech Stack:** Next.js 16、TypeScript、文件型状态存储、Node test runner + +--- + +### Task 1: 增加 Evolution 状态模型与持久化 + +**Files:** +- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/lib/boss-data.ts` +- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-state.test.ts` + +- [ ] **Step 1: 写失败测试,定义 evolution state 的默认结构与读写** + +```ts +test("boss state 初始化时包含 master agent evolution 默认配置与空集合", async () => { + const state = await readState(); + assert.deepEqual(state.masterAgentEvolutionConfig?.mode, "controlled"); + assert.deepEqual(state.masterAgentEvolutionSignals, []); + assert.deepEqual(state.masterAgentEvolutionProposals, []); + assert.deepEqual(state.masterAgentEvolutionRules, []); + assert.deepEqual(state.masterAgentEvolutionRunLogs, []); +}); +``` + +- [ ] **Step 2: 运行测试,确认当前失败** + +Run: `npx tsx --test tests/master-agent-evolution-state.test.ts` +Expected: FAIL,提示缺少 `masterAgentEvolution*` 字段或断言不匹配 + +- [ ] **Step 3: 在 `boss-data.ts` 中新增类型和默认状态** + +```ts +export type MasterAgentEvolutionMode = "controlled" | "autonomous"; + +export interface MasterAgentEvolutionConfig { + mode: MasterAgentEvolutionMode; + autoApplyLowRiskRules: boolean; + updatedAt: string; +} +``` + +- [ ] **Step 4: 扩展 `BossState`、normalize 逻辑和默认初始值** + +Run: `npx tsx --test tests/master-agent-evolution-state.test.ts` +Expected: PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/lib/boss-data.ts tests/master-agent-evolution-state.test.ts +git commit -m "feat: add master agent evolution state" +``` + +### Task 2: 实现 Evolution Engine 核心 + +**Files:** +- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/lib/master-agent-evolution.ts` +- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-engine.test.ts` + +- [ ] **Step 1: 写失败测试,覆盖 signal -> proposal 的最小闭环** + +```ts +test("recording repeated deterministic questions creates a fast_path_rule proposal", async () => { + await recordMasterAgentEvolutionSignal({ + kind: "repeated_question", + projectId: "master-agent", + account: "17600003315", + content: "当前主节点在线吗", + }); + const proposals = await listMasterAgentEvolutionProposals(); + assert.equal(proposals[0]?.proposalType, "fast_path_rule"); +}); +``` + +- [ ] **Step 2: 运行测试,确认失败** + +Run: `npx tsx --test tests/master-agent-evolution-engine.test.ts` +Expected: FAIL,提示函数或 proposal 类型不存在 + +- [ ] **Step 3: 实现最小 engine API** + +```ts +export async function recordMasterAgentEvolutionSignal(...) {} +export async function listMasterAgentEvolutionProposals(...) {} +export async function applyMasterAgentEvolutionProposal(...) {} +``` + +- [ ] **Step 4: 实现第一批 proposal 生成规则** + +规则最小集: +- repeated deterministic questions -> `fast_path_rule` +- repeated backend/model corrections -> `routing_preference_patch` +- repeated user preference statements -> `memory_patch` + +- [ ] **Step 5: 运行测试确认通过** + +Run: `npx tsx --test tests/master-agent-evolution-engine.test.ts` +Expected: PASS + +- [ ] **Step 6: 提交** + +```bash +git add src/lib/master-agent-evolution.ts tests/master-agent-evolution-engine.test.ts +git commit -m "feat: add master agent evolution engine" +``` + +### Task 3: 把 Evolution Engine 挂到主Agent主链 + +**Files:** +- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/lib/boss-master-agent.ts` +- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-integration.test.ts` + +- [ ] **Step 1: 写失败测试,验证快路径未命中后会记录 signal** + +```ts +test("master agent slow deterministic query records evolution signal when fast path misses", async () => { + await replyToMasterAgentUserMessage({ + requestText: "当前线程是不是正在运行", + requestedBy: "Boss 超级管理员", + requestedByAccount: "17600003315", + mode: "enqueue", + }); + const signals = await listMasterAgentEvolutionSignals(); + assert.ok(signals.some((item) => item.kind === "fast_path_candidate")); +}); +``` + +- [ ] **Step 2: 运行测试,确认失败** + +Run: `npx tsx --test tests/master-agent-evolution-integration.test.ts` +Expected: FAIL + +- [ ] **Step 3: 在 `replyToMasterAgentUserMessage()` 和相关成功/失败路径中记录 signal** + +最小挂载点: +- Fast Path 命中 +- Fast Path 未命中但命中确定性查询模式 +- 后端 fallback +- 用户手动切模型/切后端/切接管 + +- [ ] **Step 4: 运行测试确认通过** + +Run: `npx tsx --test tests/master-agent-evolution-integration.test.ts` +Expected: PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/lib/boss-master-agent.ts tests/master-agent-evolution-integration.test.ts +git commit -m "feat: integrate evolution engine into master agent" +``` + +### Task 4: 实现 Evolution API + +**Files:** +- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/app/api/v1/master-agent/evolution/route.ts` +- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/app/api/v1/master-agent/evolution/config/route.ts` +- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route.ts` +- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route.ts` +- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-routes.test.ts` + +- [ ] **Step 1: 写失败测试,定义 GET/POST contract** + +```ts +test("GET /api/v1/master-agent/evolution returns config proposals and rules", async () => { + const response = await GET(request); + const payload = await response.json(); + assert.equal(payload.ok, true); + assert.ok(Array.isArray(payload.proposals)); +}); +``` + +- [ ] **Step 2: 运行测试,确认失败** + +Run: `npx tsx --test tests/master-agent-evolution-routes.test.ts` +Expected: FAIL + +- [ ] **Step 3: 实现读接口和 config 切换接口** + +- [ ] **Step 4: 实现 approve / reject 接口** + +- [ ] **Step 5: 运行测试确认通过** + +Run: `npx tsx --test tests/master-agent-evolution-routes.test.ts` +Expected: PASS + +- [ ] **Step 6: 提交** + +```bash +git add src/app/api/v1/master-agent/evolution src/app/api/v1/master-agent/evolution/config src/app/api/v1/master-agent/evolution/proposals tests/master-agent-evolution-routes.test.ts +git commit -m "feat: add master agent evolution routes" +``` + +### Task 5: 加入 Controlled / Autonomous 自动采纳分界 + +**Files:** +- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/lib/master-agent-evolution.ts` +- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-autonomous.test.ts` + +- [ ] **Step 1: 写失败测试,验证 controlled 不自动采纳、autonomous 可自动采纳低风险 proposal** + +```ts +test("controlled mode keeps proposal pending_review", async () => { + await setMasterAgentEvolutionMode("controlled"); + // record signal... + assert.equal(proposals[0]?.status, "pending_review"); +}); + +test("autonomous mode auto applies low risk routing preference patch", async () => { + await setMasterAgentEvolutionMode("autonomous"); + // record signal... + assert.equal(proposals[0]?.status, "auto_applied"); +}); +``` + +- [ ] **Step 2: 运行测试,确认失败** + +Run: `npx tsx --test tests/master-agent-evolution-autonomous.test.ts` +Expected: FAIL + +- [ ] **Step 3: 实现 mode gating 与低风险自动采纳白名单** + +白名单: +- `memory_patch` +- `routing_preference_patch` +- `fast_path_rule` + +黑名单: +- 管理员全局主提示词修改 +- 高风险提示词覆盖 + +- [ ] **Step 4: 运行测试确认通过** + +Run: `npx tsx --test tests/master-agent-evolution-autonomous.test.ts` +Expected: PASS + +- [ ] **Step 5: 提交** + +```bash +git add src/lib/master-agent-evolution.ts tests/master-agent-evolution-autonomous.test.ts +git commit -m "feat: add controlled and autonomous evolution modes" +``` + +### Task 6: 更新交接文档并切双分支 + +**Files:** +- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/docs/architecture/ai_handoff_index_cn.md` +- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/docs/architecture/current_runtime_and_deploy_status_cn.md` +- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md` + +- [ ] **Step 1: 文档中加入 evolution engine 与两种模式说明** + +- [ ] **Step 2: 运行组合验证** + +Run: `npx tsx --test tests/master-agent-evolution*.test.ts tests/master-agent-message-queue.test.ts tests/master-agent-chat-controls.test.ts` +Expected: PASS + +Run: `npm run build` +Expected: PASS + +- [ ] **Step 3: 提交共享内核** + +```bash +git add docs/architecture/ai_handoff_index_cn.md docs/architecture/current_runtime_and_deploy_status_cn.md docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md +git commit -m "docs: document master agent evolution engine" +``` + +- [ ] **Step 4: 创建 controlled 分支并推送** + +```bash +git branch codex/master-agent-controlled-evolution +git push gitea codex/master-agent-controlled-evolution +``` + +- [ ] **Step 5: 创建 autonomous 分支,在其上把默认 mode 切到 autonomous 并推送** + +```bash +git checkout -b codex/master-agent-autonomous-evolution +git push gitea codex/master-agent-autonomous-evolution +``` diff --git a/docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md b/docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md new file mode 100644 index 0000000..47a0f37 --- /dev/null +++ b/docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md @@ -0,0 +1,198 @@ +# 主Agent 自动进化引擎设计 + +更新时间:`2026-04-16` + +## 1. 背景 + +当前 Boss 主Agent 已经具备: + +- 长期记忆:`masterAgentMemories` +- 提示词策略:`masterAgentPromptPolicy`、`userMasterPrompts` +- 对话控制:`userProjectAgentControls` +- 运行时自适应:Master Node / Hermes / Claw / API fallback + +但它还不具备真正的自动进化闭环。现状只能“记住”与“回退”,不能持续发现问题、形成提案、沉淀规则并在后续回复中生效。 + +## 2. 目标 + +- 给主Agent增加统一的 `Evolution Engine` +- 支持两种能力范围: + - `controlled`:受控自动进化,只生成提案,等待用户批准后落地 + - `autonomous`:完全自我进化,对高置信度提案自动落地 +- 进化逻辑对现有主链最小侵入,不重写现有回复链、任务链、记忆链 +- 所有进化行为可追踪、可审计、可回滚 + +## 3. 核心概念 + +### 3.1 Evolution Signal + +进化信号,记录“为什么应该考虑进化”。 + +第一批信号来源: + +- 高频重复问句 +- 慢回复问题 +- 用户纠正主Agent +- 模型/后端频繁手动切换 +- 后端回退或执行失败 +- Fast Path 未命中但属于确定性状态问题 + +### 3.2 Evolution Proposal + +进化提案,记录“建议怎么改”。 + +第一批提案类型: + +- `fast_path_rule` +- `prompt_policy_patch` +- `memory_patch` +- `agent_control_patch` +- `routing_preference_patch` + +### 3.3 Evolution Rule + +已生效规则,表示被批准或自动采纳后真正进入系统长期状态的变更。 + +### 3.4 Evolution Run Log + +进化执行日志,记录某次信号分析、提案生成、采纳、拒绝、自动落地的全过程。 + +## 4. 运行模式 + +### 4.1 Controlled + +受控自动进化: + +- 主Agent可以自动记录 signal +- 可以自动生成 proposal +- 不允许自动改长期状态 +- 只能在用户批准后写入 memory / prompt / controls / fast-path rules + +适合生产稳态与审计要求更高的环境。 + +### 4.2 Autonomous + +完全自我进化: + +- 主Agent可以自动记录 signal +- 自动生成 proposal +- 对满足阈值的 proposal 直接自动采纳 +- 自动把结果写入长期状态 +- 仍保留完整日志,允许后续回滚 + +适合开发期快速迭代,用更高风险换更快自优化速度。 + +## 5. 架构 + +### 5.1 新增状态模型 + +在 `BossState` 中新增: + +- `masterAgentEvolutionConfig` +- `masterAgentEvolutionSignals` +- `masterAgentEvolutionProposals` +- `masterAgentEvolutionRules` +- `masterAgentEvolutionRunLogs` + +### 5.2 新增引擎文件 + +- `src/lib/master-agent-evolution.ts` + - 统一入口 + - signal 归一化 + - proposal 生成 + - 自动采纳判断 + - 规则落地 +- `src/lib/master-agent-evolution-rules.ts` + - Fast Path 规则、路由偏好规则解析 +- `src/lib/master-agent-evolution-projections.ts` + - 给 Web/API 返回 evolution 状态摘要 + +### 5.3 挂载点 + +第一批最小侵入挂载点: + +- `replyToMasterAgentUserMessage()` + - 记录用户问题、Fast Path 命中/未命中、慢路径耗时、模型/后端实际选择 +- `appendMasterAgentSystemReply()` + - 记录主Agent输出结果 +- `completeMasterAgentTask()` + - 记录异步任务成功/失败、fallback、超时 +- `updateProjectAgentControls()` / `updateMasterAgentPromptPolicy()` / `createUserMasterMemory()` + - 记录用户主动纠偏行为,作为 evolution signal 输入 + +## 6. 第一批自动进化能力 + +### Controlled 与 Autonomous 共用 + +- 记录重复问句 +- 记录低价值慢路径 +- 记录用户纠正模型/后端/接管设置 +- 自动生成三类提案: + - 新增 Fast Path 规则 + - 调整默认快模型/强模型/后端偏好 + - 追加 workflow_rule / user_preference 记忆 + +### Controlled 专属行为 + +- 所有提案状态默认为 `pending_review` +- 需要用户批准后才能落地 + +### Autonomous 专属行为 + +- 当 proposal 满足“高置信度 + 低破坏性”时自动落地 +- 第一批只允许自动落地: + - `memory_patch` + - `routing_preference_patch` + - `fast_path_rule` +- 不允许自动改管理员全局主提示词 + +## 7. 前台入口 + +第一批只做最小 Web 能力: + +- `GET /api/v1/master-agent/evolution` + - 返回 config / pending proposals / recent run logs / effective rules +- `POST /api/v1/master-agent/evolution/config` + - 切换 `controlled` / `autonomous` +- `POST /api/v1/master-agent/evolution/proposals/[proposalId]/approve` +- `POST /api/v1/master-agent/evolution/proposals/[proposalId]/reject` + +先不做复杂 UI,先把数据和接口打通,必要时从 `我的 > 主Agent` 进入。 + +## 8. 双分支策略 + +统一内核先落在共享提交里: + +- evolution state +- evolution engine +- proposal / rule / run-log 结构 +- API 与测试 + +然后从共享基线分两条分支: + +- `codex/master-agent-controlled-evolution` + - 默认 `mode=controlled` + - 关闭自动采纳 +- `codex/master-agent-autonomous-evolution` + - 默认 `mode=autonomous` + - 开启自动采纳阈值逻辑 + +用户先使用 autonomous 分支验证真实开发效率;一旦发现策略漂移或错误写入,可直接回退 controlled 分支。 + +## 9. 风险控制 + +- 管理员全局主提示词永远不允许 autonomous 自动改写 +- 自动落地范围先限制在低风险对象 +- 每次自动采纳都必须写 run log +- 所有 rule 都要带 `sourceProposalId` +- 所有 proposal 都要可拒绝、可归档、可回滚 + +## 10. 验证基线 + +- `npx tsx --test tests/master-agent-evolution*.test.ts` +- `npx tsx --test tests/master-agent-message-queue.test.ts tests/master-agent-chat-controls.test.ts` +- `npm run build` +- 线上切到 `autonomous` 后: + - 主Agent可记录进化信号 + - 可自动生成并落地低风险规则 + - `controlled` 分支同一套信号与提案只记录、不自动采纳 diff --git a/src/app/api/v1/master-agent/evolution/config/route.ts b/src/app/api/v1/master-agent/evolution/config/route.ts new file mode 100644 index 0000000..7200ff7 --- /dev/null +++ b/src/app/api/v1/master-agent/evolution/config/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireRequestSession } from "@/lib/boss-auth"; +import { jsonNoStore } from "@/lib/api-response"; +import { setMasterAgentEvolutionMode } from "@/lib/master-agent-evolution"; + +export const runtime = "nodejs"; + +export async function GET(request: NextRequest) { + const session = await requireRequestSession(request); + if (!session) { + return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }); + } + return jsonNoStore({ ok: false, message: "METHOD_NOT_ALLOWED" }, { status: 405 }); +} + +export async function POST(request: NextRequest) { + const session = await requireRequestSession(request); + if (!session) { + return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }); + } + if (session.role !== "highest_admin") { + return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 }); + } + + const rawBody = await request.text().catch(() => ""); + let body: unknown; + try { + body = JSON.parse(rawBody); + } catch { + return NextResponse.json({ ok: false, message: "INVALID_JSON_PAYLOAD" }, { status: 400 }); + } + + if (!body || typeof body !== "object" || Array.isArray(body)) { + return NextResponse.json({ ok: false, message: "INVALID_EVOLUTION_CONFIG_PAYLOAD" }, { status: 400 }); + } + + const payload = body as { mode?: unknown }; + if (payload.mode !== "controlled" && payload.mode !== "autonomous") { + return NextResponse.json({ ok: false, message: "INVALID_EVOLUTION_MODE" }, { status: 400 }); + } + + const config = await setMasterAgentEvolutionMode(payload.mode); + return NextResponse.json({ ok: true, config }); +} diff --git a/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route.ts b/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route.ts new file mode 100644 index 0000000..ec8dbcd --- /dev/null +++ b/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireRequestSession } from "@/lib/boss-auth"; +import { approveMasterAgentEvolutionProposal } from "@/lib/master-agent-evolution"; + +export const runtime = "nodejs"; + +export async function POST( + request: NextRequest, + context: { params: Promise<{ proposalId: string }> }, +) { + const session = await requireRequestSession(request); + if (!session) { + return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }); + } + if (session.role !== "highest_admin") { + return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 }); + } + + const { proposalId } = await context.params; + try { + const proposal = await approveMasterAgentEvolutionProposal(proposalId, session.account); + return NextResponse.json({ ok: true, proposal }); + } catch (error) { + return NextResponse.json( + { ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" }, + { status: 400 }, + ); + } +} diff --git a/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route.ts b/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route.ts new file mode 100644 index 0000000..c76609b --- /dev/null +++ b/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route.ts @@ -0,0 +1,29 @@ +import { NextRequest, NextResponse } from "next/server"; +import { requireRequestSession } from "@/lib/boss-auth"; +import { rejectMasterAgentEvolutionProposal } from "@/lib/master-agent-evolution"; + +export const runtime = "nodejs"; + +export async function POST( + request: NextRequest, + context: { params: Promise<{ proposalId: string }> }, +) { + const session = await requireRequestSession(request); + if (!session) { + return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }); + } + if (session.role !== "highest_admin") { + return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 }); + } + + const { proposalId } = await context.params; + try { + const proposal = await rejectMasterAgentEvolutionProposal(proposalId, session.account); + return NextResponse.json({ ok: true, proposal }); + } catch (error) { + return NextResponse.json( + { ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" }, + { status: 400 }, + ); + } +} diff --git a/src/app/api/v1/master-agent/evolution/route.ts b/src/app/api/v1/master-agent/evolution/route.ts new file mode 100644 index 0000000..47156f6 --- /dev/null +++ b/src/app/api/v1/master-agent/evolution/route.ts @@ -0,0 +1,16 @@ +import { NextRequest } from "next/server"; +import { requireRequestSession } from "@/lib/boss-auth"; +import { jsonNoStore } from "@/lib/api-response"; +import { getMasterAgentEvolutionDashboard } from "@/lib/master-agent-evolution"; + +export const runtime = "nodejs"; + +export async function GET(request: NextRequest) { + const session = await requireRequestSession(request); + if (!session) { + return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }); + } + + const dashboard = await getMasterAgentEvolutionDashboard(); + return jsonNoStore({ ok: true, ...dashboard }); +} diff --git a/src/lib/boss-data.ts b/src/lib/boss-data.ts index 89bd560..49f2d23 100644 --- a/src/lib/boss-data.ts +++ b/src/lib/boss-data.ts @@ -270,6 +270,86 @@ export interface MasterAgentMemory { archived: boolean; } +export type MasterAgentEvolutionMode = "controlled" | "autonomous"; +export type MasterAgentEvolutionSignalKind = + | "repeated_question" + | "fast_path_candidate" + | "slow_path" + | "user_correction" + | "backend_fallback"; +export type MasterAgentEvolutionProposalType = + | "fast_path_rule" + | "prompt_policy_patch" + | "memory_patch" + | "agent_control_patch" + | "routing_preference_patch"; +export type MasterAgentEvolutionProposalStatus = + | "pending_review" + | "approved" + | "rejected" + | "auto_applied"; +export type MasterAgentEvolutionRunAction = + | "signal_recorded" + | "proposal_created" + | "proposal_approved" + | "proposal_rejected" + | "proposal_auto_applied"; + +export interface MasterAgentEvolutionConfig { + mode: MasterAgentEvolutionMode; + autoApplyLowRiskRules: boolean; + updatedAt: string; +} + +export interface MasterAgentEvolutionSignal { + signalId: string; + kind: MasterAgentEvolutionSignalKind; + account: string; + projectId: string; + requestText: string; + replyText?: string; + metadataJson?: Record; + createdAt: string; +} + +export interface MasterAgentEvolutionProposal { + proposalId: string; + proposalType: MasterAgentEvolutionProposalType; + status: MasterAgentEvolutionProposalStatus; + account: string; + projectId: string; + title: string; + summary: string; + patchJson: Record; + sourceSignalIds: string[]; + confidence: number; + riskLevel: "low" | "medium" | "high"; + createdAt: string; + updatedAt: string; +} + +export interface MasterAgentEvolutionRule { + ruleId: string; + ruleType: MasterAgentEvolutionProposalType; + account: string; + projectId?: string; + sourceProposalId: string; + patchJson: Record; + createdAt: string; + updatedAt: string; +} + +export interface MasterAgentEvolutionRunLog { + runId: string; + action: MasterAgentEvolutionRunAction; + account: string; + projectId: string; + signalId?: string; + proposalId?: string; + note: string; + createdAt: string; +} + export interface GoalItem { id: string; text: string; @@ -1078,6 +1158,11 @@ export interface BossState { masterAgentPromptPolicy: MasterAgentPromptPolicy | null; userMasterPrompts: UserMasterPrompt[]; masterAgentMemories: MasterAgentMemory[]; + masterAgentEvolutionConfig: MasterAgentEvolutionConfig; + masterAgentEvolutionSignals: MasterAgentEvolutionSignal[]; + masterAgentEvolutionProposals: MasterAgentEvolutionProposal[]; + masterAgentEvolutionRules: MasterAgentEvolutionRule[]; + masterAgentEvolutionRunLogs: MasterAgentEvolutionRunLog[]; userProjectAgentControls: UserProjectAgentControls[]; threadContextSnapshots: ThreadContextSnapshot[]; threadHandoffPackages: ThreadHandoffPackage[]; @@ -1451,6 +1536,15 @@ const initialState: BossState = { masterAgentPromptPolicy: null, userMasterPrompts: [], masterAgentMemories: [], + masterAgentEvolutionConfig: { + mode: "controlled", + autoApplyLowRiskRules: false, + updatedAt: nowIso(), + }, + masterAgentEvolutionSignals: [], + masterAgentEvolutionProposals: [], + masterAgentEvolutionRules: [], + masterAgentEvolutionRunLogs: [], userProjectAgentControls: [], masterAgentTasks: [], dispatchPlans: [], @@ -2967,6 +3061,101 @@ function normalizeUserMasterMemory( }; } +function normalizeMasterAgentEvolutionConfig( + raw: Partial | null | undefined, + fallback?: MasterAgentEvolutionConfig, +): MasterAgentEvolutionConfig { + return { + mode: raw?.mode === "autonomous" ? "autonomous" : fallback?.mode ?? "controlled", + autoApplyLowRiskRules: + typeof raw?.autoApplyLowRiskRules === "boolean" + ? raw.autoApplyLowRiskRules + : fallback?.autoApplyLowRiskRules ?? false, + updatedAt: raw?.updatedAt ?? fallback?.updatedAt ?? nowIso(), + }; +} + +function normalizeMasterAgentEvolutionSignal( + raw: Partial, + fallback?: MasterAgentEvolutionSignal, +): MasterAgentEvolutionSignal { + return { + signalId: raw.signalId ?? fallback?.signalId ?? randomToken("evo-signal"), + kind: raw.kind ?? fallback?.kind ?? "slow_path", + account: trimToDefined(raw.account ?? fallback?.account) ?? "", + projectId: trimToDefined(raw.projectId ?? fallback?.projectId) ?? "master-agent", + requestText: trimToDefined(raw.requestText ?? fallback?.requestText) ?? "", + replyText: trimToDefined(raw.replyText ?? fallback?.replyText), + metadataJson: + raw.metadataJson && typeof raw.metadataJson === "object" + ? raw.metadataJson + : fallback?.metadataJson ?? {}, + createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(), + }; +} + +function normalizeMasterAgentEvolutionProposal( + raw: Partial, + fallback?: MasterAgentEvolutionProposal, +): MasterAgentEvolutionProposal { + return { + proposalId: raw.proposalId ?? fallback?.proposalId ?? randomToken("evo-proposal"), + proposalType: raw.proposalType ?? fallback?.proposalType ?? "memory_patch", + status: raw.status ?? fallback?.status ?? "pending_review", + account: trimToDefined(raw.account ?? fallback?.account) ?? "", + projectId: trimToDefined(raw.projectId ?? fallback?.projectId) ?? "master-agent", + title: trimToDefined(raw.title ?? fallback?.title) ?? "进化提案", + summary: trimToDefined(raw.summary ?? fallback?.summary) ?? "", + patchJson: + raw.patchJson && typeof raw.patchJson === "object" + ? raw.patchJson + : fallback?.patchJson ?? {}, + sourceSignalIds: dedupeStrings( + ensureArray(raw.sourceSignalIds, fallback?.sourceSignalIds ?? []).map((value) => value.trim()), + ), + confidence: + typeof raw.confidence === "number" ? raw.confidence : fallback?.confidence ?? 0.5, + riskLevel: raw.riskLevel ?? fallback?.riskLevel ?? "low", + createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(), + updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(), + }; +} + +function normalizeMasterAgentEvolutionRule( + raw: Partial, + fallback?: MasterAgentEvolutionRule, +): MasterAgentEvolutionRule { + return { + ruleId: raw.ruleId ?? fallback?.ruleId ?? randomToken("evo-rule"), + ruleType: raw.ruleType ?? fallback?.ruleType ?? "memory_patch", + account: trimToDefined(raw.account ?? fallback?.account) ?? "", + projectId: trimToDefined(raw.projectId ?? fallback?.projectId), + sourceProposalId: trimToDefined(raw.sourceProposalId ?? fallback?.sourceProposalId) ?? "", + patchJson: + raw.patchJson && typeof raw.patchJson === "object" + ? raw.patchJson + : fallback?.patchJson ?? {}, + createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(), + updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(), + }; +} + +function normalizeMasterAgentEvolutionRunLog( + raw: Partial, + fallback?: MasterAgentEvolutionRunLog, +): MasterAgentEvolutionRunLog { + return { + runId: raw.runId ?? fallback?.runId ?? randomToken("evo-run"), + action: raw.action ?? fallback?.action ?? "signal_recorded", + account: trimToDefined(raw.account ?? fallback?.account) ?? "", + projectId: trimToDefined(raw.projectId ?? fallback?.projectId) ?? "master-agent", + signalId: trimToDefined(raw.signalId ?? fallback?.signalId), + proposalId: trimToDefined(raw.proposalId ?? fallback?.proposalId), + note: trimToDefined(raw.note ?? fallback?.note) ?? "", + createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(), + }; +} + function normalizeProject(raw: Partial, fallback?: Project): Project { const base = fallback ?? cloneInitialState().projects[0]; const projectId = raw.id ?? base.id; @@ -3495,6 +3684,46 @@ function normalizeState(raw: Partial | undefined): BossState { base.masterAgentMemories[index % Math.max(1, base.masterAgentMemories.length)], ), ), + masterAgentEvolutionConfig: normalizeMasterAgentEvolutionConfig( + raw.masterAgentEvolutionConfig, + base.masterAgentEvolutionConfig, + ), + masterAgentEvolutionSignals: ensureArray( + raw.masterAgentEvolutionSignals, + base.masterAgentEvolutionSignals, + ).map((signal, index) => + normalizeMasterAgentEvolutionSignal( + signal, + base.masterAgentEvolutionSignals[index % Math.max(1, base.masterAgentEvolutionSignals.length)], + ), + ), + masterAgentEvolutionProposals: ensureArray( + raw.masterAgentEvolutionProposals, + base.masterAgentEvolutionProposals, + ).map((proposal, index) => + normalizeMasterAgentEvolutionProposal( + proposal, + base.masterAgentEvolutionProposals[index % Math.max(1, base.masterAgentEvolutionProposals.length)], + ), + ), + masterAgentEvolutionRules: ensureArray( + raw.masterAgentEvolutionRules, + base.masterAgentEvolutionRules, + ).map((rule, index) => + normalizeMasterAgentEvolutionRule( + rule, + base.masterAgentEvolutionRules[index % Math.max(1, base.masterAgentEvolutionRules.length)], + ), + ), + masterAgentEvolutionRunLogs: ensureArray( + raw.masterAgentEvolutionRunLogs, + base.masterAgentEvolutionRunLogs, + ).map((log, index) => + normalizeMasterAgentEvolutionRunLog( + log, + base.masterAgentEvolutionRunLogs[index % Math.max(1, base.masterAgentEvolutionRunLogs.length)], + ), + ), userProjectAgentControls: ensureArray( raw.userProjectAgentControls, base.userProjectAgentControls, @@ -4991,6 +5220,191 @@ export async function touchUserMasterMemories(memoryIds: string[], account: stri }); } +export async function getMasterAgentEvolutionState() { + const state = await readState(); + return { + config: state.masterAgentEvolutionConfig, + signals: [...state.masterAgentEvolutionSignals], + proposals: [...state.masterAgentEvolutionProposals], + rules: [...state.masterAgentEvolutionRules], + runLogs: [...state.masterAgentEvolutionRunLogs], + }; +} + +export async function updateMasterAgentEvolutionConfig(input: { + mode?: MasterAgentEvolutionMode; + autoApplyLowRiskRules?: boolean; +}) { + const result = await mutateState((state) => { + const current = state.masterAgentEvolutionConfig; + const next = normalizeMasterAgentEvolutionConfig({ + mode: input.mode ?? current.mode, + autoApplyLowRiskRules: + typeof input.autoApplyLowRiskRules === "boolean" + ? input.autoApplyLowRiskRules + : current.autoApplyLowRiskRules, + updatedAt: nowIso(), + }, current); + if (next.mode === "controlled") { + next.autoApplyLowRiskRules = false; + } + if (next.mode === "autonomous" && input.autoApplyLowRiskRules === undefined) { + next.autoApplyLowRiskRules = true; + } + state.masterAgentEvolutionConfig = next; + return next; + }); + publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" }); + return result; +} + +export async function recordMasterAgentEvolutionSignalInState(input: { + kind: MasterAgentEvolutionSignalKind; + account: string; + projectId?: string; + requestText: string; + replyText?: string; + metadataJson?: Record; +}) { + const result = await mutateState((state) => { + const signal = normalizeMasterAgentEvolutionSignal({ + signalId: randomToken("evo-signal"), + kind: input.kind, + account: input.account, + projectId: input.projectId ?? "master-agent", + requestText: input.requestText, + replyText: input.replyText, + metadataJson: input.metadataJson ?? {}, + createdAt: nowIso(), + }); + state.masterAgentEvolutionSignals.unshift(signal); + state.masterAgentEvolutionRunLogs.unshift( + normalizeMasterAgentEvolutionRunLog({ + runId: randomToken("evo-run"), + action: "signal_recorded", + account: signal.account, + projectId: signal.projectId, + signalId: signal.signalId, + note: `记录进化信号:${signal.kind}`, + createdAt: nowIso(), + }), + ); + return signal; + }); + publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" }); + return result; +} + +export async function createMasterAgentEvolutionProposalInState(input: { + proposalType: MasterAgentEvolutionProposalType; + account: string; + projectId?: string; + title: string; + summary: string; + patchJson: Record; + sourceSignalIds: string[]; + confidence: number; + riskLevel?: "low" | "medium" | "high"; + status?: MasterAgentEvolutionProposalStatus; +}) { + const result = await mutateState((state) => { + const now = nowIso(); + const proposal = normalizeMasterAgentEvolutionProposal({ + proposalId: randomToken("evo-proposal"), + proposalType: input.proposalType, + status: input.status ?? "pending_review", + account: input.account, + projectId: input.projectId ?? "master-agent", + title: input.title, + summary: input.summary, + patchJson: input.patchJson, + sourceSignalIds: input.sourceSignalIds, + confidence: input.confidence, + riskLevel: input.riskLevel ?? "low", + createdAt: now, + updatedAt: now, + }); + state.masterAgentEvolutionProposals.unshift(proposal); + state.masterAgentEvolutionRunLogs.unshift( + normalizeMasterAgentEvolutionRunLog({ + runId: randomToken("evo-run"), + action: proposal.status === "auto_applied" ? "proposal_auto_applied" : "proposal_created", + account: proposal.account, + projectId: proposal.projectId, + proposalId: proposal.proposalId, + note: proposal.title, + createdAt: now, + }), + ); + return proposal; + }); + publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" }); + return result; +} + +export async function updateMasterAgentEvolutionProposalStatus(input: { + proposalId: string; + status: MasterAgentEvolutionProposalStatus; + account: string; + note?: string; +}) { + const result = await mutateState((state) => { + const proposal = state.masterAgentEvolutionProposals.find((item) => item.proposalId === input.proposalId); + if (!proposal) { + throw new Error("MASTER_AGENT_EVOLUTION_PROPOSAL_NOT_FOUND"); + } + proposal.status = input.status; + proposal.updatedAt = nowIso(); + state.masterAgentEvolutionRunLogs.unshift( + normalizeMasterAgentEvolutionRunLog({ + runId: randomToken("evo-run"), + action: + input.status === "approved" + ? "proposal_approved" + : input.status === "rejected" + ? "proposal_rejected" + : input.status === "auto_applied" + ? "proposal_auto_applied" + : "proposal_created", + account: input.account, + projectId: proposal.projectId, + proposalId: proposal.proposalId, + note: input.note ?? proposal.title, + createdAt: nowIso(), + }), + ); + return proposal; + }); + publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" }); + return result; +} + +export async function createMasterAgentEvolutionRuleInState(input: { + ruleType: MasterAgentEvolutionProposalType; + account: string; + projectId?: string; + sourceProposalId: string; + patchJson: Record; +}) { + const result = await mutateState((state) => { + const now = nowIso(); + const rule = normalizeMasterAgentEvolutionRule({ + ruleId: randomToken("evo-rule"), + ruleType: input.ruleType, + account: input.account, + projectId: input.projectId, + sourceProposalId: input.sourceProposalId, + patchJson: input.patchJson, + createdAt: now, + updatedAt: now, + }); + state.masterAgentEvolutionRules.unshift(rule); + return rule; + }); + publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" }); + return result; +} + function normalizeAutoMemoryText(value: string | undefined) { return (value ?? "") .replace(/\s+/g, " ") diff --git a/src/lib/boss-master-agent.ts b/src/lib/boss-master-agent.ts index a954b51..0b4d126 100644 --- a/src/lib/boss-master-agent.ts +++ b/src/lib/boss-master-agent.ts @@ -60,6 +60,7 @@ import { listUserMasterMemoriesView, } from "@/lib/boss-projections"; import { normalizeRemoteExecutionResult } from "@/lib/execution/remote-runtime-adapter"; +import { recordMasterAgentEvolutionSignal } from "@/lib/master-agent-evolution"; type MasterAgentReplyState = "queued" | "running" | "completed"; type MasterAgentExecutionIntent = "chat" | "deep_task"; @@ -1950,6 +1951,27 @@ async function appendFastPathError( }; } +async function tryRecordMasterAgentEvolutionSignal(input: { + kind: "fast_path_candidate" | "user_correction" | "backend_fallback"; + account: string; + requestText: string; + replyText?: string; + metadataJson?: Record; +}) { + try { + await recordMasterAgentEvolutionSignal({ + kind: input.kind, + account: input.account, + projectId: "master-agent", + requestText: input.requestText, + replyText: input.replyText, + metadataJson: input.metadataJson, + }); + } catch { + // Evolution capture is best-effort and must not break replies. + } +} + function buildModelSummaryReply(context: MasterAgentFastIntentContext, requestText: string) { const normalized = normalizeLexicalText(requestText); const manualModel = context.agentControls?.modelOverride?.trim() || ""; @@ -2060,10 +2082,15 @@ async function tryHandleMasterAgentBackendSwitchCommand(params: { { backendOverride: requestedBackend }, params.requestedByAccount, ); - return appendFastPathReply( - `已把默认后端切到 ${requestedBackend}。`, - buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), - ); + const reply = `已把默认后端切到 ${requestedBackend}。`; + await tryRecordMasterAgentEvolutionSignal({ + kind: "user_correction", + account: params.requestedByAccount, + requestText: params.requestText, + replyText: reply, + metadataJson: { requestedBackend }, + }); + return appendFastPathReply(reply, buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model)); } async function tryHandleMasterAgentTakeoverCommand(params: { @@ -2099,10 +2126,15 @@ async function tryHandleMasterAgentTakeoverCommand(params: { { globalTakeoverEnabled: nextEnabled }, params.requestedByAccount, ); - return appendFastPathReply( - nextEnabled ? "已开启全局接管。" : "已关闭全局接管。", - buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model), - ); + const reply = nextEnabled ? "已开启全局接管。" : "已关闭全局接管。"; + await tryRecordMasterAgentEvolutionSignal({ + kind: "user_correction", + account: params.requestedByAccount, + requestText: params.requestText, + replyText: reply, + metadataJson: { globalTakeoverEnabled: nextEnabled }, + }); + return appendFastPathReply(reply, buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model)); } async function tryHandleMasterAgentExecutionModeStatusQuery(params: { @@ -2228,6 +2260,13 @@ async function tryHandleMasterAgentModelCommand(params: { await updateProjectAgentControls("master-agent", patch, params.requestedByAccount); const reply = `已把主 Agent 的${scopeLabel}切到 ${requestedModel}。${availableModelsSuffix}`; + await tryRecordMasterAgentEvolutionSignal({ + kind: "user_correction", + account: params.requestedByAccount, + requestText: params.requestText, + replyText: reply, + metadataJson: { scope, requestedModel }, + }); return appendFastPathReply(reply, buildMasterAgentModelSenderLabel(requestedModel)); } @@ -3219,6 +3258,17 @@ 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) { diff --git a/src/lib/master-agent-evolution.ts b/src/lib/master-agent-evolution.ts new file mode 100644 index 0000000..a7def83 --- /dev/null +++ b/src/lib/master-agent-evolution.ts @@ -0,0 +1,226 @@ +import { + createMasterAgentEvolutionProposalInState, + createMasterAgentEvolutionRuleInState, + createUserMasterMemory, + getMasterAgentEvolutionState, + recordMasterAgentEvolutionSignalInState, + updateMasterAgentEvolutionConfig, + updateMasterAgentEvolutionProposalStatus, + updateProjectAgentControls, +} from "@/lib/boss-data"; +import type { + MasterAgentEvolutionProposal, + MasterAgentEvolutionProposalType, + MasterAgentEvolutionSignalKind, + MasterAgentEvolutionMode, +} from "@/lib/boss-data"; + +const AUTO_APPLY_LOW_RISK_TYPES = new Set([ + "fast_path_rule", + "memory_patch", + "routing_preference_patch", +]); + +function isDeterministicStatusQuestion(text: string) { + return /(当前|现在|有没有|是否|什么|哪个|在线|状态|模型|后端|接管|gui|cli|hermes|claw)/i.test(text); +} + +function inferProposalFromSignal(input: { + kind: MasterAgentEvolutionSignalKind; + account: string; + projectId: string; + requestText: string; + signalId: string; +}): Omit< + Parameters[0], + "status" +> | null { + const requestText = input.requestText.trim(); + if ( + (input.kind === "repeated_question" || input.kind === "fast_path_candidate") && + isDeterministicStatusQuestion(requestText) + ) { + return { + proposalType: "fast_path_rule", + account: input.account, + projectId: input.projectId, + title: `新增 Fast Path:${requestText.slice(0, 32)}`, + summary: `检测到确定性问题反复进入主链,建议把“${requestText}”归入本地 Fast Path。`, + patchJson: { + matcher: requestText, + action: "local_status_reply", + }, + sourceSignalIds: [input.signalId], + confidence: 0.82, + riskLevel: "low", + }; + } + + if (input.kind === "user_correction") { + return { + proposalType: "memory_patch", + account: input.account, + projectId: input.projectId, + title: "沉淀用户纠正", + summary: `检测到用户纠正主Agent行为,建议写入长期工作规则:${requestText}`, + patchJson: { + scope: "global", + memoryType: "workflow_rule", + title: "用户纠正 · 工作规则", + content: requestText, + }, + sourceSignalIds: [input.signalId], + confidence: 0.78, + riskLevel: "low", + }; + } + + if (input.kind === "backend_fallback") { + return { + proposalType: "routing_preference_patch", + account: input.account, + projectId: input.projectId, + title: "调整后端路由偏好", + summary: "检测到后端回退,建议后续优先选择最近稳定的可用后端。", + patchJson: { + backendPreference: "prefer_available_runtime", + }, + sourceSignalIds: [input.signalId], + confidence: 0.72, + riskLevel: "low", + }; + } + + return null; +} + +async function applyLowRiskProposal(proposal: MasterAgentEvolutionProposal) { + if (proposal.proposalType === "memory_patch") { + const patch = proposal.patchJson as { + scope?: "global" | "project"; + projectId?: string; + title?: string; + content?: string; + memoryType?: "workflow_rule" | "user_preference" | "decision" | "project_progress"; + tags?: string[]; + }; + await createUserMasterMemory({ + account: proposal.account, + scope: patch.scope ?? "global", + projectId: patch.scope === "project" ? patch.projectId ?? proposal.projectId : undefined, + title: patch.title ?? proposal.title, + content: patch.content ?? proposal.summary, + memoryType: patch.memoryType ?? "workflow_rule", + tags: patch.tags ?? ["evolution"], + }); + } + + if (proposal.proposalType === "routing_preference_patch") { + const patch = proposal.patchJson as { backendOverride?: "hermes-runtime" | "claw-runtime" }; + if (patch.backendOverride) { + await updateProjectAgentControls("master-agent", { backendOverride: patch.backendOverride }, proposal.account); + } + } + + await createMasterAgentEvolutionRuleInState({ + ruleType: proposal.proposalType, + account: proposal.account, + projectId: proposal.projectId, + sourceProposalId: proposal.proposalId, + patchJson: proposal.patchJson, + }); +} + +async function maybeAutoApplyProposal(proposal: MasterAgentEvolutionProposal) { + const state = await getMasterAgentEvolutionState(); + if ( + state.config.mode !== "autonomous" || + !state.config.autoApplyLowRiskRules || + proposal.riskLevel !== "low" || + proposal.confidence < 0.75 || + !AUTO_APPLY_LOW_RISK_TYPES.has(proposal.proposalType) + ) { + return proposal; + } + + await applyLowRiskProposal(proposal); + return updateMasterAgentEvolutionProposalStatus({ + proposalId: proposal.proposalId, + status: "auto_applied", + account: proposal.account, + note: "autonomous 模式自动采纳低风险提案", + }); +} + +export async function setMasterAgentEvolutionMode(mode: MasterAgentEvolutionMode) { + return updateMasterAgentEvolutionConfig({ + mode, + autoApplyLowRiskRules: mode === "autonomous", + }); +} + +export async function listMasterAgentEvolutionSignals() { + return (await getMasterAgentEvolutionState()).signals; +} + +export async function listMasterAgentEvolutionProposals() { + return (await getMasterAgentEvolutionState()).proposals; +} + +export async function getMasterAgentEvolutionDashboard() { + return getMasterAgentEvolutionState(); +} + +export async function recordMasterAgentEvolutionSignal(input: { + kind: MasterAgentEvolutionSignalKind; + account: string; + projectId?: string; + requestText: string; + replyText?: string; + metadataJson?: Record; +}) { + const signal = await recordMasterAgentEvolutionSignalInState({ + kind: input.kind, + account: input.account, + projectId: input.projectId ?? "master-agent", + requestText: input.requestText, + replyText: input.replyText, + metadataJson: input.metadataJson, + }); + const proposalInput = inferProposalFromSignal({ + kind: signal.kind, + account: signal.account, + projectId: signal.projectId, + requestText: signal.requestText, + signalId: signal.signalId, + }); + if (!proposalInput) { + return { signal, proposal: null }; + } + const proposal = await createMasterAgentEvolutionProposalInState(proposalInput); + const appliedProposal = await maybeAutoApplyProposal(proposal); + return { signal, proposal: appliedProposal }; +} + +export async function approveMasterAgentEvolutionProposal(proposalId: string, account: string) { + const proposal = (await getMasterAgentEvolutionState()).proposals.find((item) => item.proposalId === proposalId); + if (!proposal) { + throw new Error("MASTER_AGENT_EVOLUTION_PROPOSAL_NOT_FOUND"); + } + await applyLowRiskProposal(proposal); + return updateMasterAgentEvolutionProposalStatus({ + proposalId, + status: "approved", + account, + note: "用户批准进化提案", + }); +} + +export async function rejectMasterAgentEvolutionProposal(proposalId: string, account: string) { + return updateMasterAgentEvolutionProposalStatus({ + proposalId, + status: "rejected", + account, + note: "用户拒绝进化提案", + }); +} diff --git a/tests/master-agent-evolution-engine.test.ts b/tests/master-agent-evolution-engine.test.ts new file mode 100644 index 0000000..9a9fcd3 --- /dev/null +++ b/tests/master-agent-evolution-engine.test.ts @@ -0,0 +1,73 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import os from "node:os"; +import path from "node:path"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; + +let runtimeRoot = ""; +let readState: (typeof import("../src/lib/boss-data"))["readState"]; +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"]; + +async function setup() { + if (runtimeRoot) { + return; + } + runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-evolution-engine-")); + process.env.BOSS_RUNTIME_ROOT = runtimeRoot; + process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); + const [data, evolution] = await Promise.all([ + import("../src/lib/boss-data.ts"), + import("../src/lib/master-agent-evolution.ts"), + ]); + readState = data.readState; + recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal; + listMasterAgentEvolutionProposals = evolution.listMasterAgentEvolutionProposals; + setMasterAgentEvolutionMode = evolution.setMasterAgentEvolutionMode; +} + +test.after(async () => { + if (runtimeRoot) { + await rm(runtimeRoot, { recursive: true, force: true }); + } +}); + +test.beforeEach(async () => { + await setup(); + await rm(runtimeRoot, { recursive: true, force: true }); + await mkdir(runtimeRoot, { recursive: true }); +}); + +test("recording repeated deterministic questions creates a pending fast_path_rule proposal in controlled mode", async () => { + const result = await recordMasterAgentEvolutionSignal({ + kind: "repeated_question", + projectId: "master-agent", + account: "17600003315", + requestText: "当前主节点在线吗", + }); + + assert.equal(result.signal.kind, "repeated_question"); + assert.equal(result.proposal?.proposalType, "fast_path_rule"); + assert.equal(result.proposal?.status, "pending_review"); + + const proposals = await listMasterAgentEvolutionProposals(); + assert.equal(proposals[0]?.proposalType, "fast_path_rule"); + assert.equal(proposals[0]?.status, "pending_review"); +}); + +test("autonomous mode auto applies low risk fast path proposals as evolution rules", async () => { + await setMasterAgentEvolutionMode("autonomous"); + + const result = await recordMasterAgentEvolutionSignal({ + kind: "repeated_question", + projectId: "master-agent", + account: "17600003315", + requestText: "当前主节点在线吗", + }); + + assert.equal(result.proposal?.status, "auto_applied"); + const state = await readState(); + assert.equal(state.masterAgentEvolutionRules.length, 1); + assert.equal(state.masterAgentEvolutionRules[0]?.ruleType, "fast_path_rule"); +}); diff --git a/tests/master-agent-evolution-routes.test.ts b/tests/master-agent-evolution-routes.test.ts new file mode 100644 index 0000000..d306bc2 --- /dev/null +++ b/tests/master-agent-evolution-routes.test.ts @@ -0,0 +1,125 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import os from "node:os"; +import path from "node:path"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; +import { NextRequest } from "next/server"; + +let runtimeRoot = ""; +let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"]; +let recordMasterAgentEvolutionSignal: (typeof import("../src/lib/master-agent-evolution"))["recordMasterAgentEvolutionSignal"]; +let GETEvolutionRoute: (typeof import("../src/app/api/v1/master-agent/evolution/route"))["GET"]; +let POSTEvolutionConfigRoute: (typeof import("../src/app/api/v1/master-agent/evolution/config/route"))["POST"]; +let POSTApproveRoute: (typeof import("../src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route"))["POST"]; +let POSTRejectRoute: (typeof import("../src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route"))["POST"]; +let AUTH_SESSION_COOKIE = ""; + +async function setup() { + if (runtimeRoot) { + return; + } + runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-evolution-routes-")); + process.env.BOSS_RUNTIME_ROOT = runtimeRoot; + process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); + const [data, auth, evolution, evolutionRoute, configRoute, approveRoute, rejectRoute] = await Promise.all([ + import("../src/lib/boss-data.ts"), + import("../src/lib/boss-auth.ts"), + import("../src/lib/master-agent-evolution.ts"), + import("../src/app/api/v1/master-agent/evolution/route.ts"), + import("../src/app/api/v1/master-agent/evolution/config/route.ts"), + import("../src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route.ts"), + import("../src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route.ts"), + ]); + createAuthSession = data.createAuthSession; + recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal; + GETEvolutionRoute = evolutionRoute.GET; + POSTEvolutionConfigRoute = configRoute.POST; + POSTApproveRoute = approveRoute.POST; + POSTRejectRoute = rejectRoute.POST; + AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE; +} + +async function createAdminRequest(url: string, body?: unknown) { + const session = await createAuthSession({ + account: "17600003315", + role: "highest_admin", + displayName: "Boss 超级管理员", + loginMethod: "password", + }); + return new NextRequest(url, { + method: body === undefined ? "GET" : "POST", + headers: { + cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`, + ...(body === undefined ? {} : { "content-type": "application/json" }), + }, + body: body === undefined ? undefined : JSON.stringify(body), + }); +} + +test.after(async () => { + if (runtimeRoot) { + await rm(runtimeRoot, { recursive: true, force: true }); + } +}); + +test.beforeEach(async () => { + await setup(); + await rm(runtimeRoot, { recursive: true, force: true }); + await mkdir(runtimeRoot, { recursive: true }); +}); + +test("GET /api/v1/master-agent/evolution returns config proposals and rules", async () => { + const response = await GETEvolutionRoute( + await createAdminRequest("http://127.0.0.1:3000/api/v1/master-agent/evolution"), + ); + assert.equal(response.status, 200); + const payload = await response.json() as { ok: boolean; proposals: unknown[]; rules: unknown[] }; + assert.equal(payload.ok, true); + assert.ok(Array.isArray(payload.proposals)); + assert.ok(Array.isArray(payload.rules)); +}); + +test("POST /api/v1/master-agent/evolution/config switches mode", async () => { + const response = await POSTEvolutionConfigRoute( + await createAdminRequest("http://127.0.0.1:3000/api/v1/master-agent/evolution/config", { mode: "autonomous" }), + ); + assert.equal(response.status, 200); + const payload = await response.json() as { ok: boolean; config?: { mode?: string } }; + assert.equal(payload.ok, true); + assert.equal(payload.config?.mode, "autonomous"); +}); + +test("approve and reject evolution proposals update proposal status", async () => { + const created = await recordMasterAgentEvolutionSignal({ + kind: "repeated_question", + account: "17600003315", + requestText: "当前主节点在线吗", + }); + const proposalId = created.proposal?.proposalId ?? ""; + assert.ok(proposalId); + + const approveResponse = await POSTApproveRoute( + await createAdminRequest(`http://127.0.0.1:3000/api/v1/master-agent/evolution/proposals/${proposalId}/approve`, {}), + { params: Promise.resolve({ proposalId }) }, + ); + assert.equal(approveResponse.status, 200); + const approvePayload = await approveResponse.json() as { ok: boolean; proposal?: { status?: string } }; + assert.equal(approvePayload.ok, true); + assert.equal(approvePayload.proposal?.status, "approved"); + + const created2 = await recordMasterAgentEvolutionSignal({ + kind: "backend_fallback", + account: "17600003315", + requestText: "切到稳定后端", + }); + const proposalId2 = created2.proposal?.proposalId ?? ""; + assert.ok(proposalId2); + const rejectResponse = await POSTRejectRoute( + await createAdminRequest(`http://127.0.0.1:3000/api/v1/master-agent/evolution/proposals/${proposalId2}/reject`, {}), + { params: Promise.resolve({ proposalId: proposalId2 }) }, + ); + assert.equal(rejectResponse.status, 200); + const rejectPayload = await rejectResponse.json() as { ok: boolean; proposal?: { status?: string } }; + assert.equal(rejectPayload.ok, true); + assert.equal(rejectPayload.proposal?.status, "rejected"); +}); diff --git a/tests/master-agent-evolution-state.test.ts b/tests/master-agent-evolution-state.test.ts new file mode 100644 index 0000000..17bfe67 --- /dev/null +++ b/tests/master-agent-evolution-state.test.ts @@ -0,0 +1,49 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import os from "node:os"; +import path from "node:path"; +import { mkdir, mkdtemp, rm } from "node:fs/promises"; + +let runtimeRoot = ""; +let readState: (typeof import("../src/lib/boss-data"))["readState"]; +let updateMasterAgentEvolutionConfig: (typeof import("../src/lib/boss-data"))["updateMasterAgentEvolutionConfig"]; + +async function setup() { + if (runtimeRoot) { + return; + } + runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-evolution-state-")); + process.env.BOSS_RUNTIME_ROOT = runtimeRoot; + process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); + const data = await import("../src/lib/boss-data.ts"); + readState = data.readState; + updateMasterAgentEvolutionConfig = data.updateMasterAgentEvolutionConfig; +} + +test.after(async () => { + if (runtimeRoot) { + await rm(runtimeRoot, { recursive: true, force: true }); + } +}); + +test.beforeEach(async () => { + await setup(); + await rm(runtimeRoot, { recursive: true, force: true }); + await mkdir(runtimeRoot, { recursive: true }); +}); + +test("boss state 初始化时包含 master agent evolution 默认配置与空集合", async () => { + const state = await readState(); + assert.equal(state.masterAgentEvolutionConfig.mode, "controlled"); + assert.equal(state.masterAgentEvolutionConfig.autoApplyLowRiskRules, false); + assert.deepEqual(state.masterAgentEvolutionSignals, []); + assert.deepEqual(state.masterAgentEvolutionProposals, []); + assert.deepEqual(state.masterAgentEvolutionRules, []); + assert.deepEqual(state.masterAgentEvolutionRunLogs, []); +}); + +test("master agent evolution 配置可切换 autonomous 并自动打开低风险自动采纳", async () => { + const config = await updateMasterAgentEvolutionConfig({ mode: "autonomous" }); + assert.equal(config.mode, "autonomous"); + assert.equal(config.autoApplyLowRiskRules, true); +});