Commit 66cc51954c6c40381733453afdc1cbcc370a27d1

Authored by yibo
1 parent 4d518483

feat(agent): implement active heartbeat service for autonomous cycles

- Extracted HeartbeatService to periodically wake up agents flagged with 'heartbeat.enabled'
- Sends a silent [SYSTEM HEARTBEAT] message to trigger self-checks (e.g. OKR state validation)
- Automatically integrated service dispatch hook in server startup sequence
- Configured qa-judge to run autonomous heartbeat every 10 min
data/agents/qa-judge/config.json
... ... @@ -21,5 +21,9 @@
21 21 "maxMemoryMB": 512,
22 22 "maxDiskMB": 1024,
23 23 "maxProcesses": 5
  24 + },
  25 + "heartbeat": {
  26 + "enabled": true,
  27 + "intervalMinutes": 10
24 28 }
25 29 }
26 30 \ No newline at end of file
... ...
docs/system-design/23-agent-active-heartbeat.md 0 → 100644
  1 +# 23. Agent Active Heartbeat System — 主动心跳与监控架构
  2 +
  3 +> **版本**: v1.0 (设计稿)
  4 +> **日期**: 2026-03-25
  5 +> **状态**: 规划中
  6 +> **前置**: Phase 3 记忆架构已完成,OKR 目标大盘已上线
  7 +
  8 +---
  9 +
  10 +## 一、需求背景与痛点
  11 +
  12 +当前的 LemonBoss 架构中,所有的 Agent(包含管理者、执行者、QA 仲裁者等)都处于**“被动响应”**状态。
  13 +即:只有当人类或者其他 Agent 发送一条消息(如 `TaskDispatch` 或直接 Chat),它们才会被唤醒并进行一轮推理和动作执行。
  14 +
  15 +**痛点:**
  16 +随着 OKR 系统(Goal Nodes)复杂度的增加,如果一个任务的负责人(Owner)迟迟不推进进度,或者产出质量不达标。在没有外部输入时,系统的主节点(如 `qa-judge` 质量裁判,或者 `master` 主节点)只能一直假死等待,这严重违背了“**自主智能体(Autonomous Agent)**”能在后台 24/7 监控并主动下发指令的设计初衷。
  17 +
  18 +**目标:**
  19 +使特定的高级 Agent(如 `qa-judge`)具备内建的相对时间感知能力。通过实现一套主推的“心跳机制(Active Heartbeat)”,使其能每隔 N 分钟自动“苏醒”,根据当前注入体内的 OKR 目标大盘数据,主动进行一次进度巡检、质量总结,并主动向下游 Agent 节点派发反馈任务或催办通知。
  20 +
  21 +---
  22 +
  23 +## 二、架构设计原理
  24 +
  25 +由于 LLM(如 Claude)本身是无状态、无法自主定时的算法黑盒,我们必须通过 Node.js 后端作为起搏器(Pacemaker)来主动触碰 Agent 的执行核心。
  26 +
  27 +### 1. 核心流程图
  28 +
  29 +```mermaid
  30 +sequenceDiagram
  31 + participant Cron as Node.js Heartbeat Service
  32 + participant Goal as OKR / Goals Data
  33 + participant Agent as qa-judge (Agent)
  34 + participant Worker as poet (Downstream Agent)
  35 +
  36 + loop 每 N 分钟
  37 + Cron->>Goal: 获取当前活跃的 OKR 大盘状态
  38 + Cron->>Agent: [SYSTEM HEARTBEAT] 唤醒 (无前端UI展示)
  39 + Note over Agent: 挂载 memory、heartbeat.md 及最新 Goal Context
  40 + Agent->>Agent: 推理分析 (是否有节点超时/待验收?)
  41 +
  42 + alt 需要干预
  43 + Agent->>Worker: API调用: TaskDispatch (催促或驳回重做)
  44 + Agent->>Agent: "已向负责人发送警告"
  45 + else 一切正常
  46 + Agent->>Agent: "保持观察,无需操作"
  47 + end
  48 + Agent-->>Cron: 结束本次心跳推理 (保存 session 记录入短期记忆)
  49 + end
  50 +```
  51 +
  52 +### 2. 参与模块设计
  53 +
  54 +#### (1) 后端起搏器 (`server/services/heartbeat-service.js`)
  55 +- 系统级别的 `setInterval` 或使用 `node-cron` 定时任务管理器。
  56 +- 模块初始化时,读取注册了 `cron.json` 或在 `config.json` 中配置了 `active_heartbeat: true` 的全网 Agent。
  57 +- 按设定时间周期(如每 5 分钟、或 15 分钟),发起内部进程级或 HTTP `POST /api/agent/:id/chat` 请求。
  58 +- **消息构造**:必须带有特殊的标记,如 `[SYSTEM HEARTBEAT] 这是系统周期心跳。时间:{当前时间}。如果无需采取行动,请只回复[心跳正常];如果发现 OKR 或任务阻塞,请使用你的 dispatch 工具发送跟进动作。`
  59 +
  60 +#### (2) Agent 身份与职责声明 (`data/agents/qa-judge/heartbeat.md`)
  61 +这部分将作为心跳特供的系统注入层。因为 Agent 被系统唤醒后可能“不知所措”,所以需要在注入时提供明确的**心跳策略**。
  62 +- `qa-judge` 的 `heartbeat.md` 应当规定:
  63 + - 检查当前全局进度是低于预期、正常还是卡点。
  64 + - 对于已经标记为 `ready_for_review` 的节点,主动执行抽样检查工具,如果抽样不通过,则主动调度退回给原作者。
  65 + - 规定其只有遇到问题才说话,没事的时候必须节约 Token,直接结案。
  66 +
  67 +#### (3) 会话隐藏与静默运行 (隐式聊天机制)
  68 +为了不污染用户的主聊天界面:
  69 +- 心跳触发的 Conversation 应当拥有独特的 ID(如 `heartbeat-177xxxxx`)。
  70 +- 在前端 UI(AgentChat.vue)中,如果不特别切换到 heartbeat 专区,应该不展示这类后台巡逻日志,以保持人类界面的干净。
  71 +- 但心跳的整个完整推演过程(产生的 session)**必须依然进入现有的 Memory 三层架构**。也就是说,如果 `qa-judge` 在心跳中发出了很多派发任务,这个行为依然会记录进 `short-term/session.md` 里并最终写进日常报表(Daily Report)。
  72 +
  73 +---
  74 +
  75 +## 三、待实现 TODO 计划
  76 +
  77 +- [ ] **1. Heartbeat 后端引擎设计**
  78 + - 在 `server/services/heartbeat-service.js` 实现通用的心跳轮询控制器。
  79 + - 能够读取各个 Agent 配置的 `heartbeatInterval`(支持独立配置时间间隔,如 qa-judge 15分钟,master 30分钟)。
  80 +- [ ] **2. 隐式对话 API (`agent-chat.js` 支持)**
  81 + - 修改 `agent-chat.js` 中的底层 ACP 唤起逻辑,支持传入 `silent: true` 以及 `isHidden` 参数。
  82 + - 确保这些会话依然会经历 Phase 1 的记忆提取。
  83 +- [ ] **3. `qa-judge` 的专项 prompt 优化**
  84 + - 完善 `qa-judge/heartbeat.md` 与 `qa-judge/cron.json`,使其明确自己“裁判+巡检”的作用和判定 OKR 进度的标准。
  85 +- [ ] **4. 拦截发散风暴 (Anti-Loop 制衡机制)**
  86 + - 防止 Agent 每次心跳都在给别人发重复的警告。
  87 + - **方案**:因为有 Memory 架构的存在,Agent 在心跳前会读到自己最近的 session(如“我在 15分钟前刚发过催促”),因此可以通过 prompt 指导它:“如果发现最近一次记忆中已经催促过了,请安静等待至少半天再做后续操作”。这完美与最新的 22 号文档中完成的 Memory 系统契合互补。
  88 +
  89 +---
  90 +
  91 +## 四、技术优势
  92 +
  93 +1. **“起搏”+“记忆”打造真实自主引擎**:依赖已实现的海马体与短期记忆,Agent 不再是简单的代码循环,而是拥有自己时间轴与经验累积的鲜活生命体。
  94 +2. **极低侵入性**:依然可以复用现有的 Claude CLI 通道和 `runClaudeCliSession` 函数,仅仅是多了一个虚拟的“系统定时发信人”。
  95 +3. **闭环 OODA loop**:观察(OKR数据注入)- 导向(heartbeat.md规则)- 决策(推理判定)- 行动(ToolDispatch),在此实现完整的智能自主控制闭环。
