chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
# s09: Memory — Compression Loses Details, Keep a Layer That Doesn't
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s07 → s08 → `s09` → [s10](../s10_system_prompt/) → s11 → ... → s20
|
||||
> *"Compression loses details, keep a layer that doesn't"* — File store + index + on-demand loading, across compactions, across sessions.
|
||||
>
|
||||
> **Harness Layer**: Memory — knowledge that survives compaction and sessions.
|
||||
|
||||
---
|
||||
|
||||
## The Problem
|
||||
|
||||
s08's autoCompact preserves current goals, remaining work, and user constraints in the summary, but details get lost: "use tabs not spaces" might get simplified to "user has code style preferences". And when you start a new session, even the summary is gone.
|
||||
|
||||
LLMs have no persistent state; all information lives in the context window. When context fills up, it gets compressed, and compression is lossy. What's needed is a storage layer that doesn't participate in compression and persists across sessions.
|
||||
|
||||
---
|
||||
|
||||
## The Solution
|
||||
|
||||

|
||||
|
||||
The s08 compression pipeline is preserved, focusing on memory. Storage uses the filesystem: a `.memory/` directory where each memory is a `.md` file with YAML frontmatter (`name` / `description` / `type`). When files accumulate, an index is needed: `MEMORY.md` holds one link per line and gets injected into the SYSTEM.
|
||||
|
||||
Key design: the index stays in SYSTEM prompt (cacheable by prompt cache), file content is injected on demand (matched by filename/description to the current conversation, without breaking the cache). Writing has two paths: the user explicitly says "remember", or extraction runs in the background after each turn. When files accumulate, periodic consolidation deduplicates.
|
||||
|
||||
Four memory types, each answering a different question:
|
||||
|
||||
| Type | Answers | Example |
|
||||
|------|---------|---------|
|
||||
| user | Who you are | "Use tabs not spaces" |
|
||||
| feedback | How to work | "Don't mock the database" |
|
||||
| project | What's happening | "Auth rewrite is compliance-driven" |
|
||||
| reference | Where to find things | "Pipeline bugs are in Linear INGEST" |
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||

