mirror of
https://github.com/shareAI-lab/analysis_claude_code.git
synced 2026-06-20 20:23:36 +08:00
* feat: s01-s14 docs quality overhaul — tool pipeline, single-agent, knowledge & resilience Rewrite code.py and README (zh/en/ja) for s01-s14, each chapter building incrementally on the previous. Key fixes across chapters: - s01-s04: agent loop, tool dispatch, permission pipeline, hooks - s05-s08: todo write, subagent, skill loading, context compact - s09-s11: memory system, system prompt assembly, error recovery - s12-s14: task graph, background tasks, cron scheduler All chapters CC source-verified. Code inherits fixes forward (PROMPT_SECTIONS, json.dumps cache, real-state context, can_start dep protection, etc.). * feat: s15-s19 docs quality overhaul — multi-agent platform: teams, protocols, autonomy, worktree, MCP tools Rewrite code.py and README (zh/en/ja) for s15-s19, the multi-agent platform chapters. Each chapter inherits all previous fixes and adds one mechanism: - s15: agent teams (TeamCreate, teammate threads, shared task list) - s16: team protocols (plan approval, shutdown handshake, consume_inbox) - s17: autonomous agents (idle polling, auto-claim, consume_lead_inbox) - s18: worktree isolation (git worktree, bind_task, cwd switching, safety) - s19: MCP tools (MCPClient, normalize_mcp_name, assemble_tool_pool, no cache) All appendix source code references verified against CC source. Config priority corrected: claude.ai < plugin < user < project < local. * fix: 5 regressions across s05-s19 — glob safety, todo validation, memory extraction, protocol types, dep crash - s05-s09: glob results now filter with is_relative_to(WORKDIR) (inherited from s02) - s06-s08: todo_write validates content/status required fields (inherited from s05) - s09: extract_memories uses pre-compression snapshot instead of compacted messages - s16: submit_plan docstring clarifies protocol-only (not code-level gate) - s17-s19: match_response restores type mismatch validation (from s16) - s17-s19: claim_task deps list handles missing dep files without crashing * fix: s12 Todo V2 logic reversal, s14/s15 cron range validation, s18/s19 worktree name validation - s12 README (zh/en/ja): fix Todo V2 direction — interactive defaults to Task, non-interactive/SDK defaults to TodoWrite. Fix env var name to CLAUDE_CODE_ENABLE_TASKS (not TODO_V2). - s14/s15: add _validate_cron_field with per-field range checks (minute 0-59, hour 0-23, dom 1-31, month 1-12, dow 0-6), step > 0, range lo <= hi. Replace old try/except validation that only caught exceptions. - s18/s19: add validate_worktree_name() to remove_worktree and keep_worktree, not just create_worktree. * fix: align s16-s19 teaching tool consistency * fix pr265 chapter diagrams * Add comprehensive s20 harness chapter * Fix chapter smoke test regressions * Clarify README tutorial track transition --------- Co-authored-by: Haoran <bill-billion@outlook.com>
This commit is contained in:
279
s09_memory/README.en.md
Normal file
279
s09_memory/README.en.md
Normal file
@@ -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` every turn and injects the memory catalog into the SYSTEM prompt. The index in SYSTEM can be cached by prompt cache, avoiding resending it every turn.
|
||||
|
||||
**Path 2: Relevant memories on demand.** Before each LLM call, `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 -->
|
||||
279
s09_memory/README.ja.md
Normal file
279
s09_memory/README.ja.md
Normal file
@@ -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()` は毎ターン SYSTEM を再構築する際に `MEMORY.md` を読み込み、記憶カタログを注入。SYSTEM prompt 内のインデックスは prompt cache でキャッシュ可能で、毎ターン再送不要。
|
||||
|
||||
**パス 2:関連記憶をオンデマンド注入。** 各 LLM 呼び出し前、`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 -->
|
||||
279
s09_memory/README.md
Normal file
279
s09_memory/README.md
Normal file
@@ -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 会把当前目标、剩余工作、用户约束写进摘要,但细节会丢失:"用 tab 缩进不要用空格"可能被简化成"用户有代码风格偏好"。而且新开一个会话,连摘要也没了。
|
||||
|
||||
LLM 没有持久状态,所有信息都在上下文窗口里。上下文满了要压缩,压缩就有损。需要一层不参与压缩、跨会话保留的存储。
|
||||
|
||||
---
|
||||
|
||||
## 解决方案
|
||||
|
||||

