""" Player Journal — lightweight per-player memory files. Each player gets a small text file (1-5 lines) that the model reads before responding and updates after meaningful interactions. The model has free reign to overwrite these files to keep them short and contextually relevant. Files stored at: /.txt For God mode: includes sentiment, relationship history, divine interactions. For Sudo mode: includes past builds, preferences, saved locations, skill level. Usage: from agent.tools.player_journal import read_journal, write_journal, format_journal_context # Read before responding context = read_journal(config, player) # Update after interaction write_journal(config, player, new_content) # Inject into system prompt prompt += format_journal_context(config, player) """ import os import threading from pathlib import Path from typing import Optional _lock = threading.Lock() def _journal_dir(config: dict) -> Path: return Path(config.get("journal_path", config.get("memory_path", "/tmp").replace("aigod_memory.json", "journals"))) def _journal_path(config: dict, player: str) -> Path: d = _journal_dir(config) # Sanitize player name safe = "".join(c for c in player if c.isalnum() or c in "_-").strip() if not safe: safe = "unknown" return d / f"{safe}.txt" def read_journal(config: dict, player: str) -> str: """Read a player's journal. Returns empty string if none exists.""" path = _journal_path(config, player) try: with open(path) as f: return f.read().strip() except FileNotFoundError: return "" def write_journal(config: dict, player: str, content: str): """Overwrite a player's journal. Aim for 1-5 lines but longer is fine for players with complex history. Max 10 lines — if longer, the model should condense older entries to make room for new context.""" path = _journal_path(config, player) d = _journal_dir(config) with _lock: d.mkdir(parents=True, exist_ok=True) lines = [l.strip() for l in content.strip().split("\n") if l.strip()] if len(lines) > 10: lines = lines[-10:] with open(path, "w") as f: f.write("\n".join(lines) + "\n") def append_journal(config: dict, player: str, line: str): """Add a line to a player's journal, keeping it under 5 lines. Oldest line gets dropped if at capacity.""" current = read_journal(config, player) lines = [l for l in current.split("\n") if l.strip()] lines.append(line.strip()) if len(lines) > 5: lines = lines[-5:] write_journal(config, player, "\n".join(lines)) def format_journal_context(config: dict, player: str) -> str: """Format journal for injection into the system prompt. Returns empty string if no journal exists.""" content = read_journal(config, player) if not content: return "" return f"\n=== PLAYER JOURNAL: {player} ===\n{content}\n=== END JOURNAL ===\n" def list_journals(config: dict) -> list: """List all players who have journals.""" d = _journal_dir(config) if not d.exists(): return [] return [f.stem for f in d.glob("*.txt") if f.stat().st_size > 0] # ── Tool handlers ────────────────────────────────────────────────────────── def handle_journal_read(config: dict, arguments: dict) -> dict: """Tool handler for reading a player journal.""" player = arguments.get("player", "") if not player: return {"ok": False, "error": "player name required"} content = read_journal(config, player) if content: return {"ok": True, "player": player, "journal": content} return {"ok": True, "player": player, "journal": "", "note": "No journal exists for this player yet."} def handle_journal_write(config: dict, arguments: dict) -> dict: """Tool handler for writing/overwriting a player journal. The model has free reign — it decides what to keep and what to overwrite.""" player = arguments.get("player", "") content = arguments.get("content", "") if not player: return {"ok": False, "error": "player name required"} if not content: return {"ok": False, "error": "content required"} write_journal(config, player, content) lines = len([l for l in content.strip().split("\n") if l.strip()]) return {"ok": True, "player": player, "lines": lines}