{"version":3,"file":"evolution-mode.mjs","names":[],"sources":["../../../src/services/evolution-mode.ts"],"sourcesContent":["/**\n * Evolution Mode Service — stable vs evolving agent behavior.\n *\n * Two modes:\n *   stable   — default. The agent uses only static skills and does not\n *              modify any learned knowledge. Memory and skill tools are\n *              registered but return \"disabled in stable mode\" errors.\n *   evolving — full self-improvement. Memory writes, skill creation,\n *              skill patching, session recall, and periodic nudges.\n *\n * The mode is per-user and persisted to disk. A global default can be\n * set via OPENCLAWNCH_EVOLUTION_MODE env var.\n *\n * Nudge system:\n *   Every N turns, inject a system message reminder about:\n *   - Persisting important discoveries to agent memory\n *   - Creating skills from complex workflows\n *   The nudge intervals are configurable via env vars.\n */\n\nimport { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';\nimport { join } from 'node:path';\n\n// ─── Types ───────────────────────────────────────────────────────────────\n\nexport type EvolutionMode = 'stable' | 'evolving';\n\nexport interface EvolutionConfig {\n  /** Default mode for new users. Default: 'stable'. */\n  defaultMode?: EvolutionMode;\n  /** Turns between memory persistence nudges. Default: 10. */\n  memoryNudgeInterval?: number;\n  /** Turns between skill creation nudges. Default: 15. */\n  skillNudgeInterval?: number;\n  /** Minimum turns before first nudge. Default: 5. */\n  minTurnsBeforeNudge?: number;\n}\n\ninterface UserEvolutionState {\n  mode: EvolutionMode;\n  turnCount: number;\n  lastMemoryNudgeTurn: number;\n  lastSkillNudgeTurn: number;\n}\n\n// ─── Nudge Messages ──────────────────────────────────────────────────────\n\nconst MEMORY_NUDGE = `[Self-improvement reminder] If you discovered something useful in this conversation — a tool quirk, an environment fact, a user preference, or a lesson learned — save it to agent memory with the agent_memory tool (action: \"add\") so you remember it next time.`;\n\nconst SKILL_NUDGE = `[Self-improvement reminder] If you just completed a complex multi-step workflow (5+ tool calls), fixed a tricky error, or discovered a non-trivial DeFi strategy, consider saving it as a learned skill with the skill_evolve tool (action: \"create\") so you can reuse it efficiently next time.`;\n\n// ─── Evolution Mode Service ──────────────────────────────────────────────\n\nclass EvolutionModeService {\n  private config: Required<EvolutionConfig>;\n  private users = new Map<string, UserEvolutionState>();\n\n  constructor(config: EvolutionConfig = {}) {\n    const envDefault = process.env.OPENCLAWNCH_EVOLUTION_MODE as EvolutionMode | undefined;\n    this.config = {\n      defaultMode: config.defaultMode ?? envDefault ?? 'stable',\n      memoryNudgeInterval: config.memoryNudgeInterval\n        ?? (parseInt(process.env.OPENCLAWNCH_MEMORY_NUDGE_INTERVAL ?? '', 10) || 10),\n      skillNudgeInterval: config.skillNudgeInterval\n        ?? (parseInt(process.env.OPENCLAWNCH_SKILL_NUDGE_INTERVAL ?? '', 10) || 15),\n      minTurnsBeforeNudge: config.minTurnsBeforeNudge ?? 5,\n    };\n  }\n\n  // ── Mode Management ────────────────────────────────────────────────\n\n  getMode(userId: string): EvolutionMode {\n    return this.getState(userId).mode;\n  }\n\n  setMode(userId: string, mode: EvolutionMode): void {\n    const state = this.getState(userId);\n    state.mode = mode;\n    // Reset nudge counters on mode change\n    state.turnCount = 0;\n    state.lastMemoryNudgeTurn = 0;\n    state.lastSkillNudgeTurn = 0;\n    this.persistState(userId, state);\n  }\n\n  isEvolving(userId: string): boolean {\n    return this.getMode(userId) === 'evolving';\n  }\n\n  // ── Turn Tracking & Nudges ─────────────────────────────────────────\n\n  /**\n   * Record a turn and return any nudge messages that should be injected.\n   * Call this from after_tool_call or message_received.\n   * Returns null if no nudge is needed.\n   */\n  recordTurn(userId: string): string | null {\n    const state = this.getState(userId);\n    if (state.mode !== 'evolving') return null;\n\n    state.turnCount++;\n\n    // Don't nudge too early in the session\n    if (state.turnCount < this.config.minTurnsBeforeNudge) return null;\n\n    // Check for memory nudge\n    const turnsSinceMemoryNudge = state.turnCount - state.lastMemoryNudgeTurn;\n    if (turnsSinceMemoryNudge >= this.config.memoryNudgeInterval) {\n      state.lastMemoryNudgeTurn = state.turnCount;\n      return MEMORY_NUDGE;\n    }\n\n    // Check for skill nudge\n    const turnsSinceSkillNudge = state.turnCount - state.lastSkillNudgeTurn;\n    if (turnsSinceSkillNudge >= this.config.skillNudgeInterval) {\n      state.lastSkillNudgeTurn = state.turnCount;\n      return SKILL_NUDGE;\n    }\n\n    return null;\n  }\n\n  /**\n   * Get the current turn count for a user.\n   */\n  getTurnCount(userId: string): number {\n    return this.getState(userId).turnCount;\n  }\n\n  // ── State Management ───────────────────────────────────────────────\n\n  private getState(userId: string): UserEvolutionState {\n    let state = this.users.get(userId);\n    if (!state) {\n      state = this.loadState(userId);\n      this.users.set(userId, state);\n    }\n    return state;\n  }\n\n  private getStateDir(): string {\n    return join(process.env.HOME ?? '/tmp', '.openclawnch', 'evolution');\n  }\n\n  private getStatePath(userId: string): string {\n    const safeId = userId.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);\n    return join(this.getStateDir(), `${safeId}.json`);\n  }\n\n  private loadState(userId: string): UserEvolutionState {\n    try {\n      const filePath = this.getStatePath(userId);\n      if (existsSync(filePath)) {\n        const data = JSON.parse(readFileSync(filePath, 'utf8'));\n        return {\n          mode: data.mode === 'evolving' ? 'evolving' : 'stable',\n          turnCount: 0, // Reset per session\n          lastMemoryNudgeTurn: 0,\n          lastSkillNudgeTurn: 0,\n        };\n      }\n    } catch {\n      // Fall through to default\n    }\n\n    return {\n      mode: this.config.defaultMode,\n      turnCount: 0,\n      lastMemoryNudgeTurn: 0,\n      lastSkillNudgeTurn: 0,\n    };\n  }\n\n  private persistState(userId: string, state: UserEvolutionState): void {\n    try {\n      const dir = this.getStateDir();\n      if (!existsSync(dir)) mkdirSync(dir, { recursive: true });\n      writeFileSync(\n        this.getStatePath(userId),\n        JSON.stringify({ mode: state.mode }, null, 2),\n        'utf8',\n      );\n    } catch {\n      // Best effort\n    }\n  }\n\n  // ── Diagnostics ────────────────────────────────────────────────────\n\n  getStatus(): {\n    defaultMode: EvolutionMode;\n    memoryNudgeInterval: number;\n    skillNudgeInterval: number;\n    trackedUsers: number;\n    config: Required<EvolutionConfig>;\n  } {\n    return {\n      defaultMode: this.config.defaultMode,\n      memoryNudgeInterval: this.config.memoryNudgeInterval,\n      skillNudgeInterval: this.config.skillNudgeInterval,\n      trackedUsers: this.users.size,\n      config: this.config,\n    };\n  }\n}\n\n// ─── Singleton ───────────────────────────────────────────────────────────\n\nlet _instance: EvolutionModeService | null = null;\n\nexport function getEvolutionMode(config?: EvolutionConfig): EvolutionModeService {\n  if (!_instance) {\n    _instance = new EvolutionModeService(config);\n  }\n  return _instance;\n}\n\nexport function resetEvolutionMode(): void {\n  _instance = null;\n}\n"],"mappings":"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"}