337 lines
21 KiB
Markdown
Executable File
337 lines
21 KiB
Markdown
Executable File
# Hermes Self-Improvement Review (Background Memory/Skill Review)
|
||
|
||
Source: codebase analysis of `hermes-agent/agent/background_review.py`, `run_agent.py`, `agent/conversation_loop.py`, `agent/agent_init.py`.
|
||
|
||
## Что это
|
||
|
||
После каждого юзерского сообщения ([run_conversation](https://github.com/NousResearch/hermes-agent)) AIAgent может запустить **фоновый daemon-thread**, который форкает ещё один AIAgent, даёт ему снимок всего разговора и просит оценить — нужно ли сохранить что-то в память (Memory) или обновить/создать скилл (Skill).
|
||
|
||
Основной поток продолжается немедленно. Ревью не блокирует пользователя.
|
||
|
||
## Where it lives
|
||
|
||
| File | Lines | Role |
|
||
|------|-------|------|
|
||
| `agent/background_review.py` | 1–597 | Центральный модуль — промпты, `_run_review_in_thread`, `spawn_background_review_thread`, `summarize_background_review_actions` |
|
||
| `run_agent.py` | 1360–1400 | Форвардеры: `_spawn_background_review`, `_summarize_background_review_actions`, импорт констант-промптов |
|
||
| `agent/conversation_loop.py` | 4779–4805 | Место вызова — после завершения ответа, перед возвратом `result` |
|
||
| `agent/codex_runtime.py` | 150–162 | Аналогичный вызов для Codex Response API режима |
|
||
| `agent/agent_init.py` | 1067–1075, 1187–1190 | Инициализация счётчиков и интервалов |
|
||
| `agent/tool_executor.py` | 207–209, 776–778 | Сброс счётчиков при вызове `memory` или `skill_manage` |
|
||
| `tui_gateway/server.py` | 2452–2464 | Подключение `background_review_callback` для TUI (Ink) |
|
||
| `gateway/run.py` | 17849–17893 | Подключение `background_review_callback` для gateway-сообщений |
|
||
|
||
## Как триггерится
|
||
|
||
Два независимых счётчика:
|
||
|
||
### Memory review (turn-based)
|
||
|
||
- Счётчик: `_turns_since_memory`
|
||
- Интервал: `_memory_nudge_interval` (default = **10**)
|
||
- Задаётся из `config.yaml: memory.nudge_interval`
|
||
- Инкрементится **в начале** каждого `run_conversation` (строка 559)
|
||
- Проверка: `if _turns_since_memory >= _memory_nudge_interval` → `_should_review_memory = True`
|
||
- Сброс: при вызове `memory` tool (строка 207 в tool_executor.py)
|
||
- Условия: `"memory" in valid_tool_names` и `_memory_store != None`
|
||
|
||
Если `memory_enabled: false` → ревью не триггерится.
|
||
|
||
### Skills review (iteration-based)
|
||
|
||
- Счётчик: `_iters_since_skill`
|
||
- Интервал: `_skill_nudge_interval` (default = **10**)
|
||
- Инкрементится **внутри tool-call цикла** после каждого батча (строка 860)
|
||
- Проверка: `if _iters_since_skill >= _skill_nudge_interval` → `_should_review_skills = True`
|
||
- Сброс: при вызове `skill_manage` tool (строки 209, 778 в tool_executor.py)
|
||
- Условия: `"skill_manage" in valid_tool_names`
|
||
|
||
### Финальная проверка
|
||
|
||
```python
|
||
if final_response and not interrupted and (_should_review_memory or _should_review_skills):
|
||
agent._spawn_background_review(
|
||
messages_snapshot=list(messages),
|
||
review_memory=_should_review_memory,
|
||
review_skills=_should_review_skills,
|
||
)
|
||
```
|
||
|
||
## Что происходит внутри
|
||
|
||
### `_run_review_in_thread` (background_review.py:327–559)
|
||
|
||
1. **Создаёт форк AIAgent**, наследуя:
|
||
- `model`, `provider`, `base_url`, `api_key`, `credential_pool` от родителя (чтобы попасть в тот же prefix cache у Anthropic/OpenRouter)
|
||
- `_cached_system_prompt` — byte-идентичный, чтобы не разогревать кеш заново
|
||
- `session_id`, `session_start` — те же
|
||
- `_memory_store`, `_memory_enabled`, `_user_profile_enabled` — те же
|
||
- Но `skip_memory=True` (чтобы форк не создавал свой MemoryManager)
|
||
- `_memory_nudge_interval = 0`, `_skill_nudge_interval = 0` (чтобы не рекурсил)
|
||
|
||
2. **Устанавливает tool whitelist** (строки 459–472):
|
||
```python
|
||
review_whitelist = {
|
||
t["function"]["name"]
|
||
for t in get_tool_definitions(
|
||
enabled_toolsets=["memory", "skills"],
|
||
quiet_mode=True,
|
||
)
|
||
}
|
||
```
|
||
Вне whitelist — всё блокируется с сообщением `"Background review denied non-whitelisted tool: ..."`
|
||
|
||
3. **Запускает** `review_agent.run_conversation(...)` с:
|
||
- Выбранным промптом (см. ниже) + суффикс `"You can only call memory and skill management tools. Other tools will be denied at runtime — do not attempt them."`
|
||
- `conversation_history=messages_snapshot` — весь разговор до этого момента
|
||
- `max_iterations=16` (жёстко, не из конфига)
|
||
|
||
4. **После завершения** — собирает успешные tool-результаты через `summarize_background_review_actions()`, фильтруя те, что уже были в `messages_snapshot` (чтобы не показывать старые как новые).
|
||
|
||
5. **Выводит** пользователю:
|
||
```python
|
||
f" 💾 Self-improvement review: {summary}"
|
||
# Например: "💾 Self-improvement review: Memory updated · Skill 'xxx' patched"
|
||
```
|
||
Через `agent._safe_print()` и через `agent.background_review_callback()` (для TUI/gateway).
|
||
|
||
6. **На ошибках** — пишет в лог `"Background memory/skill review failed: ..."` и шлёт `_emit_auxiliary_failure`. Не ломает основной разговор.
|
||
|
||
## 3 варианта промпта
|
||
|
||
Выбираются в `spawn_background_review_thread()` по комбинации флагов:
|
||
|
||
| `review_memory` | `review_skills` | Промпт | Суть |
|
||
|---|---|---|---|
|
||
| ✅ | ❌ | `_MEMORY_REVIEW_PROMPT` (~10 строк) | Сохранить факты о пользователе (persona, preferences, style). Если нечего — 'Nothing to save.' |
|
||
| ❌ | ✅ | `_SKILL_REVIEW_PROMPT` (~120 строк) | Самая большая инструкция. Искать сигналы: коррекции, frustration, новые техники, устаревшие скиллы. Приоритет: loaded skill → existing umbrella → support file → new umbrella. Запрещено: env-зависимости, негативные фиксации, session-specific. |
|
||
| ✅ | ✅ | `_COMBINED_REVIEW_PROMPT` (~80 строк) | Смесь обоих — сначала memory, потом skills с той же логикой. |
|
||
|
||
## Потенциальные проблемы
|
||
|
||
- **Skills interval — конфигурируемый** (исправлено 2026-09-29; ранее в доке ошибочно значилось «хардкожен в 10»).
|
||
Читается из `config.yaml → skills.creation_nudge_interval`, дефолт в коде `10`.
|
||
Проверено: `agent/agent_init.py:1224` → `int(skills_config.get("creation_nudge_interval", 10))`.
|
||
На этом профиле (`hermes-whale/config.yaml:280`) стоит **`0`** (отключено 2026-09-29, коммит `d2e1586`).
|
||
- **Tool whitelist был фиксирован** — теперь настраивается через `review.allowed_toolsets` в config.yaml (см. ниже).
|
||
- **Форк не имеет MCP-серверов** — MCP tools (obsidian-mcp, etc.) не регенерируются в форке.
|
||
- **max_iterations=16** — жёстко, не из конфига. Может не хватить на комплексное ревью многоскилловых сессий.
|
||
|
||
## 🔴 Как ВЫКЛЮЧИТЬ автогенерацию скиллов (2026-09-29)
|
||
|
||
Задача Alex: «отключи автогенерацию скиллов». Разобрано до уровня кода; **ВНЕСЕНО 2026-09-29**
|
||
(вариант A — см. §Развилка). Выбор Alex: «да. только hermes-whale. без бэкапов, у нас гит. потом коммит».
|
||
|
||
### Два независимых пути, которыми агент сам правит библиотеку скиллов
|
||
|
||
| # | Механизм | Что делает | Ключ конфига | Сейчас |
|
||
|---|---|---|---|---|
|
||
| 1 | **Skill nudge** | каждые N tool-итераций спавнит фоновый форк, который **создаёт/патчит скиллы** | `skills.creation_nudge_interval` | **15** → ставить `0` |
|
||
| 2 | **Review fork** | после сессии тот же форк, но с `allowed_toolsets` | `review.allowed_toolsets` | `[memory, skills, file]` |
|
||
|
||
### Код (проверено построчно)
|
||
|
||
```python
|
||
# agent/agent_init.py:1220-1226
|
||
agent._skill_nudge_interval = 10
|
||
skills_config = _agent_cfg.get("skills", {})
|
||
agent._skill_nudge_interval = int(skills_config.get("creation_nudge_interval", 10))
|
||
```
|
||
|
||
```python
|
||
# agent/conversation_loop.py:858-860 — инкремент внутри tool-цикла
|
||
if (agent._skill_nudge_interval > 0
|
||
and "skill_manage" in agent.valid_tool_names):
|
||
agent._iters_since_skill += 1
|
||
```
|
||
|
||
```python
|
||
# agent/conversation_loop.py:4779-4785 — финальная проверка в конце хода
|
||
_should_review_skills = False
|
||
if (agent._skill_nudge_interval > 0 # ← ГЛАВНЫЙ ВЫКЛЮЧАТЕЛЬ
|
||
and agent._iters_since_skill >= agent._skill_nudge_interval
|
||
and "skill_manage" in agent.valid_tool_names):
|
||
_should_review_skills = True
|
||
agent._iters_since_skill = 0
|
||
```
|
||
|
||
```python
|
||
# agent/conversation_loop.py:4797 — спавн только если что-то взведено
|
||
if final_response and not interrupted and (_should_review_memory or _should_review_skills):
|
||
agent._spawn_background_review(...)
|
||
```
|
||
|
||
**Вывод:** `creation_nudge_interval: 0` делает первое условие ложным **всегда** →
|
||
`_should_review_skills` никогда не станет `True` → скиллы от nudge-механизма не создаются.
|
||
Это чистый выключатель, other logic не затронута. Счётчик `_iters_since_skill` при `0`
|
||
даже не инкрементится (строка 858).
|
||
|
||
### Промпты — где живут
|
||
|
||
| Промпт | Что делает | Когда выбирается |
|
||
|---|---|---|
|
||
| `_MEMORY_REVIEW_PROMPT` | сохранить факты о юзере | `review_memory=True, review_skills=False` |
|
||
| `_SKILL_REVIEW_PROMPT` | **правит библиотеку скиллов** («Be ACTIVE — most sessions produce at least one skill update») | `review_skills=True` |
|
||
| `_COMBINED_REVIEW_PROMPT` | и память, и скиллы | оба `True` |
|
||
|
||
Выбор — `spawn_background_review_thread()` (`agent/background_review.py:583-588`):
|
||
`if review_memory and review_skills → COMBINED; elif review_memory → MEMORY; else → SKILL`.
|
||
То есть **скиллы правятся в ветках COMBINED и «else»**. Убрать `skills` из
|
||
`allowed_toolsets` — упадёт сам tool, но промпт останется.
|
||
|
||
### ✅ Внесено (2026-09-29) — вариант A
|
||
|
||
| Ключ | Было | Стало | Файл |
|
||
|---|---|---|---|
|
||
| `skills.creation_nudge_interval` | `15` | **`0`** (строка 280) | `~/.hermes/hermes-whale/config.yaml` |
|
||
|
||
Коммит **`d2e1586`** (`chore(whale): disable auto skill generation`), md5 `ac0c7af01d48fa24fb763efbe56668a2`.
|
||
`review.allowed_toolsets` **не тронут** (`[memory, skills, file]`) — память продолжает обновляться.
|
||
`~/.hermes/config.yaml` не тронут (Alex: «только hermes-whale»).
|
||
|
||
**Требуется перезапуск gateway**, чтобы правка вступила в силу:
|
||
`POST /api/services/hermes_whale/restart` — конфиг читается в `AIAgent.__init__`.
|
||
|
||
### 🔴 Питфолл: `hermes config set` перезаписывает ВЕСЬ YAML
|
||
|
||
`hermes config set <key> <val>` не делает точечную правку — он сериализует конфиг заново.
|
||
Последствия на этом профиле: **63 строки комментариев срезано**, `host: "0.0.0.0"` → `0.0.0.0`,
|
||
отступы списков переформатированы (` - memory` → ` - memory`).
|
||
|
||
Значения при этом не пострадали — сверено построчно через
|
||
`git diff --cached -U0 | grep -E "^[+-]" | grep -v "^[+-][+-]" | grep -vE "^[+-]\s*#"`.
|
||
**Всегда проверять дифф на смысловые изменения, а не только на свою строку.**
|
||
|
||
### 🔴 Питфолл: `patch`/`write_file` НЕ могут писать в config.yaml
|
||
|
||
Инструменты возвращают `Refusing to write to Hermes config file` — конфиг помечен
|
||
security-sensitive. Править **только** через `hermes config set`, руками (`hermes config edit`)
|
||
или с одобрения Alex. Это не баг, обходить не надо.
|
||
|
||
### 🔴 Питфолл: `hermes` не в PATH
|
||
|
||
`hermes: command not found` — бинарь лежит в venv:
|
||
`/Users/admin/.hermes/hermes-agent/venv/bin/hermes`.
|
||
`hermes config show` **не печатает** секцию `skills` — проверять значение грепом по файлу
|
||
(`grep -n creation_nudge_interval <config>`), а не через CLI. `config get` не существует
|
||
(есть только `show|edit|set|path|env-path|check|migrate`).
|
||
|
||
### Развилка (решение принято: A)
|
||
|
||
| Вариант | Что меняется | Последствие |
|
||
|---|---|---|
|
||
| **A. Только `creation_nudge_interval: 0`** ✅ | 1 ключ | скиллы не создаются сами; **память продолжает копиться** |
|
||
| B. A + убрать `skills` из `review.allowed_toolsets` | 2 ключа | глухо оба; но фоновый review перестанет писать и в **memory** — авто-обновление USER.md/MEMORY.md умрёт. **Не выбран.** |
|
||
|
||
Файлы (две копии, они **разные** — см. питфолл ниже):
|
||
`~/.hermes/hermes-whale/config.yaml` (основная) и `~/.hermes/config.yaml`.
|
||
|
||
## Реализованные расширения (2026-06-24)
|
||
|
||
Сделаны два изменения в исходном коде Hermes Agent для того, чтобы промпты и whitelist ревью были настраиваемы из `config.yaml`:
|
||
|
||
### 1. agent_init.py — чтение секции `review:` из config.yaml
|
||
|
||
Место: `agent/agent_init.py` (после блока skills config).
|
||
|
||
Читает секцию:
|
||
```yaml
|
||
review:
|
||
# Пути к .md файлам с кастомными промптами.
|
||
# Если файл не найден/не читается — тихо падает на встроенный хардкод.
|
||
skill_prompt_path: ~/.hermes/review/skill_prompt.md
|
||
memory_prompt_path: ~/.hermes/review/memory_prompt.md
|
||
combined_prompt_path: ~/.hermes/review/combined_prompt.md
|
||
|
||
# Какие toolsets доступны форку ревью.
|
||
# По дефолту: ["memory", "skills"].
|
||
# Чтобы форк мог читать/писать .md файлы Obsidian — добавить "file".
|
||
allowed_toolsets:
|
||
- memory
|
||
- skills
|
||
# - file
|
||
```
|
||
|
||
Что делает код:
|
||
|
||
1. Выставляет `agent._review_allowed_toolsets` (по дефолту `["memory", "skills"]`)
|
||
2. Для каждого из трёх путей (`skill_prompt_path`, `memory_prompt_path`, `combined_prompt_path`) — пытается прочитать файл и записать содержимое в `agent._SKILL_REVIEW_PROMPT` / `agent._MEMORY_REVIEW_PROMPT` / `agent._COMBINED_REVIEW_PROMPT` соответственно.
|
||
3. Если путь не указан, файл не найден или не читается — тихо игнорируется, остаётся встроенный хардкод.
|
||
|
||
### 2. background_review.py — использование `_review_allowed_toolsets` с инстанса
|
||
|
||
Место: `agent/background_review.py`, функция `_run_review_in_thread`, whitelist (строка ~459).
|
||
|
||
**Было:**
|
||
```python
|
||
review_whitelist = {
|
||
t["function"]["name"]
|
||
for t in get_tool_definitions(
|
||
enabled_toolsets=["memory", "skills"],
|
||
quiet_mode=True,
|
||
)
|
||
}
|
||
```
|
||
|
||
**Стало:**
|
||
```python
|
||
_allowed_sets = getattr(agent, "_review_allowed_toolsets", ["memory", "skills"])
|
||
review_whitelist = {
|
||
t["function"]["name"]
|
||
for t in get_tool_definitions(
|
||
enabled_toolsets=_allowed_sets,
|
||
quiet_mode=True,
|
||
)
|
||
}
|
||
```
|
||
|
||
А также сообщение для форка динамически подставляет имена toolsets вместо хардкода `"memory and skill"`.
|
||
|
||
### 3. Примеры промптов
|
||
|
||
Созданы в `~/.hermes/review/`:
|
||
|
||
- **`skill_prompt.md`** — копия встроенного `_SKILL_REVIEW_PROMPT` + секция `--- Obsidian documentation update ---`, которая инструктирует форк после каждого обновления скилла читать и патчить соответствующий `.md` в `/Users/admin/obsidian/`.
|
||
- **`memory_prompt.md`** — копия `_MEMORY_REVIEW_PROMPT` + инструкция обновлять `personal/profiles/Alex.md` при сохранении user preference.
|
||
|
||
### Как включить Obsidian-обновление
|
||
|
||
1. Раскомментировать в `~/.hermes/config.yaml`:
|
||
```yaml
|
||
review:
|
||
skill_prompt_path: ~/.hermes/review/skill_prompt.md
|
||
memory_prompt_path: ~/.hermes/review/memory_prompt.md
|
||
allowed_toolsets:
|
||
- memory
|
||
- skills
|
||
- file
|
||
```
|
||
2. Перезапустить Hermes (новый сессионный конфиг прочитается при старте `AIAgent.__init__`).
|
||
3. После каждого десятого tool-батча (или любого вызова `skill_manage` раньше) форк будет: создать/обновить скилл **и** прочитать/пропатчить соответствующий Obsidian-документ через `read_file`/`patch`.
|
||
|
||
### Замечание по архитектуре
|
||
|
||
File tools работают напрямую с файловой системой, в обход obsidian-mcp. Это значит:
|
||
- **Линки** `[[wiki]]` в Obsidian не резолвятся автоматически — форк видит сырой `.md`.
|
||
- **Frontmatter** (YAML заголовки) — видит как часть текста, может патчить.
|
||
- **Vault search** недоступен — только прямой путь к файлу.
|
||
|
||
Для большинства задач (пропатчить существующий doc, создать новый .md с frontmatter) file tools достаточно. Если нужен полный Obsidian API — надо дорабатывать подключение MCP в форке (Вариант B из первоначального исследования, пока не реализован).
|
||
|
||
## Related files
|
||
|
||
- `~/.hermes/hermes-whale/review/skill_prompt.md` — кастомный skills промпт с Obsidian-инструкцией (для всех артефактов: планы, конфиги, этапы проектов)
|
||
- `~/.hermes/hermes-whale/review/memory_prompt.md` — кастомный memory промпт с Obsidian-инструкцией
|
||
- `agent/agent_init.py` — чтение секции `review:` из config.yaml
|
||
- `agent/background_review.py` — использование `_review_allowed_toolsets` и кастомных промптов
|
||
- Config: `config.yaml → review.*` (см. секцию выше)
|
||
|
||
## Хронология
|
||
|
||
- 2026-06-24: Исследование и документирование механизма self-improvement review (Whale/Alex).
|
||
- 2026-06-24: Реализация настройки через config.yaml: промпты (через пути к .md) + allowed_toolsets. Созданы примеры промптов с Obsidian-инструкцией.
|
||
- 2026-06-24: Реализация thread-scoped memory + custom memory instruction (см. `personal/plans/thread-scoped-memory.md`).
|
||
- 2026-06-24: Тесты thread-scoped memory: 9 новых тестов (76/76 passed), закоммичено в `4ebad4f69`.
|
||
- 2026-09-29: Разбор механизма — интервал скиллов оказался **конфигурируемым** (ранее в доке значилось «хардкожен в 10»), проверено по `agent_init.py:1224`.
|
||
- 2026-09-29: **Автогенерация скиллов отключена** — `skills.creation_nudge_interval: 15 → 0`, коммит `d2e1586`. Память оставлена включённой. См. §«Как ВЫКЛЮЧИТЬ».
|