123 lines
7.0 KiB
Markdown
123 lines
7.0 KiB
Markdown
# Реализация: Thread-scoped memory + custom memory prompt
|
||
|
||
**Статус:** Реализовано ✅
|
||
|
||
## Изменённые файлы
|
||
|
||
### 1. `tools/memory_tool.py` — MemoryStore с thread_key
|
||
|
||
**`__init__`** — новый параметр `thread_key: Optional[str] = None`, сохраняется как `self.thread_key`.
|
||
|
||
**`_path_for(target)`** — теперь instance method (был static):
|
||
- `target == "user"` → всегда `memories/USER.md`
|
||
- `target == "memory"` и `self.thread_key` задан → `memories/threads/<key>/MEMORY.md`
|
||
- `target == "memory"` без thread_key → `memories/MEMORY.md` (глобальный fallback)
|
||
|
||
**`load_from_disk()`** — использует `self._path_for("memory")` и `self._path_for("user")` вместо хардкода.
|
||
|
||
### 2. `agent/agent_init.py` — конфиг + проброс
|
||
|
||
Новые поля на агенте:
|
||
- `_memory_thread_scoped` — читается из `config.yaml: memory.thread_scoped`
|
||
- `_memory_instruction` — читается из `config.yaml: memory.prompt_path` (.md файл)
|
||
|
||
MemoryStore создаётся с `thread_key=_gateway_session_key` если `thread_scoped: true`.
|
||
|
||
Кастомная memory instruction логируется: `Loaded custom memory instruction from ...`.
|
||
|
||
### 3. `agent/system_prompt.py` — вставка в system prompt
|
||
|
||
В блок MEMORY добавляется:
|
||
- Заголовок `MEMORY for thread: <gateway_session_key>` (вместо `MEMORY (your personal notes)`) когда thread_scoped включён и есть ключ.
|
||
|
||
После memory блока вставляется отдельный блок `MEMORY INSTRUCTION` (с опциональным `for thread: <key>`), содержащий кастомную инструкцию из .md файла.
|
||
|
||
### 4. `gateway/run.py` — уже пробрасывает
|
||
|
||
`gateway_session_key` уже передаётся в `AIAgent.__init__` на строке ~17828. Никаких изменений не потребовалось.
|
||
|
||
### 5. `run_agent.py` — уже принимает
|
||
|
||
Параметр `gateway_session_key` уже есть в `AIAgent.__init__`. Пробрасывается в `init_agent()` где записывается как `agent._gateway_session_key`.
|
||
|
||
## Конфиг (Whale)
|
||
|
||
```yaml
|
||
memory:
|
||
...
|
||
thread_scoped: true
|
||
prompt_path: ~/.hermes/hermes-whale/review/memory_prompt.md
|
||
```
|
||
|
||
## Файлы
|
||
|
||
- **`~/.hermes/hermes-whale/review/memory_prompt.md`** — инструкция что запоминать (документы, команды, конфиги, статус проекта, решения).
|
||
- После добавления нового пункта в список patch() не перенумеровывает — нужен второй clean patch.
|
||
- **2026-06-24:** Добавлен пункт 2 — после загрузки Obsidian docs (skill_view, mcp_obsidian_read_note) извлекать ключевые факты в memory.
|
||
|
||
## Файловая структура на диске
|
||
|
||
```
|
||
~/.hermes/hermes-whale/memories/
|
||
├── MEMORY.md # глобальная (fallback для CLI/старых сессий)
|
||
├── USER.md # глобальная (всегда)
|
||
└── threads/
|
||
├── agent:main:webhook:webhook:webhook:whale/
|
||
│ └── MEMORY.md # память Whale
|
||
├── agent:main:zulip:stream:general:thread:123/
|
||
│ └── MEMORY.md # память конкретного треда
|
||
└── ...
|
||
```
|
||
|
||
## Коммиты
|
||
|
||
- `hermes-agent`: `98cb69b50` — feat: thread-scoped memory + configurable memory instruction
|
||
- `hermes-agent`: `4ebad4f69` — test: thread-scoped memory persistence, drift guard, snapshot, sanitization (+9 тестов, 142 строки)
|
||
- `hermes-whale`: `736f8f3` — whale: enable thread-scoped memory and custom memory instruction
|
||
|
||
## Тесты
|
||
|
||
9 тестов в `tests/tools/test_memory_tool.py` (всего 76 в файле, 76/76 passed):
|
||
|
||
**Persistence:**
|
||
- `test_thread_scoped_memory_writes_separate_file` — global и thread пишутся в разные файлы
|
||
- `test_user_stays_global_with_thread_key` — USER.md всегда глобальный, не залезает в `threads/`
|
||
- `test_thread_and_global_are_independent_on_load` — загрузка thread не видит global entries и vice versa
|
||
- `test_thread_key_none_falls_back_to_global` — backward compat: без thread_key пишет в `memories/MEMORY.md`
|
||
|
||
**Snapshot:**
|
||
- `test_snapshot_reflects_thread_scoped_path` — `format_for_system_prompt` берёт данные из thread-файла
|
||
- `test_snapshot_from_thread_and_global_are_independent` — thread snapshot изолирован от global
|
||
|
||
**Drift guard:**
|
||
- `test_drift_guard_with_thread_key` — `_detect_external_drift` работает на thread-scoped MEMORY.md
|
||
|
||
**Sanitization:**
|
||
- `test_load_time_sanitization_with_thread_key` — poisoned entry в thread блокируется на уровне snapshot
|
||
|
||
**Pitfalls:**
|
||
- `pytest-timeout` плагин не установлен, но `pyproject.toml` содержит `addopts = "--timeout=30"`. Запуск падает с `unrecognized arguments`. Используй `-o "addopts="` для override.
|
||
- Drift guard на thread: нужен блок > `memory_char_limit` (дефолт 2200), иначе `_detect_external_drift` не находит entry-size overflow. В тесте `"x" * 2300`.
|
||
|
||
## Тесты
|
||
|
||
- **76/76 passed** (из них 9 новых для thread_key, добавлены в `4ebad4f69`)
|
||
- **9 новых тестов:**
|
||
- `test_thread_scoped_memory_writes_separate_file` — разные файлы для global/thread
|
||
- `test_user_stays_global_with_thread_key` — USER.md не уходит в threads/
|
||
- `test_thread_and_global_are_independent_on_load` — не пересекаются при чтении
|
||
- `test_thread_key_none_falls_back_to_global` — backward compat
|
||
- `test_snapshot_reflects_thread_scoped_path` — форматирует snapshot из thread файла
|
||
- `test_snapshot_from_thread_and_global_are_independent` — не подхватывает global entry
|
||
- `test_drift_guard_with_thread_key` — детекция внешней модификации на thread файле
|
||
- `test_load_time_sanitization_with_thread_key` — poisoned entry блокируется в thread snapshot
|
||
- `test_already_blocked_entry_passes_through` — no double-wrap (расширен)
|
||
- Запуск: `cd ~/.hermes/hermes-agent && source venv/bin/activate && python -m pytest tests/tools/test_memory_tool.py -v -o "addopts="`
|
||
|
||
## Неизменённое
|
||
|
||
- `run_conversation` / `conversation_loop.py` — не трогали
|
||
- `background_review.py` — наследует `_memory_store` от родителя, thread_key приходит автоматически
|
||
- `tools/memory_tool.py` schema/MEMORY_SCHEMA — не меняли, кастомная инструкция в system prompt
|
||
- External memory providers (honcho/mem0) — не трогали
|