... ...
server/index.js
... ... @@ -60,7 +60,7 @@ async function start() {
60 60 ensureDataDirs();
61 61  
62 62 const host = config.host || '0.0.0.0';
63   - server.listen(config.port, host, () => {
  63 + server.listen(Number(config.port), host, () => {
64 64 console.log(`✅ 服务启动: http://${host}:${config.port}`);
65 65 });
66 66  
... ... @@ -70,9 +70,11 @@ async function start() {
70 70 // 插件系统(异步,不阻塞 HTTP)
71 71 initPlugins().catch(err => console.error('插件启动失败:', err.message));
72 72  
73   - // 恢复中断任务(延迟 5s)
  73 + // 恢复中断任务并起搏 Agent (延迟 5s,等 HTTP 服务完全就绪)
74 74 setTimeout(() => {
75 75 resumeIncompleteTasks().catch(err => console.error('[Startup] 恢复任务失败:', err.message));
  76 + const heartbeatService = require('./services/heartbeat-service');
  77 + heartbeatService.start();
76 78 }, 5000);
77 79 } catch (error) {
78 80 console.error('Failed to start server:', error);
... ...
server/routes/agent-chat.js
... ... @@ -1151,7 +1151,8 @@ router.post('/:agentId/chat', async (req, res) => {
1151 1151 meta = {
1152 1152 conversationId: convId,
1153 1153 agentId,
1154   - title: message.slice(0, 50),
  1154 + title: req.body.isHeartbeat ? '⏱️ 自主心跳巡检' : message.slice(0, 50),
  1155 + type: req.body.isHeartbeat ? 'heartbeat' : 'chat',
1155 1156 createdAt: new Date().toISOString(),
1156 1157 updatedAt: new Date().toISOString(),
1157 1158 status: 'running',
... ...
server/services/heartbeat-service.js 0 → 100644
  1 +const fs = require('fs');
  2 +const path = require('path');
  3 +const http = require('http');
  4 +const config = require('../config');
  5 +
  6 +const DATA_DIR = path.resolve(__dirname, '../../data/agents');
  7 +
  8 +class HeartbeatService {
  9 + constructor() {
  10 + this.timers = new Map(); // agentId -> NodeJS.Timeout
  11 + this.port = config.port || 3000;
  12 + }
  13 +
  14 + /**
  15 + * 初始化并启动全局的心跳监控
  16 + */
  17 + start() {
  18 + console.log('[Heartbeat] 💓 开始初始化 Agent 心跳服务...');
  19 + if (!fs.existsSync(DATA_DIR)) return;
  20 +
  21 + const agents = fs.readdirSync(DATA_DIR).filter(f => {
  22 + const p = path.join(DATA_DIR, f);
  23 + return fs.lstatSync(p).isDirectory() && fs.existsSync(path.join(p, 'config.json'));
  24 + });
  25 +
  26 + for (const agentId of agents) {
  27 + this._setupAgentHeartbeat(agentId);
  28 + }
  29 + }
  30 +
  31 + /**
  32 + * 停止所有心跳
  33 + */
  34 + stop() {
  35 + for (const [agentId, timer] of this.timers.entries()) {
  36 + clearInterval(timer);
  37 + }
  38 + this.timers.clear();
  39 + console.log('[Heartbeat] 🛑 心跳服务已停止');
  40 + }
  41 +
  42 + /**
  43 + * 为单个 Agent 根据 config 设置心跳
  44 + */
  45 + _setupAgentHeartbeat(agentId) {
  46 + try {
  47 + const configPath = path.join(DATA_DIR, agentId, 'config.json');
  48 + const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
  49 +
  50 + if (cfg.heartbeat && cfg.heartbeat.enabled === true) {
  51 + const intervalMs = (cfg.heartbeat.intervalMinutes || 15) * 60 * 1000;
  52 +
  53 + // 确保不会重复启动
  54 + if (this.timers.has(agentId)) {
  55 + clearInterval(this.timers.get(agentId));
  56 + }
  57 +
  58 + const timer = setInterval(() => this._triggerHeartbeat(agentId), intervalMs);
  59 + this.timers.set(agentId, timer);
  60 + console.log(`[Heartbeat] ✅ 已挂载 ${agentId} 的主动心跳 (间隔: ${cfg.heartbeat.intervalMinutes} 分钟)`);
  61 + }
  62 + } catch (err) {
  63 + console.warn(`[Heartbeat] ⚠️ 读取 ${agentId} 配置失败: ${err.message}`);
  64 + }
  65 + }
  66 +
  67 + /**
  68 + * 内部方法:向 Agent 的 Chat API 发送一次隐式的系统心跳
  69 + */
  70 + _triggerHeartbeat(agentId) {
  71 + const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19);
  72 + console.log(`[Heartbeat] 💓 触发 ${agentId} 主动心跳 (${timestamp})...`);
  73 +
  74 + const convId = `heartbeat-${Date.now().toString(36)}`;
  75 + const heartbeatMessage = [
  76 + `[SYSTEM HEARTBEAT]`,
  77 + `当前系统时间:${timestamp}`,
  78 + `这是一个后台周期的主动心跳触发。`,
  79 + `要求:请根据你的身份和 heartbeat.md 规则,扫描当前最新的全局 OKR 目标状态和节点。`,
  80 + `1. 如果发现某个负责人超时、卡点或产出需要仲裁,请主动使用 dispatch 工具给他们发送通知或驳回。`,
  81 + `2. 如果一切正常,无事发生,请直接回复“[心跳正常]”并且不要使用任何工具。`,
  82 + `无需等待人类确认,请完全自主决策。`
  83 + ].join('\n');
  84 +
  85 + const postData = JSON.stringify({
  86 + message: heartbeatMessage,
  87 + conversationId: convId,
  88 + isHeartbeat: true // 特殊标记,用于前端或后端识别
  89 + });
  90 +
  91 + const options = {
  92 + hostname: 'localhost',
  93 + port: this.port,
  94 + path: `/api/agent/${agentId}/chat`,
  95 + method: 'POST',
  96 + headers: {
  97 + 'Content-Type': 'application/json',
  98 + 'Content-Length': Buffer.byteLength(postData)
  99 + }
  100 + };
  101 +
  102 + const req = http.request(options, (res) => {
  103 + let body = '';
  104 + res.on('data', (chunk) => body += chunk);
  105 + res.on('end', () => {
  106 + if (res.statusCode >= 200 && res.statusCode < 300) {
  107 + console.log(`[Heartbeat] 📨 ${agentId} 成功启动心跳推理会话 (${convId})`);
  108 + } else {
  109 + console.warn(`[Heartbeat] ⚠️ ${agentId} 心跳触发异常: ${res.statusCode} ${body}`);
  110 + }
  111 + });
  112 + });
  113 +
  114 + req.on('error', (err) => {
  115 + console.error(`[Heartbeat] ❌ ${agentId} 心跳请求失败: ${err.message}`);
  116 + });
  117 +
  118 + req.write(postData);
  119 + req.end();
  120 + }
  121 +}
  122 +
  123 +// 导出单例
  124 +const heartbeatService = new HeartbeatService();
  125 +module.exports = heartbeatService;
... ...