Spaces:
Sleeping
Sleeping
v4: Claude Code 4-layer memory (instructions + memory + session + compact)
Browse files- codepilot_v4.py +142 -42
codepilot_v4.py
CHANGED
|
@@ -84,50 +84,119 @@ class FeedbackDB:
|
|
| 84 |
|
| 85 |
|
| 86 |
# ============================================================
|
| 87 |
-
#
|
| 88 |
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
class ProjectContext:
|
| 90 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
def __init__(self, project_dir):
|
| 93 |
self.project_dir = project_dir
|
| 94 |
-
self.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
self.session_file = os.path.join(CONFIG_DIR, "sessions",
|
| 96 |
os.path.basename(project_dir) + ".json")
|
| 97 |
os.makedirs(os.path.dirname(self.session_file), exist_ok=True)
|
| 98 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
def load_memory(self):
|
| 100 |
-
"""
|
| 101 |
-
if
|
| 102 |
-
return
|
| 103 |
return ""
|
| 104 |
|
| 105 |
-
def
|
| 106 |
-
"""
|
| 107 |
-
|
|
|
|
| 108 |
|
| 109 |
def load_session(self):
|
| 110 |
-
"""載入上次對話"""
|
|
|
|
|
|
|
| 111 |
if os.path.exists(self.session_file):
|
| 112 |
try:
|
| 113 |
data = json.loads(Path(self.session_file).read_text())
|
| 114 |
-
# 只保留最近 20 輪對話(防止 context 爆掉)
|
| 115 |
msgs = data.get("messages", [])
|
| 116 |
-
if len(msgs) > 42:
|
| 117 |
-
msgs = [msgs[0]] + msgs[-40:]
|
| 118 |
return msgs
|
| 119 |
-
except:
|
| 120 |
-
pass
|
| 121 |
return None
|
| 122 |
|
| 123 |
def save_session(self, messages):
|
| 124 |
-
"""保存當前對話"""
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
Path(self.session_file).write_text(
|
| 129 |
-
json.dumps({"messages": messages, "timestamp": datetime.now().isoformat()},
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
|
| 133 |
# ============================================================
|
|
@@ -451,21 +520,25 @@ def run_agent_loop(args):
|
|
| 451 |
# Duel 模式開關
|
| 452 |
duel_mode = args.duel and local_model_ref and cloud_model_ref
|
| 453 |
|
| 454 |
-
# 專案記憶
|
| 455 |
-
|
|
|
|
| 456 |
|
| 457 |
# Banner
|
| 458 |
banner = f"[bold cyan]CodePilot v4[/]"
|
| 459 |
if duel_mode: banner += " [bold yellow]⚔️ Duel ON[/]"
|
| 460 |
banner += f"\n[dim]Model: {model.name}\nProject: {project_dir}[/]"
|
| 461 |
-
if
|
|
|
|
|
|
|
| 462 |
console.print(Panel.fit(banner, border_style="cyan"))
|
| 463 |
|
| 464 |
git_ctx = tools.git_context()
|
| 465 |
if git_ctx != "(not a git repo)": console.print(Panel(git_ctx, title="📂 Project", border_style="dim"))
|
| 466 |
|
| 467 |
# 嘗試恢復上次對話
|
| 468 |
-
|
|
|
|
| 469 |
prev_session = ctx.load_session()
|
| 470 |
if prev_session and len(prev_session) > 1:
|
| 471 |
messages = prev_session
|
|
@@ -496,23 +569,46 @@ def run_agent_loop(args):
|
|
| 496 |
elif cmd == "/duel off":
|
| 497 |
duel_mode = False; console.print("[dim]Duel 模式已關閉[/]"); continue
|
| 498 |
|
| 499 |
-
elif cmd == "/memo":
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
if
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
|
| 509 |
-
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
| 515 |
-
console.print(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 516 |
continue
|
| 517 |
|
| 518 |
elif cmd == "/grind":
|
|
@@ -648,6 +744,10 @@ def run_agent_loop(args):
|
|
| 648 |
console.print(" [yellow]✏️[/]")
|
| 649 |
|
| 650 |
messages.append({"role": "assistant", "content": full_response})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 651 |
ctx.save_session(messages)
|
| 652 |
|
| 653 |
console.print("\n[cyan]👋[/]")
|
|
|
|
| 84 |
|
| 85 |
|
| 86 |
# ============================================================
|
| 87 |
+
# MEMORY SYSTEM — Claude Code 風格四層記憶
|
| 88 |
# ============================================================
|
| 89 |
+
# 匯入 memory.py 模組(如果存在),否則使用內建簡化版
|
| 90 |
+
try:
|
| 91 |
+
from memory import (
|
| 92 |
+
load_instructions, load_memory, save_memory, append_memory,
|
| 93 |
+
build_full_system_prompt, SessionTranscript, FileStateCache,
|
| 94 |
+
should_compact, compact_messages, estimate_tokens
|
| 95 |
+
)
|
| 96 |
+
MEMORY_MODULE_AVAILABLE = True
|
| 97 |
+
except ImportError:
|
| 98 |
+
MEMORY_MODULE_AVAILABLE = False
|
| 99 |
+
|
| 100 |
class ProjectContext:
|
| 101 |
+
"""
|
| 102 |
+
四層記憶:
|
| 103 |
+
L1: CODEPILOT.md 指令(遞迴搜尋 CWD 到根目錄)
|
| 104 |
+
L2: MEMORY.md 跨 session 記憶
|
| 105 |
+
L3: Session transcript (JSONL)
|
| 106 |
+
L4: 自動壓縮(context window 管理)
|
| 107 |
+
"""
|
| 108 |
|
| 109 |
def __init__(self, project_dir):
|
| 110 |
self.project_dir = project_dir
|
| 111 |
+
self.cwd = project_dir
|
| 112 |
+
|
| 113 |
+
if MEMORY_MODULE_AVAILABLE:
|
| 114 |
+
# 用完整 memory.py 模組
|
| 115 |
+
self.transcript = SessionTranscript.find_latest(project_dir)
|
| 116 |
+
self.file_cache = FileStateCache()
|
| 117 |
+
else:
|
| 118 |
+
self.transcript = None
|
| 119 |
+
self.file_cache = None
|
| 120 |
+
|
| 121 |
+
# Session 文件(簡化版 fallback)
|
| 122 |
self.session_file = os.path.join(CONFIG_DIR, "sessions",
|
| 123 |
os.path.basename(project_dir) + ".json")
|
| 124 |
os.makedirs(os.path.dirname(self.session_file), exist_ok=True)
|
| 125 |
|
| 126 |
+
def load_all_instructions(self):
|
| 127 |
+
"""L1: 載入所有 CODEPILOT.md 指令"""
|
| 128 |
+
if MEMORY_MODULE_AVAILABLE:
|
| 129 |
+
return load_instructions(self.cwd)
|
| 130 |
+
# Fallback: 只讀當前目錄的
|
| 131 |
+
f = os.path.join(self.project_dir, "CODEPILOT.md")
|
| 132 |
+
return Path(f).read_text(encoding="utf-8") if os.path.exists(f) else ""
|
| 133 |
+
|
| 134 |
def load_memory(self):
|
| 135 |
+
"""L2: 載入跨 session 記憶"""
|
| 136 |
+
if MEMORY_MODULE_AVAILABLE:
|
| 137 |
+
return load_memory(self.cwd)
|
| 138 |
return ""
|
| 139 |
|
| 140 |
+
def save_memory_entry(self, entry):
|
| 141 |
+
"""L2: 追加一條記憶"""
|
| 142 |
+
if MEMORY_MODULE_AVAILABLE:
|
| 143 |
+
append_memory(self.cwd, entry)
|
| 144 |
|
| 145 |
def load_session(self):
|
| 146 |
+
"""L3: 載入上次對話"""
|
| 147 |
+
if MEMORY_MODULE_AVAILABLE and self.transcript:
|
| 148 |
+
return self.transcript.load_messages()
|
| 149 |
if os.path.exists(self.session_file):
|
| 150 |
try:
|
| 151 |
data = json.loads(Path(self.session_file).read_text())
|
|
|
|
| 152 |
msgs = data.get("messages", [])
|
| 153 |
+
if len(msgs) > 42: msgs = [msgs[0]] + msgs[-40:]
|
|
|
|
| 154 |
return msgs
|
| 155 |
+
except: pass
|
|
|
|
| 156 |
return None
|
| 157 |
|
| 158 |
def save_session(self, messages):
|
| 159 |
+
"""L3: 保存當前對話"""
|
| 160 |
+
if MEMORY_MODULE_AVAILABLE:
|
| 161 |
+
if not self.transcript:
|
| 162 |
+
self.transcript = SessionTranscript(self.cwd)
|
| 163 |
+
# 追加最新訊息到 JSONL
|
| 164 |
+
if messages:
|
| 165 |
+
last = messages[-1]
|
| 166 |
+
self.transcript.append(last.get("role", "user"), last)
|
| 167 |
+
# 也保存簡化版
|
| 168 |
+
if len(messages) > 42: messages = [messages[0]] + messages[-40:]
|
| 169 |
Path(self.session_file).write_text(
|
| 170 |
+
json.dumps({"messages": messages, "timestamp": datetime.now().isoformat()}, ensure_ascii=False))
|
| 171 |
+
|
| 172 |
+
def check_compact(self, messages, model_chat_fn=None):
|
| 173 |
+
"""L4: 檢查是否需要壓縮,自動執行"""
|
| 174 |
+
if not MEMORY_MODULE_AVAILABLE:
|
| 175 |
+
# Fallback: 簡單截斷
|
| 176 |
+
if len(messages) > 42:
|
| 177 |
+
return [messages[0]] + messages[-40:]
|
| 178 |
+
return messages
|
| 179 |
+
|
| 180 |
+
if should_compact(messages):
|
| 181 |
+
edited_files = self.file_cache.get_recently_edited() if self.file_cache else []
|
| 182 |
+
if model_chat_fn:
|
| 183 |
+
return compact_messages(messages, model_chat_fn, edited_files)
|
| 184 |
+
else:
|
| 185 |
+
return [messages[0]] + messages[-30:]
|
| 186 |
+
return messages
|
| 187 |
+
|
| 188 |
+
def build_system_prompt(self, git_context=""):
|
| 189 |
+
"""組裝完整 system prompt"""
|
| 190 |
+
if MEMORY_MODULE_AVAILABLE:
|
| 191 |
+
return build_full_system_prompt(self.cwd, git_context)
|
| 192 |
+
# Fallback
|
| 193 |
+
memory = self.load_all_instructions()
|
| 194 |
+
mem = self.load_memory()
|
| 195 |
+
parts = ["You are CodePilot, an expert AI programming assistant."]
|
| 196 |
+
if memory: parts.append(memory)
|
| 197 |
+
if mem: parts.append(f"## Memory\n{mem}")
|
| 198 |
+
parts.append(f"Working directory: {self.cwd}\n{git_context}")
|
| 199 |
+
return "\n\n".join(parts)
|
| 200 |
|
| 201 |
|
| 202 |
# ============================================================
|
|
|
|
| 520 |
# Duel 模式開關
|
| 521 |
duel_mode = args.duel and local_model_ref and cloud_model_ref
|
| 522 |
|
| 523 |
+
# 專案記憶(四層)
|
| 524 |
+
instructions = ctx.load_all_instructions()
|
| 525 |
+
memory = ctx.load_memory()
|
| 526 |
|
| 527 |
# Banner
|
| 528 |
banner = f"[bold cyan]CodePilot v4[/]"
|
| 529 |
if duel_mode: banner += " [bold yellow]⚔️ Duel ON[/]"
|
| 530 |
banner += f"\n[dim]Model: {model.name}\nProject: {project_dir}[/]"
|
| 531 |
+
if instructions: banner += f"\n[dim]📋 CODEPILOT.md loaded[/]"
|
| 532 |
+
if memory: banner += f"\n[dim]🧠 MEMORY.md loaded ({len(memory)} chars)[/]"
|
| 533 |
+
if MEMORY_MODULE_AVAILABLE: banner += f"\n[dim]💾 Session JSONL + Auto-compact enabled[/]"
|
| 534 |
console.print(Panel.fit(banner, border_style="cyan"))
|
| 535 |
|
| 536 |
git_ctx = tools.git_context()
|
| 537 |
if git_ctx != "(not a git repo)": console.print(Panel(git_ctx, title="📂 Project", border_style="dim"))
|
| 538 |
|
| 539 |
# 嘗試恢復上次對話
|
| 540 |
+
git_ctx = tools.git_context()
|
| 541 |
+
system_prompt = ctx.build_system_prompt(git_ctx)
|
| 542 |
prev_session = ctx.load_session()
|
| 543 |
if prev_session and len(prev_session) > 1:
|
| 544 |
messages = prev_session
|
|
|
|
| 569 |
elif cmd == "/duel off":
|
| 570 |
duel_mode = False; console.print("[dim]Duel 模式已關閉[/]"); continue
|
| 571 |
|
| 572 |
+
elif cmd == "/memo" or cmd.startswith("/memo "):
|
| 573 |
+
# /memo → 編輯 CODEPILOT.md 指令
|
| 574 |
+
# /memo + 文字 → 快速追加到 MEMORY.md
|
| 575 |
+
quick_note = cmd[5:].strip() if cmd.startswith("/memo ") else ""
|
| 576 |
+
if quick_note:
|
| 577 |
+
ctx.save_memory_entry(quick_note)
|
| 578 |
+
console.print(f"[green]🧠 已追加到 MEMORY.md: {quick_note}[/]")
|
| 579 |
+
else:
|
| 580 |
+
console.print(f"[bold]📋 CODEPILOT.md[/] — 專案指令(提交到 repo)")
|
| 581 |
+
console.print(f"[bold]🧠 MEMORY.md[/] — 自動記憶(跨 session)\n")
|
| 582 |
+
console.print("[dim]快速追加: /memo 這是一條記憶[/]")
|
| 583 |
+
console.print("[dim]編輯指令: 輸入內容(END 結束)[/]")
|
| 584 |
+
cur = ctx.load_all_instructions()
|
| 585 |
+
if cur: console.print(f"[dim]目前 CODEPILOT.md:\n{cur[:300]}...[/]\n")
|
| 586 |
+
cur_mem = ctx.load_memory()
|
| 587 |
+
if cur_mem: console.print(f"[dim]目前 MEMORY.md:\n{cur_mem[:300]}...[/]\n")
|
| 588 |
+
console.print("選擇: [cyan]1[/]=編輯 CODEPILOT.md [cyan]2[/]=編輯 MEMORY.md Enter=取消")
|
| 589 |
+
choice = Prompt.ask(" ", choices=["1","2",""], default="", show_choices=False)
|
| 590 |
+
if choice in ("1", "2"):
|
| 591 |
+
console.print("輸入內容(END 結束):")
|
| 592 |
+
edit_lines = []
|
| 593 |
+
while True:
|
| 594 |
+
try:
|
| 595 |
+
l = input()
|
| 596 |
+
if l.strip() == "END": break
|
| 597 |
+
edit_lines.append(l)
|
| 598 |
+
except EOFError: break
|
| 599 |
+
if edit_lines:
|
| 600 |
+
content = "\n".join(edit_lines)
|
| 601 |
+
if choice == "1":
|
| 602 |
+
codepilot_md = os.path.join(project_dir, "CODEPILOT.md")
|
| 603 |
+
Path(codepilot_md).write_text(content, encoding="utf-8")
|
| 604 |
+
console.print(f"[green]✅ CODEPILOT.md 已保存[/]")
|
| 605 |
+
else:
|
| 606 |
+
if MEMORY_MODULE_AVAILABLE:
|
| 607 |
+
save_memory(project_dir, content)
|
| 608 |
+
console.print(f"[green]✅ MEMORY.md 已保存[/]")
|
| 609 |
+
# 重建 system prompt
|
| 610 |
+
system_prompt = ctx.build_system_prompt(tools.git_context())
|
| 611 |
+
messages[0] = {"role": "system", "content": system_prompt}
|
| 612 |
continue
|
| 613 |
|
| 614 |
elif cmd == "/grind":
|
|
|
|
| 744 |
console.print(" [yellow]✏️[/]")
|
| 745 |
|
| 746 |
messages.append({"role": "assistant", "content": full_response})
|
| 747 |
+
|
| 748 |
+
# L4: 自動壓縮檢查
|
| 749 |
+
messages = ctx.check_compact(messages, model_chat_fn=model.chat if hasattr(model, 'chat') else None)
|
| 750 |
+
|
| 751 |
ctx.save_session(messages)
|
| 752 |
|
| 753 |
console.print("\n[cyan]👋[/]")
|