/** * ═══════════════════════════════════════════════════════════════ * LemonBoss Agent — 通用运行时 * ═══════════════════════════════════════════════════════════════ * * 核心理念: * - 只有 1 个 Agent 类, 不做继承 * - 每个 Agent 是 data/agents/{id}/ 下的一个配置目录 * - 行为由 agent.md / soul.md 等 Bootstrap Files 决定 * - 四层 (master/cxo/role/skill) 的区别靠文件内容, 不靠代码 * * Agent 目录结构: * data/agents/{id}/ * ├── agent.md 核心指令 (职责/规则/工具) * ├── soul.md 灵魂 (人格/思维/风格) * ├── identity.md 外显身份 (名称/头像/问候) * ├── user.md 上下文画像 (上层偏好) * ├── tools.md 工具使用备注 * ├── heartbeat.md 心跳任务清单 * ├── skills/ 技能目录 * │ ├── skills.md 技能索引 * │ └── {name}/ 具体技能 * ├── memory/ 记忆目录 * │ ├── memory.md 长期记忆索引 * │ ├── short-term/ * │ ├── long-term/ * │ ├── sessions/ * │ └── daily/ * ├── knowledge/ 知识库 * ├── workspace/ 自治工作区 * │ ├── code/ * │ ├── data/ * │ └── assets/ * ├── conversations/ 对话会话 * │ └── {convId}/ conversationId 层 * │ ├── meta.json 对话元数据 * │ ├── history.json 有序消息列表 * │ └── {msgId}/ messageId 层 * │ └── messages.log 单轮日志 * ├── logs/ 日志 * └── config.json 运行配置 */ import { readFile, readdir, access, constants } from 'node:fs/promises'; import { join } from 'node:path'; import { EventEmitter } from 'node:events'; // ───────────────────────────────────────────── // Bootstrap File 定义 // ───────────────────────────────────────────── /** * Agent 的 6 大 Bootstrap Files * required: 必须存在 optional: 可选 */ const BOOTSTRAP_FILES = [ { name: 'agent.md', key: 'agentMd', required: true, desc: '操作规则' }, { name: 'soul.md', key: 'soulMd', required: true, desc: '灵魂定义' }, { name: 'identity.md', key: 'identityMd', required: false, desc: '外显身份' }, { name: 'user.md', key: 'userMd', required: false, desc: '上下文画像' }, { name: 'tools.md', key: 'toolsMd', required: false, desc: '工具备注' }, { name: 'heartbeat.md', key: 'heartbeat', required: false, desc: '心跳任务' }, ]; /** * Agent 目录内的标准子目录 */ const STANDARD_SUBDIRS = [ 'skills', 'memory', 'memory/short-term', 'memory/long-term', 'memory/sessions', 'memory/daily', 'knowledge', 'workspace', 'workspace/code', 'workspace/data', 'workspace/assets', 'conversations', 'logs', ]; // ───────────────────────────────────────────── // Agent 类 // ───────────────────────────────────────────── export class Agent extends EventEmitter { /** * @param {string} agentDir - Agent 实例目录的绝对路径 */ constructor(agentDir) { super(); this.dir = agentDir; this.id = null; this.loaded = false; // Bootstrap Files 内容 this.bootstrap = {}; // 运行配置 this.config = {}; // Skills 列表 this.skills = []; // ACP 内核 (实际连接在 start() 时建立) this.acp = null; // 运行状态 this.status = 'idle'; // idle | loading | running | paused | error // 对话会话管理 this._activeConvId = null; // 当前活跃的 conversationId this._messageCount = 0; // 当前会话的消息计数 (用于判断首次/后续) } // ───────────────────────────────────────── // 生命周期: load → start → stop // ───────────────────────────────────────── /** * 从目录加载 Agent 配置 * 读取所有 Bootstrap Files + config.json + skills 目录 */ async load() { this.status = 'loading'; this.emit('status', 'loading'); try { // 1. 读取 config.json this.config = await this._readJSON('config.json'); this.id = this.config.id || this._inferIdFromDir(); // 2. 读取 Bootstrap Files for (const file of BOOTSTRAP_FILES) { const content = await this._readFileOpt(file.name); if (content === null && file.required) { throw new Error(`必需的 Bootstrap File 缺失: ${file.name} (${file.desc})`); } this.bootstrap[file.key] = content; } // 3. 读取 skills 索引 this.bootstrap.skillsMd = await this._readFileOpt('skills/skills.md'); // 4. 读取 memory 索引 this.bootstrap.memoryMd = await this._readFileOpt('memory/memory.md'); // 5. 扫描 skills 目录 this.skills = await this._scanSkills(); this.loaded = true; this.status = 'idle'; this.emit('loaded', this.getSummary()); return this; } catch (err) { this.status = 'error'; this.emit('error', err); throw err; } } /** * 组装系统指令 (System Prompt) * 将所有 Bootstrap Files 按顺序拼接, 注入 ACP 上下文 */ buildSystemPrompt() { const parts = [ this.bootstrap.agentMd, this.bootstrap.soulMd, this.bootstrap.identityMd, this.bootstrap.userMd, this.bootstrap.toolsMd, this.bootstrap.skillsMd, this.bootstrap.memoryMd, ]; // 注入所有扫描到的 SKILL.md 内容 if (this.skills && this.skills.length > 0) { for (const skill of this.skills) { if (skill.content) { parts.push(`## Skill: ${skill.name}\n\n${skill.content}`); } } } return parts .filter(Boolean) .join('\n\n---\n\n'); } /** * 启动 Agent (连接 ACP + 开始 Loop) */ async start() { if (!this.loaded) { await this.load(); } this.status = 'running'; this.emit('status', 'running'); // 组装系统指令 const systemPrompt = this.buildSystemPrompt(); // TODO: 连接 ACP 内核 // this.acp = await launchACP({ // systemPrompt, // cwd: join(this.dir, 'workspace'), // allowedPaths: [this.dir], // env: await this._loadEnv(), // }); // TODO: 启动心跳 (如果有 heartbeat.md) // if (this.bootstrap.heartbeat) { // this._startHeartbeat(); // } this.emit('started', this.id); return this; } /** * 停止 Agent */ async stop() { this.status = 'idle'; this.emit('status', 'idle'); // TODO: 关闭 ACP 连接 // if (this.acp) { // await this.acp.close(); // this.acp = null; // } this.emit('stopped', this.id); return this; } // ───────────────────────────────────────── // 5 阶段 Agentic Loop // ───────────────────────────────────────── /** * 执行一轮完整的 Loop */ async runLoop() { this.emit('loop:start', this.id); // Phase 1: Perceive — 感知环境 const env = await this.perceive(); this.emit('loop:phase', { phase: 'perceive', data: env }); // Phase 2: Reason — 推理规划 const plan = await this.reason(env); this.emit('loop:phase', { phase: 'reason', data: plan }); // Phase 3: Execute — 执行动作 const results = await this.execute(plan); this.emit('loop:phase', { phase: 'execute', data: results }); // Phase 4: Feedback — 收集反馈 const feedback = await this.feedback(results); this.emit('loop:phase', { phase: 'feedback', data: feedback }); // Phase 5: Reflect — 反思优化 const reflection = await this.reflect(feedback); this.emit('loop:phase', { phase: 'reflect', data: reflection }); this.emit('loop:end', { id: this.id, reflection }); return reflection; } /** * Phase 1: 感知环境 */ async perceive() { // TODO: 从 Gateway 获取消息、从上级获取任务、读取当前状态 return { timestamp: new Date().toISOString(), agentId: this.id, // externalInputs: [], // parentTasks: [], // currentState: {}, // goal: this.config.goal_id, }; } /** * Phase 2: 推理与规划 */ async reason(envState) { // TODO: 调用 ACP 内核进行推理 // const plan = await this.acp.reason({ envState, memory: ... }); return { tasks: [] }; } /** * Phase 3: 执行 */ async execute(plan) { // TODO: 执行任务 (自己执行 or 分派给下级) return []; } /** * Phase 4: 收集反馈 */ async feedback(results) { // TODO: 分析执行结果 return { success: [], failures: [], metrics: {} }; } /** * Phase 5: 反思 * 注意: 不同层级的反思行为由 soul.md 中的思维方式引导 * 不需要代码级别区分 — ACP 内核根据 soul.md 自行决定反思深度 */ async reflect(feedback) { // TODO: 调用 ACP 内核进行反思 // const reflection = await this.acp.reflect({ feedback, goal: ... }); return { adapted: false }; } // ───────────────────────────────────────── // 对话会话管理 // 存储结构: conversations/{convId}/ // ├── meta.json 对话元数据 // ├── history.json 有序消息列表 [{ role, content, messageId, timestamp }] // └── messages.log 统一单文件的所有日志流 (包含打字和思维过程) // ───────────────────────────────────────── /** * 创建新对话 * @param {string} [title] - 对话标题 * @returns {{ conversationId: string }} */ async createConversation(title = '') { const convId = `conv-${Date.now().toString(36)}`; const convDir = join(this.dir, 'conversations', convId); const { mkdir, writeFile } = await import('node:fs/promises'); await mkdir(convDir, { recursive: true }); const meta = { conversationId: convId, agentId: this.id, title, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), status: 'running', stats: { messageCount: 0, toolCallCount: 0 }, }; await writeFile(join(convDir, 'meta.json'), JSON.stringify(meta, null, 2)); await writeFile(join(convDir, 'history.json'), '[]'); this._activeConvId = convId; this._messageCount = 0; this.emit('conversation:created', { conversationId: convId }); return { conversationId: convId }; } /** * 列出所有对话 */ async listConversations() { const convRoot = join(this.dir, 'conversations'); try { const entries = await readdir(convRoot, { withFileTypes: true }); const convs = []; for (const entry of entries) { if (!entry.isDirectory()) continue; try { const meta = JSON.parse(await readFile(join(convRoot, entry.name, 'meta.json'), 'utf-8')); convs.push(meta); } catch { /* skip invalid */ } } return convs.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); } catch { return []; } } /** * 加载对话历史 * @param {string} convId - conversationId * @returns {object[]} 消息列表 */ async loadConversationHistory(convId) { try { return JSON.parse(await readFile(join(this.dir, 'conversations', convId, 'history.json'), 'utf-8')); } catch { return []; } } /** * 追加消息到对话 * @param {string} convId * @param {{ role: string, content: string, messageId: string }} msg */ async appendMessage(convId, msg) { const { writeFile, appendFile } = await import('node:fs/promises'); const convDir = join(this.dir, 'conversations', convId); // 1. 写入 conversation 层日志 const ts = new Date().toISOString(); const tag = msg.role === 'user' ? 'USER' : 'AGENT'; await appendFile(join(convDir, 'messages.log'), `[${ts}] [${tag}]\n${msg.content}\n\n`, 'utf-8'); // 2. 追加到 history.json const history = await this.loadConversationHistory(convId); history.push({ ...msg, timestamp: ts }); await writeFile(join(convDir, 'history.json'), JSON.stringify(history, null, 2)); // 3. 更新 meta.json try { const meta = JSON.parse(await readFile(join(convDir, 'meta.json'), 'utf-8')); meta.updatedAt = ts; meta.stats.messageCount = history.length; await writeFile(join(convDir, 'meta.json'), JSON.stringify(meta, null, 2)); } catch { /* skip */ } } /** * 发送消息到 Agent 并获取回复 (多轮对话核心接口) * * 三种场景自动处理: * A. 全新对话的首条消息 → 注入 system prompt + 用户消息 * B. 同进程中的后续消息 → 直接发 (ACP 会话保持上下文) * C. 服务重启后恢复 → 注入 system prompt + 历史摘要 + 用户消息 * * @param {string} convId - conversationId * @param {string} message - 用户消息文本 * @param {object} [options] * @param {function} [options.onChunk] - 流式文本回调 (text) * @param {function} [options.onThinking] - 思考过程回调 (text) * @returns {Promise<{ output: string, messageId: string }>} */ async sendMessage(convId, message, options = {}) { if (!this.acp) throw new Error('Agent 未启动 (请先调用 start())'); const isNewSession = this._activeConvId !== convId || this._messageCount === 0; this._activeConvId = convId; // 构建 prompt let prompt; if (isNewSession) { const systemPrompt = this.buildSystemPrompt(); const history = await this.loadConversationHistory(convId); if (history.length === 0) { // 场景 A: 全新对话 prompt = `${systemPrompt}\n\n---\n\n## 用户消息\n\n${message}`; } else { // 场景 C: 恢复上下文 const recent = history.slice(-20); const historyText = recent.map(m => { const role = m.role === 'user' ? 'Human' : 'Assistant'; const content = m.content.length > 500 ? m.content.slice(0, 500) + '...(截断)' : m.content; return `${role}: ${content}`; }).join('\n\n'); prompt = `${systemPrompt}\n\n---\n\n## 之前的对话历史\n\n${historyText}\n\n---\n\n## 用户新消息\n\n${message}`; } } else { // 场景 B: 同进程多轮 prompt = message; } this._messageCount++; // 实际发送由子类或外部通过 this.acp 实现 // 这里定义接口规范, 具体 protocol 实现在 agent-chat.js 路由中 this.emit('message:send', { convId, prompt, messageCount: this._messageCount }); return { prompt, messageCount: this._messageCount }; } // ───────────────────────────────────────── // 信息查询 // ───────────────────────────────────────── /** * 获取 Agent 摘要信息 */ getSummary() { return { id: this.id, dir: this.dir, type: this.config.type || 'unknown', parentId: this.config.parent_id || null, status: this.status, skillCount: this.skills.length, hasHeartbeat: !!this.bootstrap.heartbeat, bootstrapFiles: BOOTSTRAP_FILES .filter(f => this.bootstrap[f.key] !== null) .map(f => f.name), }; } /** * 获取 Agent 类型 */ get type() { return this.config.type || 'unknown'; } /** * 获取上级 Agent ID */ get parentId() { return this.config.parent_id || null; } // ───────────────────────────────────────── // 内部工具方法 // ───────────────────────────────────────── /** * 从目录名推断 Agent ID */ _inferIdFromDir() { return this.dir.split('/').pop(); } /** * 读取文件 (必须存在) */ async _readFile(filename) { const filepath = join(this.dir, filename); return readFile(filepath, 'utf-8'); } /** * 读取文件 (可选, 不存在返回 null) */ async _readFileOpt(filename) { const filepath = join(this.dir, filename); try { await access(filepath, constants.R_OK); return readFile(filepath, 'utf-8'); } catch { return null; } } /** * 读取 JSON 文件 */ async _readJSON(filename) { const content = await this._readFile(filename); return JSON.parse(content); } /** * 扫描 skills/ 目录, 返回技能列表 */ async _scanSkills() { const skillsDir = join(this.dir, 'skills'); try { await access(skillsDir, constants.R_OK); } catch { return []; } const entries = await readdir(skillsDir, { withFileTypes: true }); const skills = []; for (const entry of entries) { if (!entry.isDirectory()) continue; const skillDir = join(skillsDir, entry.name); const skillMdPath = join(skillDir, 'SKILL.md'); try { await access(skillMdPath, constants.R_OK); const content = await readFile(skillMdPath, 'utf-8'); skills.push({ name: entry.name, dir: skillDir, content, }); } catch { // 没有 SKILL.md 的目录跳过 } } return skills; } } export default Agent;