identity-context.js 5.32 KB
/**
 * 本体身份上下文(soul/identity/tools/heartbeat/OKR/hippocampus/skills)的共用读法。
 *
 * ── 修的是哪个坑 ──────────────────────────────────────────────────────────
 * graph-engine 驱动的 work 节点(`agent_call`)此前只有 `agent.md` 进提示词——
 * `PromptBuilder.build()` 走的是「protocol + agent.md + companyContextPath +
 * user.md/memory.md/近期记忆」这一条链路,`soul.md`/`identity.md`/`tools.md`/
 * `heartbeat.md`/OKR 目标/hippocampus 检索/skills 全部缺失。而交互式的
 * `agent-chat.js`(`buildSystemPrompt()`)一直都在读这些文件,两条链路从未同步过。
 * 静默到什么程度:agent 照样跑、照样成功,只是少了人格/工具约束/长期记忆检索,
 * 表现为「work 节点里的 agent 比 chat 里蠢一截」,没有任何报错或日志能看出原因。
 *
 * ── 这里只补缺的,不重复已有的 ────────────────────────────────────────────
 * `agent.md` 由 `PromptBuilder.getAgentPrompt()` 已经读过;`user.md`/`memory.md`/
 * 近期记忆由 `PromptBuilder.buildCompanyStateParts()` 已经读过——这个函数**不重复**
 * 那五样,只读 agent-chat 有、PromptBuilder 没有的那几样。
 *
 * ── ponytail:没有让 agent-chat.js 反过来调用这份实现 ─────────────────────
 * 两处现在是「同构但独立」的两份代码,不是真正共享一份——`buildSystemPrompt()`
 * 是长期稳定的高频路径,为了这次修复去动它,风险和收益不成比例。真要收敛成
 * 一份实现,得先给 `buildSystemPrompt()` 补一圈契约测试再动。升级路径:有余力
 * 时把 agent-chat.js 里对应的读文件片段也换成调这份实现,届时可以直接删掉
 * agent-chat.js 里那几段、只保留它自己特有的东西(company 环境上下文 / 运行时坐标
 * 声明 / userMessage 相关的 hippocampus 搜索文本拼接)。
 */
const fs = require('fs');
const path = require('path');
const { OkrService } = require('../../services/okr-service');
const { retrieveRelevantPlans } = require('../../services/memory-service');

// 与 agent-chat.js 同款用法:全局 OKR 索引(不按公司分库),module 级单例。
const okrService = new OkrService();

function readTrim(p) {
  try { return fs.readFileSync(p, 'utf-8').trim(); } catch { return ''; }
}

/**
 * @param {object} a
 * @param {string} a.bodyDir         本体目录(agent.md/soul.md/.claude/ 所在,只读)
 * @param {string} [a.agentId]       OKR/hippocampus 检索用的身份键;没有则跳过这两段
 * @param {string} [a.searchText=''] hippocampus 检索用的当前任务文本(agent-chat 用的是
 *                                   userMessage + todo.md,这里用调用方传入的任务描述)
 * @returns {string[]} 按顺序排好的提示词分段,调用方自己 push 进 parts 数组
 */
function buildIdentityContextParts({ bodyDir, agentId, searchText = '' }) {
  const parts = [];
  if (!bodyDir) return parts;

  // soul.md / identity.md / tools.md —— agent.md 已经由调用方读过,这里不重复
  for (const f of ['soul.md', 'identity.md', 'tools.md']) {
    const c = readTrim(path.join(bodyDir, f));
    if (c) parts.push(c);
  }

  const heartbeat = readTrim(path.join(bodyDir, 'heartbeat.md'));
  if (heartbeat) parts.push(heartbeat);

  if (agentId) {
    try {
      const goalContext = okrService.getActiveContextForAgent(agentId);
      if (goalContext && goalContext.trim()) parts.push(goalContext.trim());
    } catch (err) {
      // eslint-disable-next-line no-console
      console.error(`[IdentityContext] ⚠️ 读取目标上下文失败: ${err.message}`);
    }

    // user.md / memory.md / 近期记忆已经由 PromptBuilder.buildCompanyStateParts 读过,
    // 不在这里重复;hippocampus 检索原实现里跟那三样同一批读,但 PromptBuilder 没有
    // 对应覆盖,仍然缺,补上。
    const hippocampusPlans = retrieveRelevantPlans(agentId, searchText);
    if (hippocampusPlans) parts.push(hippocampusPlans);
  }

  const mainSkill = readTrim(path.join(bodyDir, '.claude/skills/main_skill/SKILL.md'));
  if (mainSkill) parts.push('---\n\n# 我的核心主技能 (执行准则)\n\n' + mainSkill);

  const localLessons = readTrim(path.join(bodyDir, 'workspace/_lessons.md'));
  if (localLessons) {
    parts.push('---\n\n## ⚠️ 本地试错防坑经验 (_lessons.md)\n不要重复踩下面的坑,仔细阅读并绕开:\n\n' + localLessons);
  }

  const skillsMd = readTrim(path.join(bodyDir, '.claude/skills/skills.md'));
  if (skillsMd) parts.push(skillsMd);

  // 扫描除 main_skill 外的所有 skill 子目录
  const skillsDir = path.join(bodyDir, '.claude', 'skills');
  if (fs.existsSync(skillsDir)) {
    for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
      if (!entry.isDirectory() || entry.name === 'main_skill') continue;
      const c = readTrim(path.join(skillsDir, entry.name, 'SKILL.md'));
      if (c) parts.push(`---\n\n## 依赖技能: ${entry.name}\n\n${c}`);
    }
  }

  return parts;
}

module.exports = { buildIdentityContextParts };