|
||||
|
||||
s08 的压缩管线保留,聚焦记忆。存储选文件系统:`.memory/` 目录下,每个记忆一个 `.md` 文件,带 YAML frontmatter(`name` / `description` / `type`)。文件多了需要索引:`MEMORY.md` 一行一个链接,注入 SYSTEM。
|
||||
|
||||
关键设计:索引常驻 SYSTEM prompt(可被 prompt cache 缓存),文件内容按需注入(按 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()` 每轮重建 SYSTEM 时读取 `MEMORY.md`,把记忆清单注入。SYSTEM prompt 中的索引可以被 prompt cache 缓存,不需要每轮重新发送。
|
||||
|
||||
**路径二:相关记忆按需注入。** 每轮调用前,`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。
|
||||
|
||||
### 写入:每轮结束后提取
|
||||
|
||||
用户不会每次都说"记住这个"。偏好通常散落在正常对话中:"用 tab 比空格好"、"以后都用单引号"。
|
||||
|
||||
`extract_memories()` 在每轮结束时运行,条件是模型停止且没有 tool_use(说明对话告一段落):
|
||||
|
||||
```python
|
||||
# In agent_loop:
|
||||
if response.stop_reason != "tool_use":
|
||||
extract_memories(messages) # 从最近对话提取新记忆
|
||||
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 -->
|
||||
616
s09_memory/code.py
Normal file
616
s09_memory/code.py
Normal file
@@ -0,0 +1,616 @@
|
||||
#!/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 = response.content[0].text.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 = response.content[0].text.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 = response.content[0].text.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."
|
||||
)
|
||||
|
||||
SYSTEM = build_system()
|
||||
|
||||
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(task: str) -> str:
|
||||
print(f"\n\033[35m[Subagent spawned]\033[0m")
|
||||
messages = [{"role": "user", "content": task}]
|
||||
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 snip_compact(msgs, mx=50):
|
||||
if len(msgs) <= mx: return msgs
|
||||
return msgs[:3] + [{"role": "user", "content": f"[snipped {len(msgs)-mx} msgs]"}] + msgs[-(mx-3):]
|
||||
|
||||
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 r.content[0].text.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)
|
||||
summary = summarize_history(msgs)
|
||||
return [{"role": "user", "content": f"[Reactive compact]\n\n{summary}"}, *msgs[-5:]]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════
|
||||
# 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
|
||||
while True:
|
||||
# s09: rebuild system with current memory index + relevant memories
|
||||
system = build_system()
|
||||
memories_content = load_memories(messages)
|
||||
if memories_content:
|
||||
system += "\n\n" + memories_content
|
||||
|
||||
# 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:
|
||||
response = client.messages.create(
|
||||
model=MODEL, system=system, messages=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()
|
||||
104
s09_memory/images/memory-overview.en.svg
Normal file
104
s09_memory/images/memory-overview.en.svg
Normal file
@@ -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 |
104
s09_memory/images/memory-overview.ja.svg
Normal file
104
s09_memory/images/memory-overview.ja.svg
Normal file
@@ -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 |
104
s09_memory/images/memory-overview.svg
Normal file
104
s09_memory/images/memory-overview.svg
Normal file
@@ -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 |
78
s09_memory/images/memory-subsystems.en.svg
Normal file
78
s09_memory/images/memory-subsystems.en.svg
Normal file
@@ -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="108" fill="#5b21b6" font-size="10">After each turn ends</text>
|
||||
<text x="472" y="124" fill="#5b21b6" font-size="10">LLM extracts prefs/constraints</text>
|
||||
<text x="472" y="134" fill="#a78bfa" font-size="9">Check existing, 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="13" 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="108" fill="#5b21b6" font-size="10">Triggers at ≥ 10 files</text>
|
||||
<text x="615" y="124" fill="#5b21b6" font-size="10">Dedup · merge · prune</text>
|
||||
<text x="615" y="134" fill="#a78bfa" font-size="9">CC: 3-layer gating</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 174 Q 112 180 118 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="366" fill="#475569" font-size="10">• Dream consolidation: 3-layer gating (time ≥ 24h + sessions ≥ 5 + file lock), not simple count</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.2 KiB |
78
s09_memory/images/memory-subsystems.ja.svg
Normal file
78
s09_memory/images/memory-subsystems.ja.svg
Normal file
@@ -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: 3 層ゲート</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 174 Q 112 180 118 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="366" fill="#475569" font-size="10">• Dream 整理:3 層ゲート(時間 ≥ 24h + セッション ≥ 5 + ファイルロック)、単純な計数ではない</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.5 KiB |
78
s09_memory/images/memory-subsystems.svg
Normal file
78
s09_memory/images/memory-subsystems.svg
Normal file
@@ -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 174 Q 112 180 118 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