""" πŸ”± Imperial Compaction Engine (μ§€λŠ₯ν˜• μ»¨ν…μŠ€νŠΈ μ••μΆ• μ—”μ§„) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Claw Code의 'Compaction' κΈ°μˆ μ„ μ‰λ„μš° 브레인 코어에 μ΅œμ ν™”ν•˜μ—¬ μ΄μ‹ν•œ λͺ¨λ“ˆμž…λ‹ˆλ‹€. λŒ€ν™”κ°€ κΈΈμ–΄μ§ˆ λ•Œ 핡심 μ»¨ν…μŠ€νŠΈ(파일, TODO, νƒ€μž„λΌμΈ)λ₯Ό λ³΄μ‘΄ν•˜λ©° 토큰을 μ ˆμ•½ν•©λ‹ˆλ‹€. """ import os import json import time import re from typing import List, Dict, Any, Optional class CompactionEngine: """λŒ€ν™” 내역을 λΆ„μ„ν•˜κ³  μ§€λŠ₯적으둜 μ••μΆ•ν•˜λŠ” 핡심 μ—”μ§„.""" PREAMBLE = "이 μ„Έμ…˜μ€ 이전 λŒ€ν™”μ—μ„œ μ»¨ν…μŠ€νŠΈκ°€ λΆ€μ‘±ν•˜μ—¬ μš”μ•½λ³ΈμœΌλ‘œλΆ€ν„° μ΄μ–΄μ§€λŠ” λ‚΄μš©μž…λ‹ˆλ‹€.\n\n" def __init__(self, project_root: str): self.project_root = project_root self.settings_file = os.path.join(project_root, "docker_data", "shared_workspace", "JarvisRun", "JARVIS_SHADOW_SETTINGS.json") def _get_settings(self) -> Dict[str, Any]: """μ„€μ • νŒŒμΌμ—μ„œ ν•«-λ‘œλ“œ(Hot-Reload)둜 섀정을 μ½μ–΄μ˜΅λ‹ˆλ‹€.""" defaults = { "compaction_enabled": True, "compaction_threshold": 50, # λ©”μ‹œμ§€ 개수 κΈ°μ€€ "preserve_recent": 5 # μ••μΆ• μ‹œ 보쑴할 μ΅œμ‹  λ©”μ‹œμ§€ 수 } if os.path.exists(self.settings_file): try: with open(self.settings_file, 'r', encoding='utf-8') as f: data = json.load(f) # merge with defaults for k, v in defaults.items(): if k not in data: data[k] = v return data except: pass return defaults def should_compact(self, messages: List[Dict[str, Any]]) -> bool: """압좕이 ν•„μš”ν•œ μ‹œμ μΈμ§€ νŒλ‹¨ν•©λ‹ˆλ‹€.""" settings = self._get_settings() if not settings.get("compaction_enabled", True): return False threshold = int(settings.get("compaction_threshold", 50)) return len(messages) >= threshold def extract_metadata(self, messages: List[Dict[str, Any]]) -> Dict[str, Any]: """λŒ€ν™” λ‚΄μ—­μ—μ„œ 핡심 메타데이터(파일, TODO, νƒ€μž„λΌμΈ, 감정)λ₯Ό μΆ”μΆœν•©λ‹ˆλ‹€.""" files = set() todos = [] timeline = [] emotions = {} # sender별 주둜 ν‘œν˜„λœ 감정 # 파일 ν™•μž₯자 μ •κ·œμ‹ file_pattern = re.compile(r'[\w/._-]+\.(?:py|rs|dart|js|ts|md|html|css|json|yaml|yml|sh|ps1|bat)') for msg in messages: content = msg.get('text', '') or '' role = msg.get('relayGuardian') or msg.get('sender_name') or msg.get('sender_id') or 'unknown' emotion = msg.get('emotion') # 1. 파일 경둜 μΆ”μΆœ found_files = file_pattern.findall(content) for f in found_files: if len(f) > 3: files.add(f) # 2. TODO/Next Step μΆ”λ‘  lower_content = content.lower() if any(kw in lower_content for kw in ['todo', 'λ‹€μŒ', 'next', 'ν•  일', 'pending', 'remain']): lines = [line.strip() for line in content.split('\n') if line.strip()] for line in lines: if any(kw in line.lower() for kw in ['todo', 'λ‹€μŒ', 'next', 'ν•  일']): todos.append(line[:150] + "..." if len(line) > 150 else line) # 3. 감정 좔적 if emotion: if role not in emotions: emotions[role] = [] emotions[role].append(emotion) # 4. νƒ€μž„λΌμΈ (핡심 이벀트 흐름 μš”μ•½) snippet = content.replace('\n', ' ').strip() if len(snippet) > 80: snippet = snippet[:77] + "..." emo_str = f" ({emotion})" if emotion else "" timeline.append(f"{role}{emo_str}: {snippet}") # κ°€μž₯ λΉˆλ²ˆν•œ 감정 μš”μ•½ summary_emotions = {} for role, emo_list in emotions.items(): if emo_list: from collections import Counter most_common = Counter(emo_list).most_common(1)[0][0] summary_emotions[role] = most_common return { "files": sorted(list(files))[:15], "todos": todos[-5:], "timeline": timeline[-20:], "emotions": summary_emotions, "count": len(messages) } def generate_summary_message(self, metadata: Dict[str, Any]) -> str: """μΆ”μΆœλœ 메타데이터λ₯Ό μ‚¬μš©ν•˜μ—¬ λ§ˆν¬λ‹€μš΄ μš”μ•½λ³Έμ„ μƒμ„±ν•©λ‹ˆλ‹€.""" summary = ["", "πŸ”± **System Context Summary (이전 λŒ€ν™” μš”μ•½)**"] summary.append(f"\n- **μ••μΆ• λ²”μœ„**: 이전 {metadata['count']}개의 λ©”μ‹œμ§€ 톡합") if metadata['emotions']: emo_lines = [f"{role}: {emo}" for role, emo in metadata['emotions'].items()] summary.append(f"- **μ£Όμš” 감정 μƒνƒœ**: {', '.join(emo_lines)}") if metadata['files']: summary.append(f"- **참쑰된 μ£Όμš” 파일**: {', '.join([f'`{f}`' for f in metadata['files']])}") if metadata['todos']: summary.append("- **남은 과제 및 TODO**:") for todo in metadata['todos']: summary.append(f" - {todo}") summary.append("- **μ£Όμš” νƒ€μž„λΌμΈ**:") for event in metadata['timeline']: summary.append(f" - {event}") summary.append("\n") return self.PREAMBLE + "\n".join(summary) def compact(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """λ©”μ‹œμ§€ 리슀트λ₯Ό μ†Œλ²„λ¦° ν•˜λ„€μŠ€ λ°©μ‹μœΌλ‘œ μ••μΆ•ν•©λ‹ˆλ‹€.""" if not self.should_compact(messages): return messages settings = self._get_settings() preserve_count = int(settings.get("preserve_recent", 5)) # μ••μΆ• λŒ€μƒκ³Ό 보쑴 λŒ€μƒ 뢄리 split_idx = len(messages) - preserve_count to_compact = messages[:split_idx] preserved = messages[split_idx:] # μš”μ•½ 생성 metadata = self.extract_metadata(to_compact) summary_text = self.generate_summary_message(metadata) # μ‹œμŠ€ν…œ μš”μ•½ λ©”μ‹œμ§€ 생성 summary_msg = { "text": summary_text, "type": 3, # πŸ”± Imperial Type 3: System Context Summary "relayGuardian": "System", "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ"), "sender_id": "97157d36-0b88-473f-8131-fab3e46aa560" # Shadow Brain ID } return [summary_msg] + preserved # Global Singleton for Engine import os core_root = os.path.dirname(os.path.abspath(__file__)) compaction_engine = CompactionEngine(core_root)