[2026-06-25] taiga-vault: family/how-to/htpc-emulators-setup.md family/how-to/htpc-gaming-plans.md family/how-to/kraken-access.md family/how-to/openmediavault-rpi5.md family/how-to/time-machine.md family/how-to/wireguard-vpn.md personal/documents/todo-list.md personal/plans/extract-stable-prompt-blocks.md personal/plans/hermes-whale-system-prompt.md personal/plans/thread-scoped-memory.md
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
# План: Вынести хардкод stable-блоков system prompt в файлы
|
||||
|
||||
**Статус:** Реализовано ✅ (ждёт перезапуска webhook)
|
||||
|
||||
## Мотивация
|
||||
|
||||
В Hermes Whale все stable-блоки system prompt (identity, guidance, enforcement) захардкожены в `agent/prompt_builder.py` как Python-константы. Невозможно изменить их без редактирования исходного кода Hermes Agent.
|
||||
|
||||
## Решение
|
||||
|
||||
Вынести каждый блок в отдельный `.md` файл, добавить маппинг в `config.yaml: agent.prompt_overrides`, и модифицировать `agent/system_prompt.py`, чтобы он читал файлы вместо констант.
|
||||
|
||||
## Изменяемые файлы
|
||||
|
||||
### 1. `agent/system_prompt.py` — замена констант на file-load
|
||||
|
||||
Добавлена функция `_load_prompt_block(agent, block_name, default_text)` — строки 51-71 в `/Users/admin/.hermes/hermes-agent/agent/system_prompt.py`:
|
||||
- Читает `agent._prompt_overrides` (берётся из конфига на старте)
|
||||
- Если для `block_name` указан путь — читает файл, возвращает его содержимое
|
||||
- Иначе возвращает `default_text`
|
||||
|
||||
Заменены все 7 прямых ссылок на константы в `build_system_prompt_parts()` на вызовы `_load_prompt_block()`.
|
||||
|
||||
Добавлены импорты: `import logging`, `import os`, `from pathlib import Path`, `logger = logging.getLogger(__name__)`.
|
||||
|
||||
**Константы, которые заменяются (7 блоков):**
|
||||
|
||||
| Блок | Константа | Вставляется при условии |
|
||||
|------|-----------|------------------------|
|
||||
| `hermes_help` | `HERMES_AGENT_HELP_GUIDANCE` | всегда |
|
||||
| `task_completion` | `TASK_COMPLETION_GUIDANCE` | всегда |
|
||||
| `memory_guidance` | `MEMORY_GUIDANCE` | когда есть tool "memory" |
|
||||
| `session_search_guidance` | `SESSION_SEARCH_GUIDANCE` | когда есть tool "session_search" |
|
||||
| `skills_guidance` | `SKILLS_GUIDANCE` | когда есть tool "skill_manage" |
|
||||
| `tool_use_enforcement` | `TOOL_USE_ENFORCEMENT_GUIDANCE` | зависит от модели |
|
||||
| `execution_discipline` | `OPENAI_MODEL_EXECUTION_GUIDANCE` | зависит от модели |
|
||||
|
||||
**Не заменяется (остаётся в коде):**
|
||||
- `DEFAULT_AGENT_IDENTITY` — это fallback когда нет SOUL.md (у нас есть SOUL.md, не нужно)
|
||||
- `GOOGLE_MODEL_OPERATIONAL_GUIDANCE` — Google-specific, неактуально для Whale
|
||||
- `COMPUTER_USE_GUIDANCE` — нет toolset
|
||||
- `KANBAN_GUIDANCE` — нет kanban
|
||||
- `PLATFORM_HINTS` — platform-specific, другая логика
|
||||
|
||||
### 2. `agent/system_prompt.py` — добавить функцию загрузки
|
||||
|
||||
```python
|
||||
def _load_prompt_block(agent, block_name: str, default: str) -> str:
|
||||
"""Load a prompt block from a file if configured, else return default."""
|
||||
overrides = getattr(agent, "_prompt_overrides", None) or {}
|
||||
path = overrides.get(block_name)
|
||||
if path:
|
||||
try:
|
||||
resolved = os.path.expanduser(path)
|
||||
content = Path(resolved).read_text(encoding="utf-8").strip()
|
||||
if content:
|
||||
return content
|
||||
except Exception:
|
||||
logger.debug("Could not load prompt override '%s' from %s", block_name, path)
|
||||
return default
|
||||
```
|
||||
|
||||
### 3. `agent/agent_init.py` — пробросить конфиг
|
||||
|
||||
После загрузки `_agent_cfg` (строка ~1058) добавлено чтение `agent.prompt_overrides`:
|
||||
|
||||
```python
|
||||
agent._prompt_overrides = {}
|
||||
try:
|
||||
_po = _agent_cfg.get("agent", {}).get("prompt_overrides", {})
|
||||
if isinstance(_po, dict):
|
||||
agent._prompt_overrides = _po
|
||||
except Exception:
|
||||
pass
|
||||
```
|
||||
|
||||
### 4. Конфиг Whale — `config.yaml`
|
||||
|
||||
Добавить секцию:
|
||||
|
||||
```yaml
|
||||
agent:
|
||||
prompt_overrides:
|
||||
hermes_help: ~/.hermes/hermes-whale/review/hermes_help.md
|
||||
task_completion: ~/.hermes/hermes-whale/review/task_completion.md
|
||||
memory_guidance: ~/.hermes/hermes-whale/review/memory_guidance.md
|
||||
session_search_guidance: ~/.hermes/hermes-whale/review/session_search_guidance.md
|
||||
skills_guidance: ~/.hermes/hermes-whale/review/skills_guidance.md
|
||||
tool_use_enforcement: ~/.hermes/hermes-whale/review/tool_use_enforcement.md
|
||||
execution_discipline: ~/.hermes/hermes-whale/review/execution_discipline.md
|
||||
```
|
||||
|
||||
### 5. Файлы блоков
|
||||
|
||||
Создать 7 файлов в `~/.hermes/hermes-whale/review/`:
|
||||
|
||||
- `hermes_help.md` — содержимое константы `HERMES_AGENT_HELP_GUIDANCE`
|
||||
- `task_completion.md` — содержимое `TASK_COMPLETION_GUIDANCE`
|
||||
- `memory_guidance.md` — содержимое `MEMORY_GUIDANCE`
|
||||
- `session_search_guidance.md` — содержимое `SESSION_SEARCH_GUIDANCE`
|
||||
- `skills_guidance.md` — содержимое `SKILLS_GUIDANCE`
|
||||
- `tool_use_enforcement.md` — содержимое `TOOL_USE_ENFORCEMENT_GUIDANCE`
|
||||
- `execution_discipline.md` — содержимое `OPENAI_MODEL_EXECUTION_GUIDANCE`
|
||||
|
||||
### 6. Обратная совместимость
|
||||
|
||||
Если `prompt_overrides` не задан или файл не найден — используется хардкод. Никакой код не ломается для других профилей/пользователей.
|
||||
|
||||
### 7. Документация
|
||||
|
||||
- Обновить `personal/plans/thread-scoped-memory.md` → переименовать или создать отдельный doc
|
||||
- Создать `personal/plans/extract-stable-prompt-blocks.md` (этот)
|
||||
|
||||
## Порядок выполнения
|
||||
|
||||
1. ✅ Создать 7 файлов блоков в `~/.hermes/hermes-whale/review/`
|
||||
2. ✅ Пропатчить `agent/system_prompt.py` — добавить `_load_prompt_block()` и заменить 7 констант
|
||||
3. ✅ Пропатчить `agent/agent_init.py` — пробросить `prompt_overrides` из конфига
|
||||
4. ✅ Обновить `config.yaml` — добавить `agent.prompt_overrides` (через копию, т.к. patch блокирован TIRITH)
|
||||
5. ⏸️ Перезапустить webhook (ждёт команды)
|
||||
6. ⏸️ Обновить Obsidian docs (этот шаг)
|
||||
|
||||
## Проверка
|
||||
|
||||
После изменений system prompt должен содержать те же блоки, что и раньше, но загруженные из файлов. При изменении файла и рестарте сессии — новый текст. При удалении файла — fallback на хардкод.
|
||||
|
||||
## Pitfalls
|
||||
|
||||
- **TIRITH блокирует patch/config.yaml** — `config.yaml` под защитой TIRITH, patch и write_file отказываются писать в него. Решение: `cp` в `/tmp/`, отредактировать там, `cp` обратно (с аппрувом).
|
||||
- **sed не подходит для YAML конфигов** — сложные многострочные замены с вложенными отступами и escape-символами (`~`, `/`) ломаются в sed. Лучше patch.
|
||||
- **Конфиг всё равно просит аппрув** — при `cp` обратно TIRITH запрашивает подтверждение (overwrite project env/config file). Это нормально, нужно подтвердить.
|
||||
- **SOUL.md уже существует** — в Whale он лежит в `/Users/admin/.hermes/hermes-whale/SOUL.md`. Он НЕ выносится через prompt_overrides, т.к. уже является файлом и загружается отдельно через `load_soul_md()`.
|
||||
- **webhook не в PLATFORM_HINTS** — webhook нет в словаре `PLATFORM_HINTS` в `prompt_builder.py`, поэтому блок platform hints пустой. Это не менялось.
|
||||
@@ -0,0 +1,104 @@
|
||||
# Состав system prompt Hermes Whale
|
||||
|
||||
**Дата анализа:** 2026-06-24
|
||||
**Версия:** Текущее состояние на момент разговора
|
||||
|
||||
System prompt собирается в `agent/system_prompt.py` (Hermes Agent) из трёх слоёв: **stable**, **context**, **volatile**.
|
||||
|
||||
---
|
||||
|
||||
## Полный состав
|
||||
|
||||
### СТАБИЛЬНЫЙ СЛОЙ (stable) — кешируется на всю сессию
|
||||
|
||||
Этот слой не меняется между поворотами. Всё, кроме SOUL.md, захардкожено в `agent/prompt_builder.py`.
|
||||
|
||||
| # | Блок | Источник | Описание |
|
||||
|---|------|----------|----------|
|
||||
| 1 | **SOUL.md** | `~/.hermes/hermes-whale/SOUL.md` | Кастомная идентичность: «Кит (Whale) — Alex's personal agent». Правила: Obsidian MCP, jq, backup before edit, plan first, stop on стоп. |
|
||||
| 2 | **Hermes Agent help guidance** | `prompt_builder.py`: `HERMES_AGENT_HELP_GUIDANCE` | docs.hermes-agent.nousresearch.com — source of truth |
|
||||
| 3 | **Task completion guidance** | `prompt_builder.py`: `TASK_COMPLETION_GUIDANCE` | «Finishing the job» — доводить до конца, не фабриковать |
|
||||
| 4 | **Memory guidance** | `prompt_builder.py`: `MEMORY_GUIDANCE` | Как сохранять memory (declarative facts, не task progress) |
|
||||
| 5 | **Session search guidance** | `prompt_builder.py`: `SESSION_SEARCH_GUIDANCE` | session_search для кросс-сессионного контекста |
|
||||
| 6 | **Skills guidance** | `prompt_builder.py`: `SKILLS_GUIDANCE` | Сохранять сложные подходы как skills, патчить устаревшие |
|
||||
| 7 | **Tool-use enforcement** | `prompt_builder.py`: `TOOL_USE_ENFORCEMENT_GUIDANCE` | Ты ДОЛЖЕН вызывать инструменты, не описывать планы |
|
||||
| 8 | **Execution discipline** | `prompt_builder.py`: `OPENAI_MODEL_EXECUTION_GUIDANCE` | Расширенные правила: tool persistence, prerequisite checks, verification |
|
||||
| 9 | **Skills prompt** | `prompt_builder.py`: `build_skills_system_prompt()` | Список всех доступных skills (автогенерируется) |
|
||||
| 10 | **Environment hints** | `prompt_builder.py`: `build_environment_hints()` | macOS, home dir, cwd |
|
||||
| 11 | **Active profile hint** | `system_prompt.py` runtime | «default» + guard не лезть в чужие профили |
|
||||
| 12 | **Platform hints** | `prompt_builder.py`: `PLATFORM_HINTS` | webhook **нет** в словаре → пусто |
|
||||
|
||||
**Note #7–8:** Т.к. модель `deepseek-chat` попадает под `TOOL_USE_ENFORCEMENT_MODELS = ("gpt", "codex", "gemini", "gemma", "grok", "glm", "qwen", "deepseek")`, то enforcement применяется. DeepSeek НЕ входит в OpenAI-специфичную часть (GPT/Codex/Grok), поэтому блок `OPENAI_MODEL_EXECUTION_GUIDANCE` не вставляется — только `TOOL_USE_ENFORCEMENT_GUIDANCE`.
|
||||
|
||||
### КОНТЕКСТНЫЙ СЛОЙ (context) — зависит от cwd
|
||||
|
||||
| # | Блок | Что приходит |
|
||||
|---|------|-------------|
|
||||
| 13 | **AGENTS.md / CLAUDE.md / .cursorrules / .hermes.md** | Из `TERMINAL_CWD`. В этой сессии `TERMINAL_CWD=/Users/admin` — файлов нет → пусто |
|
||||
| 14 | **system_message** | Caller-supplied. Webhook не передаёт → пусто |
|
||||
|
||||
Поиск контекстных файлов (функция `build_context_files_prompt()`):
|
||||
1. `.hermes.md` / `HERMES.md` (walk to git root)
|
||||
2. `AGENTS.md` / `agents.md` (cwd only)
|
||||
3. `CLAUDE.md` / `claude.md` (cwd only)
|
||||
4. `.cursorrules` + `.cursor/rules/*.mdc` (cwd only)
|
||||
— Первый найденный wins, только один проект-контекст загружается.
|
||||
|
||||
SOUL.md из HERMES_HOME независим и не участвует в этой очереди — он загружается отдельно через `load_soul_md()`.
|
||||
|
||||
### ВОЛАТИЛЬНЫЙ СЛОЙ (volatile) — пересобирается каждый раз
|
||||
|
||||
| # | Блок | Источник | Описание |
|
||||
|---|------|----------|----------|
|
||||
| 15 | **MEMORY** | `memories/threads/<gateway_session_key>/MEMORY.md` | Thread-scoped memory (включено конфигом) |
|
||||
| 16 | **USER profile** | `memories/USER.md` | Глобальный профиль пользователя |
|
||||
| 17 | **Memory instruction** | `~/.hermes/hermes-whale/review/memory_prompt.md` | Кастомная инструкция что запоминать |
|
||||
| 18 | **Timestamp/model/provider** | Runtime | Дата старта, Model, Provider |
|
||||
|
||||
---
|
||||
|
||||
## Ключевые файлы
|
||||
|
||||
### SOUL.md
|
||||
- Путь: `~/.hermes/hermes-whale/SOUL.md` (HERMES_HOME/SOUL.md)
|
||||
- Полностью заменяет `DEFAULT_AGENT_IDENTITY`
|
||||
- Правила: Obsidian MCP first, jq, backup before edit, plan first, stop on стоп
|
||||
- Загружается `load_soul_md()` в `agent/prompt_builder.py`
|
||||
|
||||
### memory_prompt.md
|
||||
- Путь: `~/.hermes/hermes-whale/review/memory_prompt.md`
|
||||
- Конфиг: `memory.prompt_path` в config.yaml
|
||||
- Загружается в `agent/agent_init.py`, вставляется в volatile слой
|
||||
|
||||
### config.yaml (Whale)
|
||||
- Путь: `~/.hermes/hermes-whale/config.yaml`
|
||||
- Релевантные секции:
|
||||
```yaml
|
||||
memory:
|
||||
thread_scoped: true
|
||||
prompt_path: ~/.hermes/hermes-whale/review/memory_prompt.md
|
||||
```
|
||||
- Webhook route: `whale` на порту 8645, deliver в zulip
|
||||
- Webhook платформа **не имеет** platform hint в `PLATFORM_HINTS`
|
||||
|
||||
---
|
||||
|
||||
## Отсутствующие возможности (opportunity)
|
||||
|
||||
Алекс предложил сделать стабильный слой конфигурируемым через файлы так же, как `memory_prompt.md`:
|
||||
- Вынести каждый хардкод-блок (hermes help, task completion, memory guidance, session search, skills guidance и т.д.) в отдельный `.md` файл
|
||||
- Добавить `prompt_overrides` секцию в config.yaml, где указывать пути к файлам
|
||||
- Если файла нет — fallback на хардкод
|
||||
|
||||
Связанные файлы Hermes Agent:
|
||||
- `agent/system_prompt.py` — сборка system prompt, управляющая логика
|
||||
- `agent/prompt_builder.py` — константы и функции загрузки
|
||||
- `agent/agent_init.py` — чтение конфига и проброс
|
||||
|
||||
---
|
||||
|
||||
## Текущие ограничения
|
||||
|
||||
- webhook не имеет platform hint → может быть полезно добавить
|
||||
- Всё хардкоженое в `prompt_builder.py` нельзя переопределить без редактирования кода
|
||||
- Нет `.hermes.md` / `AGENTS.md` в `/Users/admin/` — контекстный слой пуст
|
||||
@@ -0,0 +1,122 @@
|
||||
# Реализация: 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) — не трогали
|
||||
Reference in New Issue
Block a user