|
||||
|
||||
### Storage: Markdown Files + Index
|
||||
|
||||
Each memory is a `.md` file with YAML frontmatter for metadata:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: user-preference-tabs
|
||||
description: User prefers tabs for indentation
|
||||
type: user
|
||||
---
|
||||
|
||||
User prefers using tabs, not spaces, for indentation.
|
||||
**Why:** Consistency with existing codebase conventions.
|
||||
**How to apply:** Always use tabs when writing or editing files.
|
||||
```
|
||||
|
||||
`MEMORY.md` is the index, one link per line:
|
||||
|
||||
```markdown
|
||||
- [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation
|
||||
```
|
||||
|
||||
Writing a new memory automatically rebuilds the index:
|
||||
|
||||
```python
|
||||
def write_memory_file(name, mem_type, description, body):
|
||||
slug = name.lower().replace(" ", "-")
|
||||
filepath = MEMORY_DIR / f"{slug}.md"
|
||||
filepath.write_text(
|
||||
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
|
||||
)
|
||||
_rebuild_index()
|
||||
```
|
||||
|
||||
### Loading: Two Paths
|
||||
|
||||
**Path 1: Index in SYSTEM.** `build_system()` reads `MEMORY.md` once at the start of each user request and injects the memory catalog into the SYSTEM prompt. Memory extraction and consolidation run only when the turn ends, so SYSTEM does not need to be rebuilt repeatedly within the same user request.
|
||||
|
||||
**Path 2: Relevant memories on demand.** At the start of each user request, `load_memories()` sends the recent conversation and the memory catalog (name + description) to the LLM as a lightweight side-query, selects relevant filenames, then reads and injects their contents. Capped at 5 to control cost.
|
||||
|
||||
```python
|
||||
def select_relevant_memories(messages, max_items=5):
|
||||
files = list_memory_files()
|
||||
if not files:
|
||||
return []
|
||||
|
||||
# Build catalog: "0: user-preference-tabs — User prefers tabs..."
|
||||
catalog = "\n".join(f"{i}: {f['name']} — {f['description']}" for i, f in enumerate(files))
|
||||
|
||||
response = client.messages.create(model=MODEL, messages=[{"role": "user",
|
||||
"content": f"Select relevant memory indices. Return JSON array.\n\n"
|
||||
f"Recent conversation:\n{recent}\n\nMemory catalog:\n{catalog}"}],
|
||||
max_tokens=200)
|
||||
indices = json.loads(re.search(r'\[.*?\]', response.content[0].text).group())
|
||||
return [files[i]["filename"] for i in indices if 0 <= i < len(files)]
|
||||
```
|
||||
|
||||
If the side-query fails (API error, JSON parse failure), it falls back to keyword matching on name + description.
|
||||
|
||||
### Writing: Extraction After Each Turn
|
||||
|
||||
Users don't always say "remember this". Preferences are usually scattered across normal dialogue: "tabs are better than spaces", "let's use single quotes from now on".
|
||||
|
||||
`extract_memories()` runs when each turn ends, triggered when the model stops without a tool_use (indicating the conversation has reached a natural break):
|
||||
|
||||
```python
|
||||
# In agent_loop:
|
||||
if response.stop_reason != "tool_use":
|
||||
extract_memories(messages) # Extract new memories from recent dialogue
|
||||
consolidate_memories() # Check if consolidation is needed
|
||||
return
|
||||
```
|
||||
|
||||
Before extraction, existing memories are checked to avoid duplicates. The extraction prompt asks the LLM to return a JSON array of `{name, type, description, body}`, writing files only when genuinely new information is found.
|
||||
|
||||
```python
|
||||
def extract_memories(messages):
|
||||
dialogue = format_recent_messages(messages[-10:])
|
||||
existing = "\n".join(f"- {m['name']}: {m['description']}" for m in list_memory_files())
|
||||
|
||||
prompt = (
|
||||
"Extract user preferences, constraints, or project facts.\n"
|
||||
"Return JSON array: [{name, type, description, body}].\n"
|
||||
"If nothing new or already covered, return [].\n\n"
|
||||
f"Existing memories:\n{existing}\n\nDialogue:\n{dialogue[:4000]}"
|
||||
)
|
||||
# ... parse response, write files ...
|
||||
```
|
||||
|
||||
### Consolidation: Low-Frequency Deduplication
|
||||
|
||||
Memory files accumulate. `consolidate_memories()` triggers when the file count reaches a threshold (default 10), asking the LLM to deduplicate, merge contradictions, and prune stale memories:
|
||||
|
||||
```python
|
||||
CONSOLIDATE_THRESHOLD = 10
|
||||
|
||||
def consolidate_memories():
|
||||
files = list_memory_files()
|
||||
if len(files) < CONSOLIDATE_THRESHOLD:
|
||||
return # Too few, not worth consolidating
|
||||
# Send all memories to LLM, get back deduplicated list
|
||||
# Replace all files with consolidated results
|
||||
```
|
||||
|
||||
CC calls this process **Dream**, with four gates in practice: time interval, scan throttle, session count, file lock. The teaching version simplifies to a file-count threshold.
|
||||
|
||||
### What Memory Stores
|
||||
|
||||
Memory stores information that remains useful across sessions: user preferences, recurring feedback, project background, common entry points, and investigation clues. It focuses on "what will be useful later" and brings that information back through an index plus on-demand loading.
|
||||
|
||||
Session memory focuses on continuity inside one session: what context should survive after compaction. The two work together: Memory handles long-term knowledge; session memory handles the current session across compaction.
|
||||
|
||||
---
|
||||
|
||||
## Changes From s08
|
||||
|
||||
| Component | Before (s08) | After (s09) |
|
||||
|-----------|-------------|-------------|
|
||||
| Memory capability | None (preferences degrade with compaction) | Storage + loading + extraction + consolidation |
|
||||
| New functions | — | write_memory_file, select_relevant_memories, load_memories, extract_memories, consolidate_memories |
|
||||
| Storage | — | .memory/MEMORY.md index + .memory/*.md files |
|
||||
| Tools | bash, read, write, edit, glob, todo_write, task, load_skill, compact (9) | bash, read_file, write_file, edit_file, glob, task (6) |
|
||||
| Loop | Only compression each turn | Memory injection + compression + post-turn extraction + periodic consolidation |
|
||||
|
||||
---
|
||||
|
||||
## Try It
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s09_memory/code.py
|
||||
```
|
||||
|
||||
Try these prompts (enter across multiple turns, observe memory accumulation and loading):
|
||||
|
||||
1. `I prefer using tabs for indentation, not spaces. Remember that.`
|
||||
2. `Create a Python file called test.py` (observe whether the Agent uses tabs)
|
||||
3. `What did I tell you about my preferences?` (observe whether the Agent remembers)
|
||||
4. `I also prefer single quotes over double quotes for strings.`
|
||||
|
||||
What to watch for: Does `[Memory: extracted N new memories]` appear after each turn? Are `.md` files generated in `.memory/`? Is `MEMORY.md` index updated? Does the Agent automatically load previous memories in new conversations?
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
Memory, compression, and tools are all in place. But the system prompt is still a hardcoded string. Adding a new tool means manually adding a description; switching projects means rewriting the whole prompt. Prompts should be assembled at runtime.
|
||||
|
||||
s10 System Prompt → segments + runtime assembly. Different projects, different tools, different prompts.
|
||||
|
||||
<details>
|
||||
<summary>Deep Dive Into CC Source Code</summary>
|
||||
|
||||
> The following is based on analysis of CC source code under `src/` in `memdir/`, `services/`, `utils/`, `query/`. Line numbers verified against source.
|
||||
|
||||
### Source Code Paths
|
||||
|
||||
| File | Lines | Responsibility |
|
||||
|------|-------|---------------|
|
||||
| `memdir/memdir.ts` | 507 | Core: MEMORY.md definition (`34-38`), memory behavior instructions distinguishing memory/plan/tasks (`199-266`), `loadMemoryPrompt()` three paths (`419-490`) |
|
||||
| `memdir/findRelevantMemories.ts` | 141 | Sonnet side-query memory selection (`18-24` system prompt, `97-122` call logic) |
|
||||
| `memdir/memoryTypes.ts` | 271 | Type definitions, frontmatter fields |
|
||||
| `memdir/memoryScan.ts` | — | Scan .md files, exclude MEMORY.md, read frontmatter, max 200 files, sorted by mtime desc (`35-94`) |
|
||||
| `services/extractMemories/extractMemories.ts` | 615 | Forked agent extraction, restricted permissions, `skipTranscript: true`, `maxTurns: 5` (`371-427`) |
|
||||
| `services/autoDream/autoDream.ts` | 324 | Dream consolidation, four-layer gating (`63-66` defaults, `130-190` gating, `224-233` forked agent) |
|
||||
| `services/SessionMemory/sessionMemory.ts` | 495 | Session-level memory management |
|
||||
| `services/compact/sessionMemoryCompact.ts` | — | Session memory lightweight summary, thresholds 10K/5/40K (`56-61`) |
|
||||
| `utils/attachments.ts` | — | Injection budget: 200 lines / 4096 bytes per file, 60KB per session (`269-288`); find relevant memory by query (`2196-2241`) |
|
||||
| `query.ts` | — | Memory prefetch at start of each user turn (`301-304`), non-blocking collection (`1592-1614`) |
|
||||
| `query/stopHooks.ts` | — | Stop hook fire-and-forget triggers extraction and Dream (`141-155`) |
|
||||
|
||||
### Memory Selection: LLM, Not Embedding
|
||||
|
||||
CC uses **Sonnet itself to select** (`findRelevantMemories.ts`), not embedding vector similarity:
|
||||
|
||||
1. `memoryScan.ts` scans all `.md` files in `.memory/` (excluding MEMORY.md), max 200 files, sorted by mtime descending
|
||||
2. Lists all memory files' `name` + `description` as a catalog
|
||||
3. Sends to Sonnet side-query: "Select truly useful memories by name and description (max 5). Skip if unsure."
|
||||
4. Sonnet returns `{ selected_memories: ["file1.md", ...] }`
|
||||
5. Selected files' full contents are read (≤ 200 lines / 4096 bytes per file) and injected. Total session budget: 60KB
|
||||
|
||||
At the start of each user turn, `query.ts:301-304` starts memory prefetch (async); after tool execution, `1592-1614` collects completed results non-blocking.
|
||||
|
||||
### Extraction Timing: Stop Hook, Not After autoCompact
|
||||
|
||||
Trigger location (`stopHooks.ts:141-155`): inside `handleStopHooks()`, fire-and-forget triggers extraction and Dream. The teaching version places extraction in the `stop_reason != "tool_use"` branch, matching the direction.
|
||||
|
||||
CC's extraction runs via forked agent (`extractMemories.ts:371-427`): restricted permissions, `skipTranscript: true`, `maxTurns: 5`. Also has overlap protection: if the main Agent already wrote memory files, extraction is skipped.
|
||||
|
||||
### Memory File Format
|
||||
|
||||
CC uses Markdown + YAML frontmatter, consistent with the teaching version. Four types: `user`, `feedback`, `project`, `reference`.
|
||||
|
||||
`memdir.ts:34-38` defines index constraints: `MEMORY.md` max 200 lines / 25KB. `memdir.ts:199-266` builds memory behavior instructions, explicitly distinguishing memory from plan and tasks. Storage location: `~/.claude/projects/<sanitized-git-root>/memory/`.
|
||||
|
||||
### Dream: Four-Layer Gating
|
||||
|
||||
Not "triggered when idle" or "consolidate when count is enough", but four gates (`autoDream.ts`, defaults `63-66`, gating logic `130-190`):
|
||||
|
||||
1. **Time gate**: ≥ 24 hours since last consolidation
|
||||
2. **Scan throttle**: Avoid frequent filesystem scans
|
||||
3. **Session gate**: ≥ 5 session transcripts modified since last consolidation
|
||||
4. **Lock gate**: No other process currently consolidating (`.consolidate-lock` file)
|
||||
|
||||
The merge itself runs via forked agent (`224-233`): locate → collect recent signals → merge and write files → prune and update index. Lock file mtime serves as lastConsolidatedAt. Crash recovery: lock auto-expires after 1 hour.
|
||||
|
||||
### User Memory vs Session Memory
|
||||
|
||||
| | User Memory | Session Memory |
|
||||
|---|---|---|
|
||||
| Persistence | Cross-session | Single session |
|
||||
| Storage | Multiple .md files in `memory/` | `session-memory/<id>/memory.md` |
|
||||
| Loaded into | system prompt | compact summary |
|
||||
| Purpose | Cross-session knowledge accumulation | Cross-compact context continuity |
|
||||
|
||||
sessionMemoryCompact (mentioned in s08) uses Session Memory: before autoCompact, it reads the session memory file and, if sufficient (≥ 10K tokens, ≥ 5 text messages, ≤ 40K tokens, `sessionMemoryCompact.ts:56-61`), uses it as a summary without calling the LLM.
|
||||
|
||||
### Where the Real Implementation Is More Complex
|
||||
|
||||
- **Feature flags**: Memory features have multiple feature gate layers
|
||||
- **Team memory**: Shared team memories, `loadMemoryPrompt()` has a dedicated path (not covered in teaching version)
|
||||
- **KAIROS**: Timing-aware memory extraction strategy, daily-log mode in `loadMemoryPrompt()`
|
||||
- **Prompt cache**: Memory injection must account for prompt cache TTL, avoiding full system prompt rewrites each turn
|
||||
- **File locks**: Concurrency control for multi-process scenarios
|
||||
- **Memory prefetch**: Async prefetch, non-blocking main flow
|
||||
|
||||
### Teaching Version Simplifications Are Intentional
|
||||
|
||||
- LLM side-query → LLM side-query + keyword fallback: teaching version keeps LLM selection, adds fallback path
|
||||
- Memory JSON → Markdown + frontmatter: teaching version matches CC
|
||||
- Stop hook trigger → `stop_reason != "tool_use"` branch: same direction
|
||||
- Four-layer gating → file-count threshold: teaching version lacks transcript system and multi-session concepts
|
||||
- Forked agent + restricted permissions → direct call: teaching version has no subprocess isolation
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
@@ -0,0 +1,279 @@
|
||||
# s09: Memory — 圧縮は詳細を失う、失わない層が必要
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s07 → s08 → `s09` → [s10](../s10_system_prompt/) → s11 → ... → s20
|
||||
> *"圧縮は詳細を失う、失わない層が必要"* — ファイルストア + インデックス + オンデマンド読み込み。圧縮を越え、セッションを越えて。
|
||||
>
|
||||
> **Harness レイヤー**: 記憶 — 圧縮とセッションを越える知識の蓄積。
|
||||
|
||||
---
|
||||
|
||||
## 課題
|
||||
|
||||
s08 の autoCompact は現在の目標、残りの作業、ユーザーの制約をサマリに保持するが、詳細は失われる:「タブでインデント、スペース不可」が「ユーザーにコードスタイルの好みあり」と簡略化される。そして新しいセッションを開始すると、サマリすらない。
|
||||
|
||||
LLM には永続状態がなく、すべての情報はコンテキストウィンドウ内にある。コンテキストが満杯になれば圧縮され、圧縮は非可逆。圧縮に参加せず、セッションを越えて保持されるストレージ層が必要。
|
||||
|
||||
---
|
||||
|
||||
## ソリューション
|
||||
|
||||

|
||||
|
||||
s08 の圧縮パイプラインを維持し、記憶に焦点を当てる。ストレージにはファイルシステムを採用:`.memory/` ディレクトリに各記憶を `.md` ファイルとして保存、YAML frontmatter(`name` / `description` / `type`)付き。ファイルが増えたらインデックスが必要:`MEMORY.md` に 1 行 1 リンクを記録し、SYSTEM に注入。
|
||||
|
||||
重要な設計:インデックスは SYSTEM prompt に常駐(prompt cache でキャッシュ可能)、ファイル内容はオンデマンド注入(filename/description で現在の会話にマッチ、cache を破壊しない)。書き込みは 2 つのパス:ユーザーが明示的に「覚えて」と言うか、毎ターン終了後にバックグラウンドで抽出。ファイルが蓄積されたら、定期的に整理して重複排除。
|
||||
|
||||
4 種類の記憶、それぞれ異なる質問に答える:
|
||||
|
||||
| タイプ | 何に答えるか | 例 |
|
||||
|--------|-------------|-----|
|
||||
| user | あなたは誰か | "タブでスペース不可" |
|
||||
| feedback | どう作業するか | "DB をモックしない" |
|
||||
| project | 何が起きているか | "auth 書き直しはコンプライアンス主導" |
|
||||
| reference | どこで探すか | "パイプラインのバグは Linear INGEST" |
|
||||
|
||||
---
|
||||
|
||||
## 仕組み
|
||||
|
||||

|
||||
|
||||
### ストレージ:Markdown ファイル + インデックス
|
||||
|
||||
各記憶は `.md` ファイル、YAML frontmatter でメタデータを記録:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: user-preference-tabs
|
||||
description: User prefers tabs for indentation
|
||||
type: user
|
||||
---
|
||||
|
||||
User prefers using tabs, not spaces, for indentation.
|
||||
**Why:** Consistency with existing codebase conventions.
|
||||
**How to apply:** Always use tabs when writing or editing files.
|
||||
```
|
||||
|
||||
`MEMORY.md` はインデックス、1 行に 1 リンク:
|
||||
|
||||
```markdown
|
||||
- [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation
|
||||
```
|
||||
|
||||
新しい記憶を書き込むとインデックスを自動再構築:
|
||||
|
||||
```python
|
||||
def write_memory_file(name, mem_type, description, body):
|
||||
slug = name.lower().replace(" ", "-")
|
||||
filepath = MEMORY_DIR / f"{slug}.md"
|
||||
filepath.write_text(
|
||||
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
|
||||
)
|
||||
_rebuild_index()
|
||||
```
|
||||
|
||||
### 読み込み:2 つのパス
|
||||
|
||||
**パス 1:インデックスを SYSTEM に常駐。** `build_system()` は各ユーザーリクエストの開始時に 1 回だけ `MEMORY.md` を読み込み、記憶カタログを SYSTEM prompt に注入。記憶の抽出と整理はターン終了時にだけ実行されるため、同じユーザーリクエスト内で SYSTEM を繰り返し再構築する必要はない。
|
||||
|
||||
**パス 2:関連記憶をオンデマンド注入。** 各ユーザーリクエストの開始時に、`load_memories()` は最近の会話と記憶カタログ(name + description)を LLM に軽量 side-query として送信し、関連するファイル名を選択、ファイル内容を読み込んで注入。上限 5 件でコストを制御。
|
||||
|
||||
```python
|
||||
def select_relevant_memories(messages, max_items=5):
|
||||
files = list_memory_files()
|
||||
if not files:
|
||||
return []
|
||||
|
||||
# Build catalog: "0: user-preference-tabs — User prefers tabs..."
|
||||
catalog = "\n".join(f"{i}: {f['name']} — {f['description']}" for i, f in enumerate(files))
|
||||
|
||||
response = client.messages.create(model=MODEL, messages=[{"role": "user",
|
||||
"content": f"Select relevant memory indices. Return JSON array.\n\n"
|
||||
f"Recent conversation:\n{recent}\n\nMemory catalog:\n{catalog}"}],
|
||||
max_tokens=200)
|
||||
indices = json.loads(re.search(r'\[.*?\]', response.content[0].text).group())
|
||||
return [files[i]["filename"] for i in indices if 0 <= i < len(files)]
|
||||
```
|
||||
|
||||
side-query が失敗した場合(API エラー、JSON パース失敗)、name + description のキーワードマッチにフォールバック。
|
||||
|
||||
### 書き込み:毎ターン終了後の抽出
|
||||
|
||||
ユーザーが毎回「これを覚えて」と言うわけではない。好みは通常、通常の会話の中に散らばっている:「タブの方がスペースより良い」「これからはシングルクォートにしよう」。
|
||||
|
||||
`extract_memories()` は各ターン終了時に実行、モデルが tool_use なしで停止した場合にトリガー(会話が自然な区切りに達したことを示す):
|
||||
|
||||
```python
|
||||
# In agent_loop:
|
||||
if response.stop_reason != "tool_use":
|
||||
extract_memories(messages) # 最近の会話から新しい記憶を抽出
|
||||
consolidate_memories() # 整理が必要かチェック
|
||||
return
|
||||
```
|
||||
|
||||
抽出前に既存の記憶を確認し、重複を回避。抽出プロンプトは LLM に `{name, type, description, body}` の JSON 配列を要求、本当に新しい情報がある場合のみファイルに書き込む。
|
||||
|
||||
```python
|
||||
def extract_memories(messages):
|
||||
dialogue = format_recent_messages(messages[-10:])
|
||||
existing = "\n".join(f"- {m['name']}: {m['description']}" for m in list_memory_files())
|
||||
|
||||
prompt = (
|
||||
"Extract user preferences, constraints, or project facts.\n"
|
||||
"Return JSON array: [{name, type, description, body}].\n"
|
||||
"If nothing new or already covered, return [].\n\n"
|
||||
f"Existing memories:\n{existing}\n\nDialogue:\n{dialogue[:4000]}"
|
||||
)
|
||||
# ... parse response, write files ...
|
||||
```
|
||||
|
||||
### 整理:低頻度の重複排除
|
||||
|
||||
記憶ファイルは蓄積される。`consolidate_memories()` はファイル数が閾値(デフォルト 10)に達した時にトリガー、LLM に重複排除、矛盾の統合、古い記憶の剪定を依頼:
|
||||
|
||||
```python
|
||||
CONSOLIDATE_THRESHOLD = 10
|
||||
|
||||
def consolidate_memories():
|
||||
files = list_memory_files()
|
||||
if len(files) < CONSOLIDATE_THRESHOLD:
|
||||
return # 少なすぎる、整理する価値なし
|
||||
# Send all memories to LLM, get back deduplicated list
|
||||
# Replace all files with consolidated results
|
||||
```
|
||||
|
||||
CC はこのプロセスを **Dream** と呼び、実際には 4 層のゲートがある:時間間隔、スキャンスロットル、セッション数、ファイルロック。教学版はファイル数閾値に簡略化。
|
||||
|
||||
### Memory に保存するもの
|
||||
|
||||
Memory はセッションを越えて有用な情報を保存する:ユーザーの好み、繰り返し出るフィードバック、プロジェクト背景、よく使う入口、調査の手がかりなど。「あとでまた使うもの」を対象にし、インデックス + オンデマンド読み込みで現在の会話に戻す。
|
||||
|
||||
session memory は 1 つのセッション内の連続性を扱う:compact 後も現在の会話に残すべき文脈を保持する。両者は役割が分かれている。Memory は長期知識を扱い、session memory は現在のセッションを compact 越しにつなぐ。
|
||||
|
||||
---
|
||||
|
||||
## s08 からの変更点
|
||||
|
||||
| コンポーネント | 変更前 (s08) | 変更後 (s09) |
|
||||
|-----------|-------------|-------------|
|
||||
| 記憶能力 | なし(圧縮後、好みはサマリと共に劣化) | ストレージ + 読み込み + 抽出 + 整理 |
|
||||
| 新規関数 | — | write_memory_file, select_relevant_memories, load_memories, extract_memories, consolidate_memories |
|
||||
| ストレージ | — | .memory/MEMORY.md インデックス + .memory/*.md ファイル |
|
||||
| ツール | bash, read, write, edit, glob, todo_write, task, load_skill, compact (9) | bash, read_file, write_file, edit_file, glob, task (6) |
|
||||
| ループ | 毎ターン圧縮のみ | 記憶注入 + 圧縮 + ターン終了後の抽出 + 定期整理 |
|
||||
|
||||
---
|
||||
|
||||
## 試してみよう
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s09_memory/code.py
|
||||
```
|
||||
|
||||
以下のプロンプトを試してみてください(複数ターンに分けて入力し、記憶の蓄積と読み込みを観察):
|
||||
|
||||
1. `I prefer using tabs for indentation, not spaces. Remember that.`
|
||||
2. `Create a Python file called test.py`(Agent がタブを使用したか観察)
|
||||
3. `What did I tell you about my preferences?`(Agent が覚えているか観察)
|
||||
4. `I also prefer single quotes over double quotes for strings.`
|
||||
|
||||
観察のポイント:各ターン終了後に `[Memory: extracted N new memories]` が表示されるか?`.memory/` ディレクトリに `.md` ファイルが生成されたか?`MEMORY.md` インデックスが更新されたか?新しい会話で Agent が以前の記憶を自動的に読み込んだか?
|
||||
|
||||
---
|
||||
|
||||
## 次へ
|
||||
|
||||
記憶、圧縮、ツールはすべて揃った。しかし system prompt はまだハードコードされた文字列。新しいツールを追加するには手動で説明を書き、プロジェクトを変えるにはプロンプト全体を書き直す。プロンプトは実行時に組み立てられるべき。
|
||||
|
||||
s10 System Prompt → セグメント + 実行時組み立て。異なるプロジェクト、異なるツール、異なるプロンプト。
|
||||
|
||||
<details>
|
||||
<summary>CC ソースコードの詳細</summary>
|
||||
|
||||
> 以下は CC ソースコード `src/` 下の `memdir/`、`services/`、`utils/`、`query/` の分析に基づく。行番号はソースコードと照合済み。
|
||||
|
||||
### ソースコードパス
|
||||
|
||||
| ファイル | 行数 | 職責 |
|
||||
|------|------|------|
|
||||
| `memdir/memdir.ts` | 507 | 核心:MEMORY.md 定義(`34-38`)、記憶動作指示で memory/plan/tasks を区別(`199-266`)、`loadMemoryPrompt()` 3 パス(`419-490`) |
|
||||
| `memdir/findRelevantMemories.ts` | 141 | Sonnet side-query で記憶選択(`18-24` システムプロンプト、`97-122` 呼び出しロジック) |
|
||||
| `memdir/memoryTypes.ts` | 271 | 型定義、frontmatter フィールド |
|
||||
| `memdir/memoryScan.ts` | — | .md ファイルをスキャン、MEMORY.md を除外、frontmatter を読み取り、最大 200 ファイル、mtime 降順(`35-94`) |
|
||||
| `services/extractMemories/extractMemories.ts` | 615 | forked agent で記憶を抽出、制限付き権限、`skipTranscript: true`、`maxTurns: 5`(`371-427`) |
|
||||
| `services/autoDream/autoDream.ts` | 324 | Dream 整理、4 層ゲート(`63-66` デフォルト値、`130-190` ゲート、`224-233` forked agent) |
|
||||
| `services/SessionMemory/sessionMemory.ts` | 495 | セッションレベルの記憶管理 |
|
||||
| `services/compact/sessionMemoryCompact.ts` | — | session memory 軽量サマリ、閾値 10K/5/40K(`56-61`) |
|
||||
| `utils/attachments.ts` | — | 注入予算:200 行 / 4096 バイト/ファイル、60KB/セッション(`269-288`);query で関連記憶を検索(`2196-2241`) |
|
||||
| `query.ts` | — | memory prefetch を毎ターン開始時に起動(`301-304`)、非ブロッキング収集(`1592-1614`) |
|
||||
| `query/stopHooks.ts` | — | stop hook fire-and-forget で抽出と Dream をトリガー(`141-155`) |
|
||||
|
||||
### 記憶選択:embedding ではなく LLM
|
||||
|
||||
CC は **Sonnet 自身で選択**(`findRelevantMemories.ts`)、embedding ベクトル類似度ではない:
|
||||
|
||||
1. `memoryScan.ts` が `.memory/` 下のすべての `.md` ファイルをスキャン(MEMORY.md を除外)、最大 200 ファイル、mtime 降順
|
||||
2. `name` + `description` をカタログとしてリスト化
|
||||
3. Sonnet side-query に送信:「名前と説明から本当に有用な記憶を選択(最大 5 件)。不明ならスキップ。」
|
||||
4. Sonnet が `{ selected_memories: ["file1.md", ...] }` を返却
|
||||
5. 選択されたファイルの完全な内容を読み込み(≤ 200 行 / 4096 バイト/ファイル)、注入。セッション総予算:60KB
|
||||
|
||||
毎ターンのユーザー turn 開始時、`query.ts:301-304` が memory prefetch を起動(非同期);ツール実行後、`1592-1614` が非ブロッキングで結果を収集。
|
||||
|
||||
### 抽出タイミング:stop hook、autoCompact 後ではない
|
||||
|
||||
トリガー位置(`stopHooks.ts:141-155`):`handleStopHooks()` 内で、fire-and-forget で抽出と Dream をトリガー。教学版は `stop_reason != "tool_use"` 分岐に抽出を配置、方向は一致。
|
||||
|
||||
CC の抽出は forked agent で実行(`extractMemories.ts:371-427`):制限付き権限、`skipTranscript: true`、`maxTurns: 5`。重複保護もある:メイン Agent が既に記憶ファイルを書き込んだ場合、抽出をスキップ。
|
||||
|
||||
### 記憶ファイル形式
|
||||
|
||||
CC は Markdown + YAML frontmatter を使用、教学版と一致。4 種類:`user`、`feedback`、`project`、`reference`。
|
||||
|
||||
`memdir.ts:34-38` がインデックス制約を定義:`MEMORY.md` 最大 200 行 / 25KB。`memdir.ts:199-266` が記憶動作指示を構築、memory と plan と tasks を明確に区別。保存場所:`~/.claude/projects/<sanitized-git-root>/memory/`。
|
||||
|
||||
### Dream:4 層ゲート
|
||||
|
||||
「アイドル時にトリガー」や「数が足りたら統合」ではなく、4 層のゲート(`autoDream.ts`、デフォルト値 `63-66`、ゲートロジック `130-190`):
|
||||
|
||||
1. **時間ゲート**:前回の統合から ≥ 24 時間
|
||||
2. **スキャンスロットル**:頻繁なファイルシステムスキャンを回避
|
||||
3. **セッションゲート**:前回の統合以降 ≥ 5 セッションの transcript が変更された
|
||||
4. **ロックゲート**:他のプロセスが統合中でない(`.consolidate-lock` ファイル)
|
||||
|
||||
統合自体は forked agent で実行(`224-233`):定位 → 直近のシグナル収集 → 統合してファイル書き込み → 剪定してインデックス更新。ロックファイルの mtime が lastConsolidatedAt。クラッシュリカバリ:1 時間後にロックが自動期限切れ。
|
||||
|
||||
### User Memory vs Session Memory
|
||||
|
||||
| | User Memory | Session Memory |
|
||||
|---|---|---|
|
||||
| 永続性 | セッション間 | 単一セッション |
|
||||
| ストレージ | `memory/` 下の複数 .md ファイル | `session-memory/<id>/memory.md` |
|
||||
| 注入先 | system prompt | compact サマリ |
|
||||
| 目的 | セッション間の知識蓄積 | compact を越えたコンテキストの連続性 |
|
||||
|
||||
sessionMemoryCompact(s08 で触れた仕組み)は Session Memory を活用:autoCompact の前に session memory ファイルを読み込み、内容が十分であれば(≥ 10K token、≥ 5 テキストメッセージ、≤ 40K token、`sessionMemoryCompact.ts:56-61`)、LLM を呼び出さずにサマリとして使用。
|
||||
|
||||
### 実際の実装が教学版より複雑な点
|
||||
|
||||
- **Feature flags**:記憶関連機能には複数の feature gate 層がある
|
||||
- **Team memory**:チーム共有記憶、`loadMemoryPrompt()` に専用パスあり(教学版では未カバー)
|
||||
- **KAIROS**:タイミング認識型の記憶抽出戦略、`loadMemoryPrompt()` の daily-log モード
|
||||
- **Prompt cache**:記憶注入は prompt cache の TTL を考慮する必要があり、毎ターン system prompt の大部分を書き直すことを避ける
|
||||
- **ファイルロック**:マルチプロセス時の並行制御
|
||||
- **Memory prefetch**:非同期プレフェッチ、メインフローをブロックしない
|
||||
|
||||
### 教学版の簡略化は意図的
|
||||
|
||||
- LLM side-query → LLM side-query + キーワードフォールバック:教学版は LLM 選択を維持し、フォールバックパスを追加
|
||||
- 記憶 JSON → Markdown + frontmatter:教学版は CC と一致
|
||||
- stop hook トリガー → `stop_reason != "tool_use"` 分岐:方向は一致
|
||||
- 4 層ゲート → ファイル数閾値:教学版には transcript システムやマルチセッションの概念がない
|
||||
- forked agent + 制限付き権限 → 直接呼び出し:教学版にはサブプロセス分離がない
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
@@ -0,0 +1,280 @@
|
||||
# s09: Memory — 压缩会丢细节,要有一层不丢的
|
||||
|
||||
[中文](README.md) · [English](README.en.md) · [日本語](README.ja.md)
|
||||
|
||||
s01 → ... → s07 → s08 → `s09` → [s10](../s10_system_prompt/) → s11 → ... → s20
|
||||
> *"压缩会丢细节, 要有一层不丢的"* — 文件仓库 + 索引 + 按需加载,跨压缩、跨会话。
|
||||
>
|
||||
> **Harness 层**: 记忆 — 跨压缩、跨会话的知识积累。
|
||||
|
||||
---
|
||||
|
||||
## 问题
|
||||
|
||||
s08 的 autoCompact 会把当前目标、剩余工作、用户约束写进摘要,但细节会丢失:"用 tab 缩进不要用空格"可能被简化成"用户有代码风格偏好"。而且新开一个会话,连摘要也没了。
|
||||
|
||||
LLM 没有持久状态,所有信息都在上下文窗口里。上下文满了要压缩,压缩就有损。需要一层不参与压缩、跨会话保留的存储。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
s08 的压缩管线保留,聚焦记忆。存储选文件系统:`.memory/` 目录下,每个记忆一个 `.md` 文件,带 YAML frontmatter(`name` / `description` / `type`)。文件多了需要索引:`MEMORY.md` 一行一个链接,注入 SYSTEM。
|
||||
|
||||
关键设计:索引常驻 SYSTEM prompt(可被 prompt cache 缓存),文件内容按需注入到当前 user turn(按 filename/description 匹配当前对话,不破坏 cache)。写入由每轮结束后的提取器完成:用户显式说"记住"或表达稳定偏好时,提取器会保存为记忆。文件积累多了,定期整理去重。
|
||||
|
||||
四类记忆,各有用途:
|
||||
|
||||
| 类型 | 回答什么 | 示例 |
|
||||
|------|---------|------|
|
||||
| user | 你是谁 | "用 tab 不用空格" |
|
||||
| feedback | 怎么做事 | "别 mock 数据库" |
|
||||
| project | 正在发生什么 | "auth 重写是合规驱动" |
|
||||
| reference | 东西在哪找 | "pipeline bug 在 Linear INGEST" |
|
||||
|
||||
---
|
||||
|
||||
## 工作原理
|
||||
|
||||

|
||||
|
||||
### 存储:Markdown 文件 + 索引
|
||||
|
||||
每个记忆是一个 `.md` 文件,YAML frontmatter 记录元数据:
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: user-preference-tabs
|
||||
description: User prefers tabs for indentation
|
||||
type: user
|
||||
---
|
||||
|
||||
User prefers using tabs, not spaces, for indentation.
|
||||
**Why:** Consistency with existing codebase conventions.
|
||||
**How to apply:** Always use tabs when writing or editing files.
|
||||
```
|
||||
|
||||
`MEMORY.md` 是索引,一行一个链接:
|
||||
|
||||
```markdown
|
||||
- [user-preference-tabs](user-preference-tabs.md) — User prefers tabs for indentation
|
||||
```
|
||||
|
||||
写入新记忆时自动重建索引:
|
||||
|
||||
```python
|
||||
def write_memory_file(name, mem_type, description, body):
|
||||
slug = name.lower().replace(" ", "-")
|
||||
filepath = MEMORY_DIR / f"{slug}.md"
|
||||
filepath.write_text(
|
||||
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
|
||||
)
|
||||
_rebuild_index()
|
||||
```
|
||||
|
||||
### 加载:两条路径
|
||||
|
||||
**路径一:索引常驻 SYSTEM。** `build_system()` 在每次用户请求开始时读取 `MEMORY.md`,把记忆清单注入。记忆提取和整理只在本轮结束时触发,因此同一轮用户请求中不需要重复重建 SYSTEM。
|
||||
|
||||
**路径二:相关记忆按需注入。** 每次用户请求开始时,`load_memories()` 把最近对话和记忆目录(name + description)一起发给 LLM 做一次轻量 side-query,选出相关的文件名,再读文件内容临时注入到当前 user turn。最多 5 条,控制开销。
|
||||
|
||||
```python
|
||||
def select_relevant_memories(messages, max_items=5):
|
||||
files = list_memory_files()
|
||||
if not files:
|
||||
return []
|
||||
|
||||
# Build catalog: "0: user-preference-tabs — User prefers tabs..."
|
||||
catalog = "\n".join(f"{i}: {f['name']} — {f['description']}" for i, f in enumerate(files))
|
||||
|
||||
response = client.messages.create(model=MODEL, messages=[{"role": "user",
|
||||
"content": f"Select relevant memory indices. Return JSON array.\n\n"
|
||||
f"Recent conversation:\n{recent}\n\nMemory catalog:\n{catalog}"}],
|
||||
max_tokens=200)
|
||||
text = extract_text(response.content).strip()
|
||||
indices = json.loads(re.search(r'\[.*?\]', text).group())
|
||||
return [files[i]["filename"] for i in indices if 0 <= i < len(files)]
|
||||
```
|
||||
|
||||
如果 side-query 失败(API 错误、JSON 解析失败),降级到关键词匹配 name + description。
|
||||
|
||||
### 写入:每轮结束后提取
|
||||
|
||||
用户不会每次都说"记住这个"。偏好通常散落在正常对话中:"用 tab 比空格好"、"以后都用单引号"。
|
||||
|
||||
`extract_memories()` 在每轮结束时运行,条件是模型停止且没有 tool_use(说明对话告一段落):
|
||||
|
||||
```python
|
||||
# In agent_loop:
|
||||
if response.stop_reason != "tool_use":
|
||||
extract_memories(pre_compress) # 从压缩前快照提取新记忆
|
||||
consolidate_memories() # 检查是否需要整理
|
||||
return
|
||||
```
|
||||
|
||||
提取前先检查已有记忆,避免重复。提取 prompt 要求 LLM 返回 `{name, type, description, body}` 的 JSON 数组,只有确实有新信息时才写文件。
|
||||
|
||||
```python
|
||||
def extract_memories(messages):
|
||||
dialogue = format_recent_messages(messages[-10:])
|
||||
existing = "\n".join(f"- {m['name']}: {m['description']}" for m in list_memory_files())
|
||||
|
||||
prompt = (
|
||||
"Extract user preferences, constraints, or project facts.\n"
|
||||
"Return JSON array: [{name, type, description, body}].\n"
|
||||
"If nothing new or already covered, return [].\n\n"
|
||||
f"Existing memories:\n{existing}\n\nDialogue:\n{dialogue[:4000]}"
|
||||
)
|
||||
# ... parse response, write files ...
|
||||
```
|
||||
|
||||
### 整理:低频合并去重
|
||||
|
||||
记忆文件会积累。`consolidate_memories()` 在文件数达到阈值(默认 10)时触发,让 LLM 去重、合并矛盾、淘汰过时记忆:
|
||||
|
||||
```python
|
||||
CONSOLIDATE_THRESHOLD = 10
|
||||
|
||||
def consolidate_memories():
|
||||
files = list_memory_files()
|
||||
if len(files) < CONSOLIDATE_THRESHOLD:
|
||||
return # 太少,不值得整理
|
||||
# Send all memories to LLM, get back deduplicated list
|
||||
# Replace all files with consolidated results
|
||||
```
|
||||
|
||||
CC 把这个过程叫 Dream,实际有四层门控:时间间隔、扫描节流、会话数、文件锁。教学版简化为文件数阈值。
|
||||
|
||||
### Memory 适合保存什么
|
||||
|
||||
Memory 保存跨会话仍然有用的信息:用户偏好、反复出现的反馈、项目背景、常用入口和排查线索。它关注“以后还会用到什么”,并通过索引 + 按需加载把这些信息带回当前对话。
|
||||
|
||||
session memory 关注同一会话内的连续性:compact 之后,当前会话还需要保留哪些上下文。两者配合使用:Memory 管长期知识,session memory 管当前会话的压缩续接。
|
||||
|
||||
---
|
||||
|
||||
## 相对 s08 的变更
|
||||
|
||||
| 组件 | 之前 (s08) | 之后 (s09) |
|
||||
|------|-----------|-----------|
|
||||
| 记忆能力 | 无(压缩后偏好随摘要退化) | 存储 + 加载 + 提取 + 整理 |
|
||||
| 新函数 | — | write_memory_file, select_relevant_memories, load_memories, extract_memories, consolidate_memories |
|
||||
| 存储 | — | .memory/MEMORY.md 索引 + .memory/*.md 文件 |
|
||||
| 工具 | bash, read, write, edit, glob, todo_write, task, load_skill, compact (9) | bash, read_file, write_file, edit_file, glob, task (6) |
|
||||
| 循环 | 每轮只做压缩 | 每轮注入记忆 + 压缩 + 每轮结束后提取 + 定期整理 |
|
||||
|
||||
---
|
||||
|
||||
## 试一下
|
||||
|
||||
```sh
|
||||
cd learn-claude-code
|
||||
python s09_memory/code.py
|
||||
```
|
||||
|
||||
试试这些 prompt(分多轮输入,观察记忆的累积和加载):
|
||||
|
||||
1. `I prefer using tabs for indentation, not spaces. Remember that.`
|
||||
2. `Create a Python file called test.py`(观察 Agent 是否用了 tab)
|
||||
3. `What did I tell you about my preferences?`(观察 Agent 是否记得)
|
||||
4. `I also prefer single quotes over double quotes for strings.`
|
||||
|
||||
观察重点:每轮结束后是否出现 `[Memory: extracted N new memories]`?`.memory/` 目录下是否生成了 `.md` 文件?`MEMORY.md` 索引是否更新?新一轮对话时 Agent 是否自动加载了之前的记忆?
|
||||
|
||||
---
|
||||
|
||||
## 接下来
|
||||
|
||||
记忆、压缩、工具都已就绪。但 system prompt 还是硬编码的一大段字符串。加了新工具要手动加描述,换了项目要重写整个 prompt。prompt 应该运行时组装。
|
||||
|
||||
s10 System Prompt → 分段 + 运行时组装。不同项目、不同工具,拼出不同的 prompt。
|
||||
|
||||
<details>
|
||||
<summary>深入 CC 源码</summary>
|
||||
|
||||
> 以下基于 CC 源码 `src/` 下 `memdir/`、`services/`、`utils/`、`query/` 的分析,行号已对照核实。
|
||||
|
||||
### 源码路径
|
||||
|
||||
| 文件 | 行数 | 职责 |
|
||||
|------|------|------|
|
||||
| `memdir/memdir.ts` | 507 | 核心:MEMORY.md 定义(`34-38`)、记忆行为指令区分 memory/plan/tasks(`199-266`)、`loadMemoryPrompt()` 三条路径(`419-490`) |
|
||||
| `memdir/findRelevantMemories.ts` | 141 | Sonnet side-query 选记忆(`18-24` 系统提示、`97-122` 调用逻辑) |
|
||||
| `memdir/memoryTypes.ts` | 271 | 类型定义,frontmatter 字段 |
|
||||
| `memdir/memoryScan.ts` | — | 扫描 .md 文件,排除 MEMORY.md,读 frontmatter,最多 200 个,按 mtime 降序(`35-94`) |
|
||||
| `services/extractMemories/extractMemories.ts` | 615 | forked agent 提取记忆,受限权限,`skipTranscript: true`,`maxTurns: 5`(`371-427`) |
|
||||
| `services/autoDream/autoDream.ts` | 324 | Dream 整理,四层门控(`63-66` 默认值、`130-190` 门控、`224-233` forked agent) |
|
||||
| `services/SessionMemory/sessionMemory.ts` | 495 | 会话级记忆管理 |
|
||||
| `services/compact/sessionMemoryCompact.ts` | — | session memory 轻量摘要,阈值 10K/5/40K(`56-61`) |
|
||||
| `utils/attachments.ts` | — | 注入预算:200 行 / 4096 字节每文件,60KB 每 session(`269-288`);按 query 找相关 memory(`2196-2241`) |
|
||||
| `query.ts` | — | memory prefetch 每轮启动(`301-304`),非阻塞收集(`1592-1614`) |
|
||||
| `query/stopHooks.ts` | — | stop hook fire-and-forget 触发提取和 Dream(`141-155`) |
|
||||
|
||||
### 记忆选择:LLM 选,不是 embedding
|
||||
|
||||
CC 用 **Sonnet 本身来选**(`findRelevantMemories.ts`),不是 embedding 向量相似度:
|
||||
|
||||
1. `memoryScan.ts` 扫描 `.memory/` 下所有 `.md` 文件(排除 MEMORY.md),最多 200 个,按 mtime 降序
|
||||
2. 把 `name` + `description` 列成清单
|
||||
3. 发给 Sonnet side-query:"根据名称和描述选出真正有用的记忆(最多 5 个)。不确定就不要选。"
|
||||
4. Sonnet 返回 `{ selected_memories: ["file1.md", ...] }`
|
||||
5. 选中文件读取完整内容(每文件 ≤ 200 行 / 4096 字节),注入上下文。单 session 总预算 60KB
|
||||
|
||||
每轮用户 turn 开始时,`query.ts:301-304` 启动 memory prefetch(异步);工具执行后 `1592-1614` 非阻塞收集结果,不卡主流程。
|
||||
|
||||
### 提取时机:stop hook,不是 autoCompact 后
|
||||
|
||||
触发位置(`stopHooks.ts:141-155`):在 `handleStopHooks()` 中,fire-and-forget 触发提取和 Dream。教学版把提取放在 `stop_reason != "tool_use"` 分支里,方向一致。
|
||||
|
||||
CC 的提取通过 forked agent 执行(`extractMemories.ts:371-427`):受限权限、`skipTranscript: true`、`maxTurns: 5`。还有重叠保护:如果主 Agent 已经写入了记忆文件,跳过提取。
|
||||
|
||||
### 记忆文件格式
|
||||
|
||||
CC 用 Markdown + YAML frontmatter,和教学版一致。四种类型:`user`、`feedback`、`project`、`reference`。
|
||||
|
||||
`memdir.ts:34-38` 定义索引约束:`MEMORY.md` 最多 200 行 / 25KB。`memdir.ts:199-266` 构建记忆行为指令,明确区分 memory、plan、tasks。存储位置:`~/.claude/projects/<sanitized-git-root>/memory/`。
|
||||
|
||||
### Dream:四层门控
|
||||
|
||||
不是"空闲时触发"或"数量够了就合并",而是四层门控(`autoDream.ts`,默认值 `63-66`,门控逻辑 `130-190`):
|
||||
|
||||
1. **时间门控**:距上次合并 ≥ 24 小时
|
||||
2. **扫描节流**:避免频繁扫描文件系统
|
||||
3. **会话门控**:自上次合并以来修改了 ≥ 5 个会话 transcript
|
||||
4. **锁门控**:没有其他进程正在合并(`.consolidate-lock` 文件)
|
||||
|
||||
合并本身通过 forked agent 执行(`224-233`):定位 → 收集近期信号 → 合并写文件 → 剪枝更新索引。锁文件 mtime 就是 lastConsolidatedAt。崩溃恢复:1 小时后锁自动过期。
|
||||
|
||||
### User Memory vs Session Memory
|
||||
|
||||
| | User Memory | Session Memory |
|
||||
|---|---|---|
|
||||
| 持久性 | 跨会话 | 单会话 |
|
||||
| 存储 | `memory/` 下多个 .md 文件 | `session-memory/<id>/memory.md` |
|
||||
| 加载到 | system prompt | compact 摘要 |
|
||||
| 用途 | 跨会话的知识积累 | 跨 compact 的上下文连续性 |
|
||||
|
||||
sessionMemoryCompact(s08 中提到的机制)正是使用了 Session Memory:autoCompact 前先读 session memory 文件,如果内容足够(≥ 10K token、≥ 5 条文本消息、≤ 40K token,`sessionMemoryCompact.ts:56-61`),就用它做摘要,不调 LLM。
|
||||
|
||||
### 真实实现比教学版复杂的地方
|
||||
|
||||
- **Feature flags**:记忆相关功能有多层 feature gate 控制
|
||||
- **Team memory**:团队共享记忆,`loadMemoryPrompt()` 有专门路径(教学版未涉及)
|
||||
- **KAIROS**:时机感知的记忆提取策略,`loadMemoryPrompt()` 中 daily-log 模式
|
||||
- **Prompt cache**:记忆注入需要考虑 prompt cache 的 TTL,避免每次都重写 system prompt 的大段内容
|
||||
- **文件锁**:多进程并发时的锁机制
|
||||
- **Memory prefetch**:异步预取,不阻塞主流程
|
||||
|
||||
### 教学版的简化是刻意的
|
||||
|
||||
- LLM side-query → LLM side-query + 关键词降级:教学版保留了 LLM 选择,加了降级路径
|
||||
- 记忆 JSON → Markdown + frontmatter:教学版与 CC 一致
|
||||
- stop hook 触发 → `stop_reason != "tool_use"` 分支:方向一致
|
||||
- 四层门控 → 文件数阈值:教学版没有 transcript 系统和多会话概念
|
||||
- forked agent + 受限权限 → 直接调用:教学版没有子进程隔离
|
||||
|
||||
</details>
|
||||
|
||||
<!-- translation-sync: zh@v1, en@v1, ja@v1 -->
|
||||
@@ -0,0 +1,655 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
s09_memory.py - Memory System
|
||||
|
||||
Persistent, cross-session knowledge for the coding agent.
|
||||
|
||||
Storage:
|
||||
.memory/
|
||||
MEMORY.md ← index (one line per memory, ≤200 lines)
|
||||
feedback_tabs.md ← individual memory files (Markdown + YAML frontmatter)
|
||||
user_profile.md
|
||||
project_facts.md
|
||||
|
||||
Flow in agent_loop:
|
||||
1. Load MEMORY.md index into SYSTEM prompt (cheap, always present)
|
||||
2. Select relevant memories by filename/description → inject content
|
||||
3. Run compression pipeline from s08
|
||||
4. After each turn ends → extract new memories from original messages
|
||||
5. Periodically consolidate (Dream)
|
||||
|
||||
Builds on s08 (context compact). Usage:
|
||||
|
||||
python s09_memory/code.py
|
||||
Needs: pip install anthropic python-dotenv + ANTHROPIC_API_KEY in .env
|
||||
"""
|
||||
|
||||
import os, subprocess, json, time, re
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import readline
|
||||
readline.parse_and_bind('set bind-tty-special-chars off')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from anthropic import Anthropic
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv(override=True)
|
||||
if os.getenv("ANTHROPIC_BASE_URL"): os.environ.pop("ANTHROPIC_AUTH_TOKEN", None)
|
||||
|
||||
WORKDIR = Path.cwd()
|
||||
MEMORY_DIR = WORKDIR / ".memory"; MEMORY_DIR.mkdir(exist_ok=True)
|
||||
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
|
||||
SKILLS_DIR = WORKDIR / "skills"
|
||||
TRANSCRIPT_DIR = WORKDIR / ".transcripts"
|
||||
TOOL_RESULTS_DIR = WORKDIR / ".task_outputs" / "tool-results"
|
||||
client = Anthropic(base_url=os.getenv("ANTHROPIC_BASE_URL"))
|
||||
MODEL = os.environ["MODEL_ID"]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# NEW in s09: Memory System
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
MEMORY_TYPES = ["user", "feedback", "project", "reference"]
|
||||
|
||||
def _parse_frontmatter(text: str) -> tuple[dict, str]:
|
||||
if not text.startswith("---"):
|
||||
return {}, text
|
||||
parts = text.split("---", 2)
|
||||
if len(parts) < 3:
|
||||
return {}, text
|
||||
meta = {}
|
||||
for line in parts[1].strip().splitlines():
|
||||
if ":" in line:
|
||||
k, v = line.split(":", 1)
|
||||
meta[k.strip()] = v.strip().strip('"').strip("'")
|
||||
return meta, parts[2].strip()
|
||||
|
||||
|
||||
def write_memory_file(name: str, mem_type: str, description: str, body: str):
|
||||
"""Write a single memory file with YAML frontmatter."""
|
||||
slug = name.lower().replace(" ", "-").replace("/", "-")
|
||||
filename = f"{slug}.md"
|
||||
filepath = MEMORY_DIR / filename
|
||||
filepath.write_text(
|
||||
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
|
||||
)
|
||||
_rebuild_index()
|
||||
return filepath
|
||||
|
||||
|
||||
def _rebuild_index():
|
||||
"""Rebuild MEMORY.md index from all memory files."""
|
||||
lines = []
|
||||
for f in sorted(MEMORY_DIR.glob("*.md")):
|
||||
if f.name == "MEMORY.md":
|
||||
continue
|
||||
raw = f.read_text()
|
||||
meta, body = _parse_frontmatter(raw)
|
||||
name = meta.get("name", f.stem)
|
||||
desc = meta.get("description", body.split("\n")[0][:80])
|
||||
lines.append(f"- [{name}]({f.name}) — {desc}")
|
||||
MEMORY_INDEX.write_text("\n".join(lines) + "\n" if lines else "")
|
||||
|
||||
|
||||
def read_memory_index() -> str:
|
||||
"""Read MEMORY.md index (injected into SYSTEM every turn)."""
|
||||
if not MEMORY_INDEX.exists():
|
||||
return ""
|
||||
text = MEMORY_INDEX.read_text().strip()
|
||||
return text if text else ""
|
||||
|
||||
|
||||
def read_memory_file(filename: str) -> str | None:
|
||||
"""Read a single memory file's full content."""
|
||||
path = MEMORY_DIR / filename
|
||||
if not path.exists():
|
||||
return None
|
||||
return path.read_text()
|
||||
|
||||
|
||||
def list_memory_files() -> list[dict]:
|
||||
"""List all memory files with metadata."""
|
||||
result = []
|
||||
for f in sorted(MEMORY_DIR.glob("*.md")):
|
||||
if f.name == "MEMORY.md":
|
||||
continue
|
||||
raw = f.read_text()
|
||||
meta, body = _parse_frontmatter(raw)
|
||||
result.append({
|
||||
"filename": f.name,
|
||||
"name": meta.get("name", f.stem),
|
||||
"description": meta.get("description", ""),
|
||||
"type": meta.get("type", "user"),
|
||||
"body": body,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def select_relevant_memories(messages: list, max_items: int = 5) -> list[str]:
|
||||
"""Select relevant memory filenames by matching recent conversation against
|
||||
memory names/descriptions. Uses a simple LLM call (or falls back to keyword
|
||||
matching on name+description)."""
|
||||
files = list_memory_files()
|
||||
if not files:
|
||||
return []
|
||||
|
||||
# Collect recent user text for context
|
||||
recent_texts = []
|
||||
for msg in reversed(messages):
|
||||
if msg.get("role") == "user":
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, list):
|
||||
content = " ".join(
|
||||
str(getattr(b, "text", "")) for b in content
|
||||
if getattr(b, "type", None) == "text"
|
||||
)
|
||||
if isinstance(content, str):
|
||||
recent_texts.append(content)
|
||||
if len(recent_texts) >= 3:
|
||||
break
|
||||
recent = " ".join(reversed(recent_texts))[:2000]
|
||||
|
||||
if not recent.strip():
|
||||
return []
|
||||
|
||||
# Build catalog of name + description for LLM to choose from
|
||||
catalog_lines = []
|
||||
for i, f in enumerate(files):
|
||||
catalog_lines.append(f"{i}: {f['name']} — {f['description']}")
|
||||
catalog = "\n".join(catalog_lines)
|
||||
|
||||
prompt = (
|
||||
"Given the recent conversation and the memory catalog below, "
|
||||
"select the indices of memories that are clearly relevant. "
|
||||
"Return ONLY a JSON array of integers, e.g. [0, 3]. "
|
||||
"If none are relevant, return [].\n\n"
|
||||
f"Recent conversation:\n{recent}\n\n"
|
||||
f"Memory catalog:\n{catalog}"
|
||||
)
|
||||
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model=MODEL,
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
max_tokens=200,
|
||||
)
|
||||
text = extract_text(response.content).strip()
|
||||
# Extract JSON array from response
|
||||
match = re.search(r'\[.*?\]', text, re.DOTALL)
|
||||
if match:
|
||||
indices = json.loads(match.group())
|
||||
selected = []
|
||||
for idx in indices:
|
||||
if isinstance(idx, int) and 0 <= idx < len(files):
|
||||
selected.append(files[idx]["filename"])
|
||||
if len(selected) >= max_items:
|
||||
break
|
||||
return selected
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback: keyword matching on name + description
|
||||
keywords = [w.lower() for w in recent.split() if len(w) > 3]
|
||||
selected = []
|
||||
for f in files:
|
||||
text = (f["name"] + " " + f["description"]).lower()
|
||||
if any(kw in text for kw in keywords):
|
||||
selected.append(f["filename"])
|
||||
if len(selected) >= max_items:
|
||||
break
|
||||
return selected
|
||||
|
||||
|
||||
def load_memories(messages: list) -> str:
|
||||
"""Load relevant memory content for injection into context."""
|
||||
selected_files = select_relevant_memories(messages)
|
||||
if not selected_files:
|
||||
return ""
|
||||
|
||||
parts = ["<relevant_memories>"]
|
||||
for filename in selected_files:
|
||||
content = read_memory_file(filename)
|
||||
if content:
|
||||
parts.append(content)
|
||||
parts.append("</relevant_memories>")
|
||||
return "\n\n".join(parts)
|
||||
|
||||
|
||||
def extract_memories(messages: list):
|
||||
"""Extract new memories from recent dialogue. Runs after each turn."""
|
||||
# Collect recent conversation text
|
||||
dialogue_parts = []
|
||||
for msg in messages[-10:]:
|
||||
role = msg.get("role", "?")
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, list):
|
||||
content = " ".join(
|
||||
str(getattr(b, "text", "")) for b in content
|
||||
if getattr(b, "type", None) == "text"
|
||||
)
|
||||
if isinstance(content, str) and content.strip():
|
||||
dialogue_parts.append(f"{role}: {content}")
|
||||
dialogue = "\n".join(dialogue_parts)
|
||||
|
||||
if not dialogue.strip():
|
||||
return
|
||||
|
||||
# Check existing memories to avoid duplicates
|
||||
existing = list_memory_files()
|
||||
existing_desc = "\n".join(f"- {m['name']}: {m['description']}" for m in existing) if existing else "(none)"
|
||||
|
||||
prompt = (
|
||||
"Extract user preferences, constraints, or project facts from this dialogue.\n"
|
||||
"Return a JSON array. Each item: {name, type, description, body}.\n"
|
||||
"- name: short kebab-case identifier (e.g. 'user-preference-tabs')\n"
|
||||
"- type: one of 'user' (user preference), 'feedback' (guidance), "
|
||||
"'project' (project fact), 'reference' (external pointer)\n"
|
||||
"- description: one-line summary for index lookup\n"
|
||||
"- body: full detail in markdown\n"
|
||||
"If nothing new or already covered by existing memories, return [].\n\n"
|
||||
f"Existing memories:\n{existing_desc}\n\n"
|
||||
f"Dialogue:\n{dialogue[:4000]}"
|
||||
)
|
||||
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model=MODEL, messages=[{"role": "user", "content": prompt}], max_tokens=800
|
||||
)
|
||||
text = extract_text(response.content).strip()
|
||||
# Extract JSON array from response
|
||||
match = re.search(r'\[.*\]', text, re.DOTALL)
|
||||
if not match:
|
||||
return
|
||||
items = json.loads(match.group())
|
||||
if not items:
|
||||
return
|
||||
count = 0
|
||||
for mem in items:
|
||||
name = mem.get("name", f"memory_{int(time.time())}")
|
||||
mem_type = mem.get("type", "user")
|
||||
desc = mem.get("description", "")
|
||||
body = mem.get("body", "")
|
||||
if desc and body:
|
||||
write_memory_file(name, mem_type, desc, body)
|
||||
count += 1
|
||||
if count:
|
||||
print(f"\n\033[33m[Memory: extracted {count} new memories]\033[0m")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
CONSOLIDATE_THRESHOLD = 10
|
||||
|
||||
def consolidate_memories():
|
||||
"""Merge duplicate/stale memories. Triggered when file count ≥ threshold."""
|
||||
files = list_memory_files()
|
||||
if len(files) < CONSOLIDATE_THRESHOLD:
|
||||
return
|
||||
|
||||
catalog = "\n\n".join(
|
||||
f"## {f['filename']}\nname: {f['name']}\ndescription: {f['description']}\n{f['body']}"
|
||||
for f in files
|
||||
)
|
||||
|
||||
prompt = (
|
||||
"Consolidate the following memory files. Rules:\n"
|
||||
"1. Merge duplicates into one\n"
|
||||
"2. Remove outdated/contradicted memories\n"
|
||||
"3. Keep the total under 30 memories\n"
|
||||
"4. Preserve important user preferences above all\n"
|
||||
"Return a JSON array. Each item: {name, type, description, body}.\n\n"
|
||||
f"{catalog[:16000]}"
|
||||
)
|
||||
|
||||
try:
|
||||
response = client.messages.create(
|
||||
model=MODEL, messages=[{"role": "user", "content": prompt}], max_tokens=3000
|
||||
)
|
||||
text = extract_text(response.content).strip()
|
||||
match = re.search(r'\[.*\]', text, re.DOTALL)
|
||||
if not match:
|
||||
return
|
||||
items = json.loads(match.group())
|
||||
|
||||
# Remove old memory files (keep MEMORY.md)
|
||||
for f in MEMORY_DIR.glob("*.md"):
|
||||
if f.name != "MEMORY.md":
|
||||
f.unlink()
|
||||
|
||||
for mem in items:
|
||||
name = mem.get("name", f"memory_{int(time.time())}")
|
||||
mem_type = mem.get("type", "user")
|
||||
desc = mem.get("description", "")
|
||||
body = mem.get("body", "")
|
||||
if desc and body:
|
||||
write_memory_file(name, mem_type, desc, body)
|
||||
|
||||
print(f"\n\033[33m[Memory: consolidated {len(files)} → {len(items)} memories]\033[0m")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# Build SYSTEM with memory index
|
||||
def build_system() -> str:
|
||||
index = read_memory_index()
|
||||
memories_section = f"\n\nMemories available:\n{index}" if index else ""
|
||||
return (
|
||||
f"You are a coding agent at {WORKDIR}."
|
||||
f"{memories_section}\n"
|
||||
"Relevant memories are injected below. Respect user preferences from memory.\n"
|
||||
"When the user says 'remember' or expresses a clear preference, extract it as a memory."
|
||||
)
|
||||
|
||||
SUB_SYSTEM = (
|
||||
f"You are a coding agent at {WORKDIR}. "
|
||||
"Complete the task you were given, then return a concise summary. "
|
||||
"Do not delegate further."
|
||||
)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FROM s02-s08 (skeleton): Basic tools
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
def safe_path(p: str) -> Path:
|
||||
path = (WORKDIR / p).resolve()
|
||||
if not path.is_relative_to(WORKDIR): raise ValueError(f"Path escapes workspace: {p}")
|
||||
return path
|
||||
|
||||
def run_bash(command: str) -> str:
|
||||
try:
|
||||
r = subprocess.run(command, shell=True, cwd=WORKDIR, capture_output=True, text=True, timeout=120)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
return out[:50000] if out else "(no output)"
|
||||
except subprocess.TimeoutExpired: return "Error: Timeout (120s)"
|
||||
|
||||
def run_read(path: str, limit: int | None = None) -> str:
|
||||
try:
|
||||
lines = safe_path(path).read_text().splitlines()
|
||||
if limit and limit < len(lines): lines = lines[:limit] + [f"... ({len(lines) - limit} more lines)"]
|
||||
return "\n".join(lines)
|
||||
except Exception as e: return f"Error: {e}"
|
||||
|
||||
def run_write(path: str, content: str) -> str:
|
||||
try:
|
||||
file_path = safe_path(path); file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
file_path.write_text(content); return f"Wrote {len(content)} bytes to {path}"
|
||||
except Exception as e: return f"Error: {e}"
|
||||
|
||||
def run_edit(path: str, old_text: str, new_text: str) -> str:
|
||||
try:
|
||||
file_path = safe_path(path)
|
||||
text = file_path.read_text()
|
||||
if old_text not in text: return f"Error: text not found in {path}"
|
||||
file_path.write_text(text.replace(old_text, new_text, 1))
|
||||
return f"Edited {path}"
|
||||
except Exception as e: return f"Error: {e}"
|
||||
|
||||
def run_glob(pattern: str) -> str:
|
||||
import glob as g
|
||||
try:
|
||||
results = []
|
||||
for match in g.glob(pattern, root_dir=WORKDIR):
|
||||
if (WORKDIR / match).resolve().is_relative_to(WORKDIR):
|
||||
results.append(match)
|
||||
return "\n".join(results) if results else "(no matches)"
|
||||
except Exception as e: return f"Error: {e}"
|
||||
|
||||
def extract_text(content) -> str:
|
||||
if not isinstance(content, list): return str(content)
|
||||
return "\n".join(getattr(b, "text", "") for b in content if getattr(b, "type", None) == "text")
|
||||
|
||||
# Subagent (simplified from s06-s07)
|
||||
SUB_TOOLS = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
|
||||
{"name": "read_file", "description": "Read file contents.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write content to a file.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
||||
]
|
||||
SUB_HANDLERS = {"bash": run_bash, "read_file": run_read, "write_file": run_write}
|
||||
|
||||
def spawn_subagent(description: str) -> str:
|
||||
print(f"\n\033[35m[Subagent spawned]\033[0m")
|
||||
messages = [{"role": "user", "content": description}]
|
||||
for _ in range(30):
|
||||
response = client.messages.create(model=MODEL, system=SUB_SYSTEM,
|
||||
messages=messages, tools=SUB_TOOLS, max_tokens=8000)
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use": break
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type == "tool_use":
|
||||
handler = SUB_HANDLERS.get(block.name)
|
||||
output = handler(**block.input) if handler else f"Unknown: {block.name}"
|
||||
print(f" \033[90m[sub] {block.name}: {str(output)[:100]}\033[0m")
|
||||
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
result = extract_text(messages[-1]["content"])
|
||||
if not result:
|
||||
for msg in reversed(messages):
|
||||
if msg["role"] == "assistant":
|
||||
result = extract_text(msg["content"])
|
||||
if result: break
|
||||
if not result: result = "Subagent stopped after 30 turns without final answer."
|
||||
print(f"\033[35m[Subagent done]\033[0m")
|
||||
return result
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# FROM s08 (skeleton): Compaction pipeline
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
CONTEXT_LIMIT = 50000; KEEP_RECENT = 3; PERSIST_THRESHOLD = 30000
|
||||
|
||||
def estimate_size(msgs): return len(str(msgs))
|
||||
|
||||
def _block_type(block):
|
||||
return block.get("type") if isinstance(block, dict) else getattr(block, "type", None)
|
||||
|
||||
def _message_has_tool_use(msg):
|
||||
if msg.get("role") != "assistant":
|
||||
return False
|
||||
content = msg.get("content")
|
||||
if not isinstance(content, list):
|
||||
return False
|
||||
return any(_block_type(block) == "tool_use" for block in content)
|
||||
|
||||
def _is_tool_result_message(msg):
|
||||
if msg.get("role") != "user":
|
||||
return False
|
||||
content = msg.get("content")
|
||||
if not isinstance(content, list):
|
||||
return False
|
||||
return any(isinstance(block, dict) and block.get("type") == "tool_result" for block in content)
|
||||
|
||||
def snip_compact(msgs, mx=50):
|
||||
if len(msgs) <= mx: return msgs
|
||||
head_end, tail_start = 3, len(msgs) - (mx - 3)
|
||||
if head_end > 0 and _message_has_tool_use(msgs[head_end - 1]):
|
||||
while head_end < len(msgs) and _is_tool_result_message(msgs[head_end]):
|
||||
head_end += 1
|
||||
if (tail_start > 0 and tail_start < len(msgs)
|
||||
and _is_tool_result_message(msgs[tail_start])
|
||||
and _message_has_tool_use(msgs[tail_start - 1])):
|
||||
tail_start -= 1
|
||||
if head_end >= tail_start:
|
||||
return msgs
|
||||
return msgs[:head_end] + [{"role": "user", "content": f"[snipped {tail_start - head_end} msgs]"}] + msgs[tail_start:]
|
||||
|
||||
def collect_tool_results(msgs):
|
||||
blocks = []
|
||||
for mi, msg in enumerate(msgs):
|
||||
if msg.get("role") != "user" or not isinstance(msg.get("content"), list): continue
|
||||
for bi, block in enumerate(msg["content"]):
|
||||
if isinstance(block, dict) and block.get("type") == "tool_result": blocks.append((mi, bi, block))
|
||||
return blocks
|
||||
|
||||
def micro_compact(msgs):
|
||||
tr = collect_tool_results(msgs)
|
||||
if len(tr) <= KEEP_RECENT: return msgs
|
||||
for _, _, b in tr[:-KEEP_RECENT]:
|
||||
if len(b.get("content", "")) > 120: b["content"] = "[Earlier tool result compacted.]"
|
||||
return msgs
|
||||
|
||||
def persist_large(tid, out):
|
||||
if len(out) <= PERSIST_THRESHOLD: return out
|
||||
TOOL_RESULTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
p = TOOL_RESULTS_DIR / f"{tid}.txt"
|
||||
if not p.exists(): p.write_text(out)
|
||||
return f"<persisted-output>\nFull: {p}\nPreview:\n{out[:2000]}\n</persisted-output>"
|
||||
|
||||
def tool_result_budget(msgs, mx=200_000):
|
||||
last = msgs[-1] if msgs else None
|
||||
if not last or last.get("role") != "user" or not isinstance(last.get("content"), list): return msgs
|
||||
blocks = [(i, b) for i, b in enumerate(last["content"]) if isinstance(b, dict) and b.get("type") == "tool_result"]
|
||||
total = sum(len(str(b.get("content", ""))) for _, b in blocks)
|
||||
if total <= mx: return msgs
|
||||
for _, block in sorted(blocks, key=lambda p: len(str(p[1].get("content", ""))), reverse=True):
|
||||
if total <= mx: break
|
||||
c = str(block.get("content", ""))
|
||||
if len(c) <= PERSIST_THRESHOLD: continue
|
||||
block["content"] = persist_large(block.get("tool_use_id", "?"), c)
|
||||
total = sum(len(str(b.get("content", ""))) for _, b in blocks)
|
||||
return msgs
|
||||
|
||||
def write_transcript(msgs):
|
||||
TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
p = TRANSCRIPT_DIR / f"transcript_{int(time.time())}.jsonl"
|
||||
with p.open("w") as f:
|
||||
for m in msgs: f.write(json.dumps(m, default=str) + "\n")
|
||||
return p
|
||||
|
||||
def summarize_history(msgs):
|
||||
conv = json.dumps(msgs, default=str)[:80000]
|
||||
r = client.messages.create(model=MODEL, messages=[{"role": "user", "content":
|
||||
"Summarize this coding-agent conversation so work can continue.\n"
|
||||
"Preserve: 1. current goal, 2. key findings, 3. files changed, 4. remaining work, 5. user constraints.\n\n" + conv}],
|
||||
max_tokens=2000)
|
||||
return extract_text(r.content).strip()
|
||||
|
||||
def compact_history(msgs):
|
||||
write_transcript(msgs)
|
||||
summary = summarize_history(msgs)
|
||||
return [{"role": "user", "content": f"[Compacted]\n\n{summary}"}]
|
||||
|
||||
def reactive_compact(msgs):
|
||||
write_transcript(msgs)
|
||||
tail_start = max(0, len(msgs) - 5)
|
||||
if (tail_start > 0 and tail_start < len(msgs)
|
||||
and _is_tool_result_message(msgs[tail_start])
|
||||
and _message_has_tool_use(msgs[tail_start - 1])):
|
||||
tail_start -= 1
|
||||
summary = summarize_history(msgs[:tail_start])
|
||||
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *msgs[tail_start:]]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# Tool Definitions (skeleton — fewer tools to focus on memory)
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
TOOLS = [
|
||||
{"name": "bash", "description": "Run a shell command.",
|
||||
"input_schema": {"type": "object", "properties": {"command": {"type": "string"}}, "required": ["command"]}},
|
||||
{"name": "read_file", "description": "Read file contents.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}},
|
||||
{"name": "write_file", "description": "Write content to a file.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "content": {"type": "string"}}, "required": ["path", "content"]}},
|
||||
{"name": "edit_file", "description": "Replace exact text in a file once.",
|
||||
"input_schema": {"type": "object", "properties": {"path": {"type": "string"}, "old_text": {"type": "string"}, "new_text": {"type": "string"}}, "required": ["path", "old_text", "new_text"]}},
|
||||
{"name": "glob", "description": "Find files matching a glob pattern.",
|
||||
"input_schema": {"type": "object", "properties": {"pattern": {"type": "string"}}, "required": ["pattern"]}},
|
||||
{"name": "task", "description": "Launch a subagent to handle a subtask.",
|
||||
"input_schema": {"type": "object", "properties": {"description": {"type": "string"}}, "required": ["description"]}},
|
||||
]
|
||||
|
||||
TOOL_HANDLERS = {
|
||||
"bash": run_bash, "read_file": run_read, "write_file": run_write,
|
||||
"edit_file": run_edit, "glob": run_glob, "task": spawn_subagent,
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# agent_loop — s09: inject memories + extract after each turn
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
|
||||
MAX_REACTIVE_RETRIES = 1
|
||||
|
||||
def agent_loop(messages: list):
|
||||
reactive_retries = 0
|
||||
# s09: inject relevant memory content into the current user turn
|
||||
memories_content = load_memories(messages)
|
||||
memory_turn = len(messages) - 1 if messages and isinstance(messages[-1].get("content"), str) else None
|
||||
# s09: build system once per user turn; memory is updated after the loop returns
|
||||
system = build_system()
|
||||
|
||||
while True:
|
||||
# s09: save pre-compression snapshot for accurate memory extraction
|
||||
pre_compress = [m if isinstance(m, dict) else {"role": m.get("role",""),
|
||||
"content": str(m.get("content",""))} for m in messages]
|
||||
|
||||
# s08: compression pipeline (budget → snip → micro)
|
||||
messages[:] = tool_result_budget(messages)
|
||||
messages[:] = snip_compact(messages)
|
||||
messages[:] = micro_compact(messages)
|
||||
|
||||
if estimate_size(messages) > CONTEXT_LIMIT:
|
||||
print("[auto compact]")
|
||||
messages[:] = compact_history(messages)
|
||||
|
||||
try:
|
||||
request_messages = messages
|
||||
if memories_content and memory_turn is not None and memory_turn < len(messages):
|
||||
request_messages = messages.copy()
|
||||
request_messages[memory_turn] = {
|
||||
**messages[memory_turn],
|
||||
"content": memories_content + "\n\n" + messages[memory_turn]["content"],
|
||||
}
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=system, messages=request_messages, tools=TOOLS, max_tokens=8000
|
||||
)
|
||||
reactive_retries = 0
|
||||
except Exception as e:
|
||||
if ("prompt_too_long" in str(e).lower() or "too many tokens" in str(e).lower()) and reactive_retries < MAX_REACTIVE_RETRIES:
|
||||
print("[reactive compact]")
|
||||
messages[:] = reactive_compact(messages)
|
||||
reactive_retries += 1
|
||||
continue
|
||||
raise
|
||||
|
||||
messages.append({"role": "assistant", "content": response.content})
|
||||
if response.stop_reason != "tool_use":
|
||||
# s09: extract from pre-compression snapshot for full fidelity
|
||||
extract_memories(pre_compress)
|
||||
consolidate_memories()
|
||||
return
|
||||
|
||||
results = []
|
||||
for block in response.content:
|
||||
if block.type != "tool_use": continue
|
||||
print(f"\033[36m> {block.name}\033[0m")
|
||||
handler = TOOL_HANDLERS.get(block.name)
|
||||
output = handler(**block.input) if handler else f"Unknown: {block.name}"
|
||||
print(str(output)[:200])
|
||||
results.append({"type": "tool_result", "tool_use_id": block.id, "content": output})
|
||||
messages.append({"role": "user", "content": results})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("s09: Memory — persistent cross-session knowledge")
|
||||
print("输入问题,回车发送。输入 q 退出。\n")
|
||||
history = []
|
||||
while True:
|
||||
try: query = input("\033[36ms09 >> \033[0m")
|
||||
except (EOFError, KeyboardInterrupt): break
|
||||
if query.strip().lower() in ("q", "exit", ""): break
|
||||
history.append({"role": "user", "content": query})
|
||||
agent_loop(history)
|
||||
for block in history[-1]["content"]:
|
||||
if getattr(block, "type", None) == "text": print(block.text)
|
||||
print()
|
||||
@@ -0,0 +1,104 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 430" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="430" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Memory — Memory loading, extraction, and consolidation on s08 compression pipeline</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s08 preserved</text>
|
||||
<rect x="160" y="56" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="178" y="66" fill="#7c3aed" font-size="10" font-weight="600">s09 new</text>
|
||||
|
||||
<!-- ===== messages[] ===== -->
|
||||
<rect x="30" y="96" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="80" y="126" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- arrow → compression -->
|
||||
<line x1="130" y1="122" x2="152" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== Compression pipeline (s08) ===== -->
|
||||
<rect x="155" y="86" width="135" height="72" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="222" y="108" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">Compression</text>
|
||||
<text x="222" y="124" fill="#64748b" font-size="9" text-anchor="middle">budget → snip → micro</text>
|
||||
<text x="222" y="138" fill="#64748b" font-size="9" text-anchor="middle">→ autoCompact</text>
|
||||
<text x="222" y="152" fill="#94a3b8" font-size="8" text-anchor="middle">(s08)</text>
|
||||
|
||||
<!-- arrow → Loading (purple) -->
|
||||
<line x1="290" y1="122" x2="317" y2="122" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- ===== Loading (s09) ===== -->
|
||||
<rect x="320" y="86" width="120" height="72" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="380" y="108" fill="#5b21b6" font-size="11" font-weight="700" text-anchor="middle">Loading</text>
|
||||
<text x="380" y="124" fill="#7c3aed" font-size="9" text-anchor="middle">LLM side-query select</text>
|
||||
<text x="380" y="138" fill="#7c3aed" font-size="9" text-anchor="middle">inject file contents</text>
|
||||
<text x="380" y="152" fill="#a78bfa" font-size="8" text-anchor="middle">≤ 5 items</text>
|
||||
|
||||
<!-- arrow → LLM -->
|
||||
<line x1="440" y1="122" x2="472" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== LLM (s08) ===== -->
|
||||
<rect x="475" y="96" width="80" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="515" y="114" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="515" y="132" fill="#64748b" font-size="9" text-anchor="middle">stop_reason</text>
|
||||
<text x="515" y="144" fill="#64748b" font-size="9" text-anchor="middle">=tool_use?</text>
|
||||
|
||||
<!-- LLM → no → return result -->
|
||||
<line x1="515" y1="148" x2="515" y2="178" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="528" y="168" fill="#16a34a" font-size="9" font-weight="600">no, stop</text>
|
||||
<rect x="460" y="180" width="110" height="24" rx="12" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="515" y="196" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">return result</text>
|
||||
|
||||
<!-- LLM → yes → TOOL_HANDLERS -->
|
||||
<line x1="555" y1="122" x2="587" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="568" y="114" fill="#64748b" font-size="9" font-weight="600">yes</text>
|
||||
|
||||
<!-- ===== TOOL_HANDLERS (s08) ===== -->
|
||||
<rect x="590" y="88" width="130" height="68" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="655" y="112" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
|
||||
<text x="655" y="128" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
|
||||
<text x="655" y="142" fill="#94a3b8" font-size="8" text-anchor="middle">edit · glob · task</text>
|
||||
|
||||
<!-- ===== Memory Files (s09) ===== -->
|
||||
<rect x="155" y="232" width="430" height="36" rx="6" fill="#faf5ff" stroke="#7c3aed" stroke-width="1.5" stroke-dasharray="4,2"/>
|
||||
<text x="370" y="255" fill="#5b21b6" font-size="11" font-weight="600" text-anchor="middle">.memory/ — MEMORY.md index + *.md files (cross-session persistent)</text>
|
||||
|
||||
<!-- Arrow: Memory Files → Loading -->
|
||||
<path d="M 395 232 L 395 162" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
<text x="408" y="200" fill="#7c3aed" font-size="9">read</text>
|
||||
|
||||
<!-- Arrow: return result → Extraction → Memory Files -->
|
||||
<path d="M 515 204 L 515 232" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
<text x="528" y="222" fill="#7c3aed" font-size="9">Extraction (after each turn)</text>
|
||||
|
||||
<!-- Consolidation note -->
|
||||
<text x="222" y="284" fill="#a78bfa" font-size="9">Consolidation: triggers at ≥ 10 files, dedup·merge·prune</text>
|
||||
|
||||
<!-- ===== Loop back ===== -->
|
||||
<path d="M 720 122 L 748 122 Q 756 122 756 130 L 756 310 Q 756 318 748 318 L 88 318 Q 80 318 80 310 L 80 148" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<text x="400" y="340" fill="#64748b" font-size="10" text-anchor="middle">tool results → messages[] → compress → load memories → LLM → extract after each turn</text>
|
||||
|
||||
<!-- ===== Bottom notes ===== -->
|
||||
<rect x="40" y="358" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="60" y="372" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="382" fill="#475569" font-size="10">s08 preserved: compression pipeline (budget → snip → micro → auto) + emergency trim + loop</text>
|
||||
<rect x="60" y="392" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="80" y="402" fill="#475569" font-size="10">s09 new: Loading (index in SYSTEM + on-demand inject) + Extraction (after each turn) + Consolidation (threshold)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,104 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 430" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="430" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Memory — s08 圧縮パイプラインに記憶の読み込み・抽出・整理を挿入</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s08 維持</text>
|
||||
<rect x="130" y="56" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="148" y="66" fill="#7c3aed" font-size="10" font-weight="600">s09 追加</text>
|
||||
|
||||
<!-- ===== messages[] ===== -->
|
||||
<rect x="30" y="96" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="80" y="126" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- arrow → compression -->
|
||||
<line x1="130" y1="122" x2="152" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== Compression pipeline (s08) ===== -->
|
||||
<rect x="155" y="86" width="135" height="72" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="222" y="108" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">圧縮パイプライン</text>
|
||||
<text x="222" y="124" fill="#64748b" font-size="9" text-anchor="middle">budget → snip → micro</text>
|
||||
<text x="222" y="138" fill="#64748b" font-size="9" text-anchor="middle">→ autoCompact</text>
|
||||
<text x="222" y="152" fill="#94a3b8" font-size="8" text-anchor="middle">(s08)</text>
|
||||
|
||||
<!-- arrow → Loading (purple) -->
|
||||
<line x1="290" y1="122" x2="317" y2="122" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- ===== Loading (s09) ===== -->
|
||||
<rect x="320" y="86" width="120" height="72" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="380" y="108" fill="#5b21b6" font-size="11" font-weight="700" text-anchor="middle">Loading</text>
|
||||
<text x="380" y="124" fill="#7c3aed" font-size="9" text-anchor="middle">LLM side-query 選択</text>
|
||||
<text x="380" y="138" fill="#7c3aed" font-size="9" text-anchor="middle">ファイル内容を注入</text>
|
||||
<text x="380" y="152" fill="#a78bfa" font-size="8" text-anchor="middle">≤ 5 件</text>
|
||||
|
||||
<!-- arrow → LLM -->
|
||||
<line x1="440" y1="122" x2="472" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== LLM (s08) ===== -->
|
||||
<rect x="475" y="96" width="80" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="515" y="114" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="515" y="132" fill="#64748b" font-size="9" text-anchor="middle">stop_reason</text>
|
||||
<text x="515" y="144" fill="#64748b" font-size="9" text-anchor="middle">=tool_use?</text>
|
||||
|
||||
<!-- LLM → no → return result -->
|
||||
<line x1="515" y1="148" x2="515" y2="178" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="528" y="168" fill="#16a34a" font-size="9" font-weight="600">なし、停止</text>
|
||||
<rect x="460" y="180" width="110" height="24" rx="12" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="515" y="196" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">結果を返す</text>
|
||||
|
||||
<!-- LLM → yes → TOOL_HANDLERS -->
|
||||
<line x1="555" y1="122" x2="587" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="568" y="114" fill="#64748b" font-size="9" font-weight="600">あり</text>
|
||||
|
||||
<!-- ===== TOOL_HANDLERS (s08) ===== -->
|
||||
<rect x="590" y="88" width="130" height="68" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="655" y="112" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
|
||||
<text x="655" y="128" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
|
||||
<text x="655" y="142" fill="#94a3b8" font-size="8" text-anchor="middle">edit · glob · task</text>
|
||||
|
||||
<!-- ===== Memory Files (s09) ===== -->
|
||||
<rect x="155" y="232" width="430" height="36" rx="6" fill="#faf5ff" stroke="#7c3aed" stroke-width="1.5" stroke-dasharray="4,2"/>
|
||||
<text x="370" y="255" fill="#5b21b6" font-size="11" font-weight="600" text-anchor="middle">.memory/ — MEMORY.md インデックス + *.md ファイル(セッション間永続化)</text>
|
||||
|
||||
<!-- Arrow: Memory Files → Loading -->
|
||||
<path d="M 395 232 L 395 162" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
<text x="408" y="200" fill="#7c3aed" font-size="9">読み込み</text>
|
||||
|
||||
<!-- Arrow: return result → Extraction → Memory Files -->
|
||||
<path d="M 515 204 L 515 232" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
<text x="528" y="222" fill="#7c3aed" font-size="9">Extraction(毎ターン終了後)</text>
|
||||
|
||||
<!-- Consolidation note -->
|
||||
<text x="222" y="284" fill="#a78bfa" font-size="9">Consolidation: ファイル ≥ 10 でトリガー、重複排除・統合・剪定</text>
|
||||
|
||||
<!-- ===== Loop back ===== -->
|
||||
<path d="M 720 122 L 748 122 Q 756 122 756 130 L 756 310 Q 756 318 748 318 L 88 318 Q 80 318 80 310 L 80 148" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<text x="400" y="340" fill="#64748b" font-size="10" text-anchor="middle">ツール結果 → messages[] → 圧縮 → 記憶読み込み → LLM → 毎ターン終了後に抽出</text>
|
||||
|
||||
<!-- ===== Bottom notes ===== -->
|
||||
<rect x="40" y="358" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="60" y="372" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="382" fill="#475569" font-size="10">s08 維持:圧縮パイプライン(budget → snip → micro → auto)+ 緊急トリム + ループ</text>
|
||||
<rect x="60" y="392" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="80" y="402" fill="#475569" font-size="10">s09 追加:Loading(インデックス常駐 + オンデマンド注入)+ Extraction(毎ターン終了後)+ Consolidation(閾値トリガー)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.2 KiB |
@@ -0,0 +1,104 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 760 430" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#555"/>
|
||||
</marker>
|
||||
<marker id="arrow-purple" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
<marker id="arrow-green" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#16a34a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="760" height="430" fill="#fafbfc" rx="8"/>
|
||||
|
||||
<!-- Title -->
|
||||
<rect x="0" y="0" width="760" height="44" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="36" width="760" height="8" fill="url(#header)"/>
|
||||
<text x="380" y="28" fill="#fff" font-size="15" font-weight="700" text-anchor="middle">Memory — 在 s08 压缩管线上,插入记忆加载、提取与整理</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<rect x="40" y="56" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="58" y="66" fill="#2563eb" font-size="10" font-weight="600">s08 保留</text>
|
||||
<rect x="140" y="56" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="158" y="66" fill="#7c3aed" font-size="10" font-weight="600">s09 新增</text>
|
||||
|
||||
<!-- ===== messages[] ===== -->
|
||||
<rect x="30" y="96" width="100" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="80" y="126" fill="#1e3a5f" font-size="12" font-weight="600" text-anchor="middle">messages[]</text>
|
||||
|
||||
<!-- arrow → compression -->
|
||||
<line x1="130" y1="122" x2="152" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== Compression pipeline (s08) ===== -->
|
||||
<rect x="155" y="86" width="135" height="72" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="222" y="108" fill="#1e3a5f" font-size="11" font-weight="700" text-anchor="middle">压缩管线</text>
|
||||
<text x="222" y="124" fill="#64748b" font-size="9" text-anchor="middle">budget → snip → micro</text>
|
||||
<text x="222" y="138" fill="#64748b" font-size="9" text-anchor="middle">→ autoCompact</text>
|
||||
<text x="222" y="152" fill="#94a3b8" font-size="8" text-anchor="middle">(s08)</text>
|
||||
|
||||
<!-- arrow → Loading (purple) -->
|
||||
<line x1="290" y1="122" x2="317" y2="122" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
|
||||
<!-- ===== Loading (s09) ===== -->
|
||||
<rect x="320" y="86" width="120" height="72" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="380" y="108" fill="#5b21b6" font-size="11" font-weight="700" text-anchor="middle">Loading</text>
|
||||
<text x="380" y="124" fill="#7c3aed" font-size="9" text-anchor="middle">LLM side-query 选文件</text>
|
||||
<text x="380" y="138" fill="#7c3aed" font-size="9" text-anchor="middle">注入文件内容</text>
|
||||
<text x="380" y="152" fill="#a78bfa" font-size="8" text-anchor="middle">≤ 5 条</text>
|
||||
|
||||
<!-- arrow → LLM -->
|
||||
<line x1="440" y1="122" x2="472" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- ===== LLM (s08) ===== -->
|
||||
<rect x="475" y="96" width="80" height="52" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="515" y="114" fill="#1e3a5f" font-size="14" font-weight="700" text-anchor="middle">LLM</text>
|
||||
<text x="515" y="132" fill="#64748b" font-size="9" text-anchor="middle">stop_reason</text>
|
||||
<text x="515" y="144" fill="#64748b" font-size="9" text-anchor="middle">=tool_use?</text>
|
||||
|
||||
<!-- LLM → 否 → 返回结果 -->
|
||||
<line x1="515" y1="148" x2="515" y2="178" stroke="#16a34a" stroke-width="1.5" marker-end="url(#arrow-green)"/>
|
||||
<text x="528" y="168" fill="#16a34a" font-size="9" font-weight="600">否,停止</text>
|
||||
<rect x="460" y="180" width="110" height="24" rx="12" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
|
||||
<text x="515" y="196" fill="#166534" font-size="10" font-weight="600" text-anchor="middle">返回结果</text>
|
||||
|
||||
<!-- LLM → 是 → TOOL_HANDLERS -->
|
||||
<line x1="555" y1="122" x2="587" y2="122" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="568" y="114" fill="#64748b" font-size="9" font-weight="600">是</text>
|
||||
|
||||
<!-- ===== TOOL_HANDLERS (s08) ===== -->
|
||||
<rect x="590" y="88" width="130" height="68" rx="8" fill="#f0f4ff" stroke="#2563eb" stroke-width="1.5"/>
|
||||
<text x="655" y="112" fill="#1e3a5f" font-size="10" font-weight="600" text-anchor="middle">TOOL_HANDLERS</text>
|
||||
<text x="655" y="128" fill="#64748b" font-size="9" text-anchor="middle">bash · read · write</text>
|
||||
<text x="655" y="142" fill="#94a3b8" font-size="8" text-anchor="middle">edit · glob · task</text>
|
||||
|
||||
<!-- ===== Memory Files (s09) ===== -->
|
||||
<rect x="155" y="232" width="430" height="36" rx="6" fill="#faf5ff" stroke="#7c3aed" stroke-width="1.5" stroke-dasharray="4,2"/>
|
||||
<text x="370" y="255" fill="#5b21b6" font-size="11" font-weight="600" text-anchor="middle">.memory/ — MEMORY.md 索引 + *.md 文件(跨会话持久化)</text>
|
||||
|
||||
<!-- Arrow: Memory Files → Loading -->
|
||||
<path d="M 395 232 L 395 162" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
<text x="408" y="200" fill="#7c3aed" font-size="9">读取</text>
|
||||
|
||||
<!-- Arrow: 返回结果 → Extraction → Memory Files -->
|
||||
<path d="M 515 204 L 515 232" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow-purple)"/>
|
||||
<text x="528" y="222" fill="#7c3aed" font-size="9">Extraction(每轮结束后)</text>
|
||||
|
||||
<!-- Consolidation note -->
|
||||
<text x="222" y="284" fill="#a78bfa" font-size="9">Consolidation: 文件数 ≥ 10 时触发,去重·合并·剪枝</text>
|
||||
|
||||
<!-- ===== Loop back ===== -->
|
||||
<path d="M 720 122 L 748 122 Q 756 122 756 130 L 756 310 Q 756 318 748 318 L 88 318 Q 80 318 80 310 L 80 148" fill="none" stroke="#555" stroke-width="1.5" marker-end="url(#arrow)" stroke-dasharray="6,3"/>
|
||||
<text x="400" y="340" fill="#64748b" font-size="10" text-anchor="middle">工具结果追加到 messages[] → 压缩 → 加载记忆 → LLM → 每轮结束后提取</text>
|
||||
|
||||
<!-- ===== Bottom notes ===== -->
|
||||
<rect x="40" y="358" width="680" height="56" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<rect x="60" y="372" width="12" height="10" rx="2" fill="#f0f4ff" stroke="#2563eb" stroke-width="1"/>
|
||||
<text x="80" y="382" fill="#475569" font-size="10">s08 保留:压缩管线(budget → snip → micro → auto)+ 应急裁剪 + 循环</text>
|
||||
<rect x="60" y="392" width="12" height="10" rx="2" fill="#f3e8ff" stroke="#7c3aed" stroke-width="1"/>
|
||||
<text x="80" y="402" fill="#475569" font-size="10">s09 新增:Loading(索引常驻 + 按需注入)+ Extraction(每轮结束后)+ Consolidation(阈值触发)</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 7.0 KiB |
@@ -0,0 +1,78 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 380" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="380" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Memory System — Store · Load · Extract · Consolidate</text>
|
||||
|
||||
<!-- Storage -->
|
||||
<rect x="40" y="58" width="145" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="112" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Storage</text>
|
||||
<line x1="55" y1="90" x2="170" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="55" y="108" fill="#5b21b6" font-size="10">.memory/*.md files</text>
|
||||
<text x="55" y="124" fill="#5b21b6" font-size="10">MEMORY.md index</text>
|
||||
|
||||
<line x1="190" y1="98" x2="218" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Loading -->
|
||||
<rect x="222" y="58" width="200" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="322" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Load</text>
|
||||
<line x1="237" y1="90" x2="407" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="237" y="108" fill="#5b21b6" font-size="10">Index in SYSTEM (always)</text>
|
||||
<text x="237" y="124" fill="#5b21b6" font-size="10">LLM side-query select files</text>
|
||||
<text x="237" y="134" fill="#a78bfa" font-size="9">≤ 5 items, fallback to keyword</text>
|
||||
|
||||
<line x1="425" y1="98" x2="453" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- Extraction -->
|
||||
<rect x="457" y="58" width="130" height="80" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="522" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">Extract</text>
|
||||
<line x1="472" y1="90" x2="572" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="472" y="106" fill="#5b21b6" font-size="9.5">After each turn</text>
|
||||
<text x="472" y="121" fill="#5b21b6" font-size="9.5">Extract prefs</text>
|
||||
<text x="472" y="134" fill="#a78bfa" font-size="8.5">Avoid duplicates</text>
|
||||
|
||||
<!-- Consolidation -->
|
||||
<rect x="600" y="58" width="100" height="80" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="650" y="80" fill="#5b21b6" font-size="12" font-weight="700" text-anchor="middle">Consolidate</text>
|
||||
<line x1="615" y1="90" x2="685" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="615" y="106" fill="#5b21b6" font-size="9.5">≥ 10 files</text>
|
||||
<text x="615" y="121" fill="#5b21b6" font-size="9.5">Dedup · merge</text>
|
||||
<text x="615" y="134" fill="#a78bfa" font-size="8.5">CC: gated Dream</text>
|
||||
|
||||
<!-- Memory Files -->
|
||||
<rect x="40" y="180" width="660" height="36" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="370" y="203" fill="#475569" font-size="11" text-anchor="middle">.memory/ — MEMORY.md index + *.md files (YAML frontmatter: name / description / type)</text>
|
||||
|
||||
<!-- Arrow: Storage → Memory Files -->
|
||||
<path d="M 112 138 L 112 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="120" y="162" fill="#7c3aed" font-size="9">read/write</text>
|
||||
|
||||
<!-- Arrow: Extraction → Memory Files -->
|
||||
<path d="M 522 138 L 522 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="536" y="164" fill="#7c3aed" font-size="9">write</text>
|
||||
|
||||
<!-- Arrow: Consolidation → Memory Files -->
|
||||
<path d="M 650 138 L 650 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="662" y="164" fill="#7c3aed" font-size="9">overwrite</text>
|
||||
|
||||
<!-- Four types -->
|
||||
<rect x="40" y="240" width="660" height="40" rx="6" fill="#faf5ff" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="60" y="260" fill="#5b21b6" font-size="10" font-weight="600">Four types:</text>
|
||||
<text x="140" y="260" fill="#475569" font-size="10">user (who you are) · feedback (how to work) · project (what's happening) · reference (where to find things)</text>
|
||||
|
||||
<!-- CC source comparison -->
|
||||
<rect x="40" y="296" width="660" height="72" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="60" y="316" fill="#5b21b6" font-size="11" font-weight="600">CC Source Comparison</text>
|
||||
<text x="60" y="334" fill="#475569" font-size="10">• Selection: LLM side-query (Sonnet selects), not embedding vector similarity</text>
|
||||
<text x="60" y="350" fill="#475569" font-size="10">• Extraction timing: stop hook (after each turn ends), not after autoCompact</text>
|
||||
<text x="60" y="365" fill="#475569" font-size="10">• Dream: time + sessions + file lock, not simple count</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.1 KiB |
@@ -0,0 +1,78 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 380" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="380" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Memory System — ストレージ · 読み込み · 抽出 · 整理</text>
|
||||
|
||||
<!-- ストレージ -->
|
||||
<rect x="40" y="58" width="145" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="112" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">ストレージ</text>
|
||||
<line x1="55" y1="90" x2="170" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="55" y="108" fill="#5b21b6" font-size="10">.memory/*.md ファイル</text>
|
||||
<text x="55" y="124" fill="#5b21b6" font-size="10">MEMORY.md インデックス</text>
|
||||
|
||||
<line x1="190" y1="98" x2="218" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- 読み込み -->
|
||||
<rect x="222" y="58" width="200" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="322" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">読み込み</text>
|
||||
<line x1="237" y1="90" x2="407" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="237" y="108" fill="#5b21b6" font-size="10">インデックスを SYSTEM に常駐</text>
|
||||
<text x="237" y="124" fill="#5b21b6" font-size="10">LLM side-query でファイル選択</text>
|
||||
<text x="237" y="134" fill="#a78bfa" font-size="9">≤ 5 件、失敗時はキーワードに降格</text>
|
||||
|
||||
<line x1="425" y1="98" x2="453" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- 抽出 -->
|
||||
<rect x="457" y="58" width="130" height="80" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="522" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">抽出</text>
|
||||
<line x1="472" y1="90" x2="572" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="472" y="106" fill="#5b21b6" font-size="9.5">毎ターン終了後</text>
|
||||
<text x="472" y="121" fill="#5b21b6" font-size="9.5">好み/制約を抽出</text>
|
||||
<text x="472" y="134" fill="#a78bfa" font-size="8.5">重複を回避</text>
|
||||
|
||||
<!-- 整理 -->
|
||||
<rect x="600" y="58" width="100" height="80" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="650" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">整理</text>
|
||||
<line x1="615" y1="90" x2="685" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="615" y="106" fill="#5b21b6" font-size="9.5">≥ 10 ファイル</text>
|
||||
<text x="615" y="121" fill="#5b21b6" font-size="9.5">重複排除・統合</text>
|
||||
<text x="615" y="134" fill="#a78bfa" font-size="8.5">CC: Dream ゲート</text>
|
||||
|
||||
<!-- Memory Files -->
|
||||
<rect x="40" y="180" width="660" height="36" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="370" y="203" fill="#475569" font-size="11" text-anchor="middle">.memory/ — MEMORY.md インデックス + *.md ファイル(YAML frontmatter: name / description / type)</text>
|
||||
|
||||
<!-- Arrow: ストレージ → Memory Files -->
|
||||
<path d="M 112 138 L 112 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="120" y="162" fill="#7c3aed" font-size="9">読み/書き</text>
|
||||
|
||||
<!-- Arrow: 抽出 → Memory Files -->
|
||||
<path d="M 522 138 L 522 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="536" y="164" fill="#7c3aed" font-size="9">書き込み</text>
|
||||
|
||||
<!-- Arrow: 整理 → Memory Files -->
|
||||
<path d="M 650 138 L 650 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="662" y="164" fill="#7c3aed" font-size="9">上書き</text>
|
||||
|
||||
<!-- 4 種類の記憶 -->
|
||||
<rect x="40" y="240" width="660" height="40" rx="6" fill="#faf5ff" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="60" y="260" fill="#5b21b6" font-size="10" font-weight="600">4 種類の記憶:</text>
|
||||
<text x="148" y="260" fill="#475569" font-size="10">user(あなたは誰か)· feedback(どう作業するか)· project(何が起きているか)· reference(どこで探すか)</text>
|
||||
|
||||
<!-- CC ソースコード対照 -->
|
||||
<rect x="40" y="296" width="660" height="72" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="60" y="316" fill="#5b21b6" font-size="11" font-weight="600">CC ソースコード対照</text>
|
||||
<text x="60" y="334" fill="#475569" font-size="10">• 記憶選択:LLM side-query(Sonnet が選択)、embedding ベクトル類似度ではない</text>
|
||||
<text x="60" y="350" fill="#475569" font-size="10">• 抽出タイミング:stop hook(毎ターン終了後)、autoCompact 後ではない</text>
|
||||
<text x="60" y="365" fill="#475569" font-size="10">• Dream:時間・セッション・ロックで判定</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,78 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 380" font-family="system-ui, -apple-system, sans-serif">
|
||||
<defs>
|
||||
<linearGradient id="header" x1="0" y1="0" x2="1" y2="0">
|
||||
<stop offset="0%" stop-color="#1e3a5f"/><stop offset="100%" stop-color="#7c3aed"/>
|
||||
</linearGradient>
|
||||
<marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
|
||||
<path d="M 0 0 L 10 5 L 0 10 z" fill="#7c3aed"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="720" height="380" fill="#fafbfc" rx="8"/>
|
||||
<rect x="0" y="0" width="720" height="38" fill="url(#header)" rx="8"/>
|
||||
<rect x="0" y="30" width="720" height="8" fill="url(#header)"/>
|
||||
<text x="360" y="25" fill="#fff" font-size="14" font-weight="700" text-anchor="middle">Memory System — 存储 · 加载 · 提取 · 整理</text>
|
||||
|
||||
<!-- 存储 -->
|
||||
<rect x="40" y="58" width="145" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="112" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">存储</text>
|
||||
<line x1="55" y1="90" x2="170" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="55" y="108" fill="#5b21b6" font-size="10">.memory/*.md 文件</text>
|
||||
<text x="55" y="124" fill="#5b21b6" font-size="10">MEMORY.md 索引</text>
|
||||
|
||||
<line x1="190" y1="98" x2="218" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- 加载 -->
|
||||
<rect x="222" y="58" width="200" height="80" rx="8" fill="#ede9fe" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="322" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">加载</text>
|
||||
<line x1="237" y1="90" x2="407" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="237" y="108" fill="#5b21b6" font-size="10">索引常驻 SYSTEM</text>
|
||||
<text x="237" y="124" fill="#5b21b6" font-size="10">LLM side-query 选文件</text>
|
||||
<text x="237" y="134" fill="#a78bfa" font-size="9">≤ 5 条,失败降级到关键词</text>
|
||||
|
||||
<line x1="425" y1="98" x2="453" y2="98" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
|
||||
<!-- 提取 -->
|
||||
<rect x="457" y="58" width="130" height="80" rx="8" fill="#f3e8ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="522" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">提取</text>
|
||||
<line x1="472" y1="90" x2="572" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="472" y="108" fill="#5b21b6" font-size="10">每轮结束后触发</text>
|
||||
<text x="472" y="124" fill="#5b21b6" font-size="10">LLM 提取偏好/约束</text>
|
||||
<text x="472" y="134" fill="#a78bfa" font-size="9">检查已有,避免重复</text>
|
||||
|
||||
<!-- 整理 -->
|
||||
<rect x="600" y="58" width="100" height="80" rx="8" fill="#f5f3ff" stroke="#7c3aed" stroke-width="2"/>
|
||||
<text x="650" y="80" fill="#5b21b6" font-size="13" font-weight="700" text-anchor="middle">整理</text>
|
||||
<line x1="615" y1="90" x2="685" y2="90" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="615" y="108" fill="#5b21b6" font-size="10">文件 ≥ 10 触发</text>
|
||||
<text x="615" y="124" fill="#5b21b6" font-size="10">去重·合并·剪枝</text>
|
||||
<text x="615" y="134" fill="#a78bfa" font-size="9">CC: 三层门控</text>
|
||||
|
||||
<!-- Memory Files -->
|
||||
<rect x="40" y="180" width="660" height="36" rx="6" fill="#f8fafc" stroke="#94a3b8" stroke-width="1" stroke-dasharray="4,2"/>
|
||||
<text x="370" y="203" fill="#475569" font-size="11" text-anchor="middle">.memory/ — MEMORY.md 索引 + *.md 文件(YAML frontmatter: name / description / type)</text>
|
||||
|
||||
<!-- Arrow: 存储 → Memory Files -->
|
||||
<path d="M 112 138 L 112 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="120" y="162" fill="#7c3aed" font-size="9">写入/读取</text>
|
||||
|
||||
<!-- Arrow: Extraction → Memory Files -->
|
||||
<path d="M 522 138 L 522 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="536" y="164" fill="#7c3aed" font-size="9">写入</text>
|
||||
|
||||
<!-- Arrow: 整理 → Memory Files -->
|
||||
<path d="M 650 138 L 650 180" fill="none" stroke="#7c3aed" stroke-width="1.5" marker-end="url(#arrow)"/>
|
||||
<text x="662" y="164" fill="#7c3aed" font-size="9">覆写</text>
|
||||
|
||||
<!-- 四类记忆 -->
|
||||
<rect x="40" y="240" width="660" height="40" rx="6" fill="#faf5ff" stroke="#c4b5fd" stroke-width="0.5"/>
|
||||
<text x="60" y="260" fill="#5b21b6" font-size="10" font-weight="600">四类记忆:</text>
|
||||
<text x="140" y="260" fill="#475569" font-size="10">user(你是谁)· feedback(怎么做事)· project(正在发生什么)· reference(东西在哪找)</text>
|
||||
|
||||
<!-- CC 源码对照 -->
|
||||
<rect x="40" y="296" width="660" height="72" rx="6" fill="#f8fafc" stroke="#e2e8f0" stroke-width="1"/>
|
||||
<text x="60" y="316" fill="#5b21b6" font-size="11" font-weight="600">CC 源码对照</text>
|
||||
<text x="60" y="334" fill="#475569" font-size="10">• 记忆选择:LLM side-query(Sonnet 选),不是 embedding 向量相似度</text>
|
||||
<text x="60" y="350" fill="#475569" font-size="10">• 提取时机:stop hook 中触发(每轮结束后),不是 autoCompact 后</text>
|
||||
<text x="60" y="366" fill="#475569" font-size="10">• Dream 整理:三层门控(时间 ≥ 24h + 会话 ≥ 5 + 文件锁),不是简单计数</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
Reference in New Issue
Block a user