Justin-lee commited on
Commit
f395db4
·
verified ·
1 Parent(s): 1b9138e

v4: Claude Code 4-layer memory (instructions + memory + session + compact)

Browse files
Files changed (1) hide show
  1. codepilot_v4.py +142 -42
codepilot_v4.py CHANGED
@@ -84,50 +84,119 @@ class FeedbackDB:
84
 
85
 
86
  # ============================================================
87
- # PROJECT CONTEXT — 記憶系統
88
  # ============================================================
 
 
 
 
 
 
 
 
 
 
 
89
  class ProjectContext:
90
- """專案上下文記憶"""
 
 
 
 
 
 
91
 
92
  def __init__(self, project_dir):
93
  self.project_dir = project_dir
94
- self.memory_file = os.path.join(project_dir, "CODEPILOT.md")
 
 
 
 
 
 
 
 
 
 
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
- """讀取 CODEPILOT.md 專案記憶"""
101
- if os.path.exists(self.memory_file):
102
- return Path(self.memory_file).read_text(encoding="utf-8")
103
  return ""
104
 
105
- def save_memory(self, content):
106
- """保存專案記憶"""
107
- Path(self.memory_file).write_text(content, encoding="utf-8")
 
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: # system + 20 rounds * 2 + buffer
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
- # 只保最近 20 輪
126
- if len(messages) > 42:
127
- messages = [messages[0]] + messages[-40:]
 
 
 
 
 
 
128
  Path(self.session_file).write_text(
129
- json.dumps({"messages": messages, "timestamp": datetime.now().isoformat()},
130
- ensure_ascii=False))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- project_memory = ctx.load_memory()
 
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 project_memory: banner += f"\n[dim]📝 CODEPILOT.md loaded ({len(project_memory)} chars)[/]"
 
 
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
- system_prompt = build_system_prompt(tools, project_memory)
 
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
- console.print(f"[bold]📝 CODEPILOT.md[/]")
501
- console.print("[dim]輸入專案筆記(END 結束),會注入每次對話的 system prompt:[/]")
502
- lines = []
503
- if project_memory: console.print(f"[dim]目前內容:\n{project_memory[:500]}[/]\n")
504
- while True:
505
- try:
506
- l = input()
507
- if l.strip() == "END": break
508
- lines.append(l)
509
- except EOFError: break
510
- if lines:
511
- project_memory = "\n".join(lines)
512
- ctx.save_memory(project_memory)
513
- system_prompt = build_system_prompt(tools, project_memory)
514
- messages[0] = {"role": "system", "content": system_prompt}
515
- console.print(f"[green] 已保存 CODEPILOT.md ({len(project_memory)} chars)[/]")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 SYSTEMClaude 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]👋[/]")