From e6b82f03765b3dae52d43670ab424b75975d29a8 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Thu, 21 May 2026 23:51:23 +0800 Subject: [PATCH] Stabilize streaming runtime completion --- .env.example | 2 +- CURRENT_STATE.md | 535 ++++++---------------------- duck_core/api.py | 34 +- duck_core/runtime_loop.py | 14 +- tests/smoke/test_api_stream_chat.py | 117 ++++++ 5 files changed, 261 insertions(+), 441 deletions(-) diff --git a/.env.example b/.env.example index 1fc1560..2d3a9b1 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,7 @@ DUCK_CTX_SIZE=4096 DUCK_N_GPU_LAYERS=20 DUCK_PARALLEL=1 DUCK_LLAMA_DEVICE=Vulkan0 -DUCK_LLAMA_EXTRA_ARGS="--reasoning-budget 512 --cache-ram 0" +DUCK_LLAMA_EXTRA_ARGS="--reasoning off --cache-ram 0" DUCK_HOST=127.0.0.1 DUCK_API_HOST=127.0.0.1 diff --git a/CURRENT_STATE.md b/CURRENT_STATE.md index 482d8ca..9be8b48 100644 --- a/CURRENT_STATE.md +++ b/CURRENT_STATE.md @@ -1,462 +1,131 @@ -# DuckLM — Текущее состояние проекта +# DuckLM — текущее состояние проекта -**Дата анализа:** 2026-05-21 -**Версия:** 0.2.0 -**Расположение:** `~/git/ducklm_2` +Дата обновления: 2026-05-21 +Рабочая копия: `/home/mirivlad/git/ducklm` +Git remote: `origin/main` ---- +## Общий статус -## Последние изменения (Phase 1-7) +DuckLM сейчас является рабочим локальным MVP/beta runtime поверх `llama-server`. -### Phase 1: MemoryPolicy — LLM-классификация памяти ✅ -- `duck_core/memory/policy.py` — переписан с нуля: LLM-классификация через critic-роль -- Роль `memory_policy` добавлена в `config/models.yaml` + промпт -- Интегрирован в RuntimeLoop: `_run_memory_policy()` после каждой задачи -- События: `memory_policy_decision`, `memory_stored`, `memory_policy_failed` -- 6 тестов в `tests/smoke/test_memory_policy.py` +Система реализует основной цикл из `Ducklm.md`: -### Phase 2: Рефлексия (Critic) — автоматический вызов ✅ -- `_run_reflection()` в RuntimeLoop — transcript из event store → critic → experience -- Параметр `reflect: bool = True` в `run_chat()` -- `ExperienceRecorder` передаётся через `create_app()` -- События: `reflection_completed`, `reflection_failed` -- 3 теста в `tests/smoke/test_reflection.py` - -### Phase 3-4: ContextBuilder v2 + Summary-роль ✅ -- Полностью переписан `duck_core/context_builder.py` -- Token budget awareness, приоритизация, суммаризация через summary-роль -- `estimate_tokens()`, `estimate_messages_tokens()` утилиты -- Подключён model_client для LLM-суммаризации -- 11 тестов в `tests/smoke/test_context_builder.py` - -### Phase 5: VectorMemory — интеграция ✅ -- `VectorMemory` добавлен в RuntimeLoop и `create_app()` -- **Локальная модель эмбеддингов**: `all-MiniLM-L6-v2` (384 размерности, sentence-transformers) -- При `memory_stored` также сохраняется в Qdrant (graceful fallback при ошибках) -- Поддержка двух режимов: локальная модель + remote `/v1/embeddings` endpoint -- `sentence-transformers` добавлен в зависимости `pyproject.toml` -- 4 теста в `tests/smoke/test_vector_memory_integration.py` - -### Embeddings — архитектура - -### Phase 6: Recall-роль ✅ -- Роль `recall` добавлена в `config/models.yaml` + промпт `prompts/roles/recall.md` -- `ContextBuilder.recall_relevant_memory()` — LLM-фильтрация релевантных воспоминаний -- Интегрирован в `/v1/chat` endpoint - -### Phase 7: Coder-роль — интеграция ✅ -- `CoderTool` создан в `duck_core/tools/coder.py` -- Зарегистрирован в `ToolGateway.default()` -- Описан в `prompts/roles/action.md` - -### Статус тестов -- **72 из 73** smoke-тестов проходят -- 1 тест (`test_llama_server_connection_live_skip_by_env`) требует живой llama-server - ---- - -## 1. Что такое DuckLM - -DuckLM — это **локальная агентная система (cognitive runtime)**, работающая поверх локальных языковых моделей через `llama-server`. Это не inference-сервер, а полноценный когнитивный цикл: - -``` -состояние → контекст → мышление → намерение → действие → наблюдение → рефлексия → память → опыт +```text +сообщение -> задача -> контекст -> action directive -> tools -> observations +-> thinker response -> task events -> memory/reflection/experience ``` -Ключевая идея: DuckLM — это **оркестратор**, который управляет задачами, инструментами, памятью, навыками и рефлексией, используя локальные LLM через OpenAI-совместимый API (`llama-server`). +WebChat доступен через FastAPI на `http://127.0.0.1:8000/`. +Основной `llama-server` ожидается на `http://127.0.0.1:8081/v1`. ---- +## Что реализовано -## 2. Архитектурные принципы +- FastAPI HTTP API и WebChat. +- ConversationStore для диалогов и истории. +- TaskStore и EventStore в SQLite. +- ModelClient с логическими ролями из `config/models.yaml`. +- Роли: `thinker`, `critic`, `coder`, `action`, `summary`, `memory_policy`, `recall`. +- SSE streaming chat: reasoning/content deltas, runtime status events, final stats. +- Runtime status в чате для долгих этапов: planning, running_tool(s), answering. +- Min/avg/max token speed в конце ответа. +- ToolGateway: + - `file_read` + - `file_write` + - `list_dir` + - `search_files` + - `shell_exec_safe` + - `coder` +- Workspace guard с approval для доступа к внешним путям. +- Approval flow: + - allow once + - allow forever by exact normalized action hash + - deny + - sudo password flow +- Command audit для shell-команд. +- SkillRegistry и API/UI для skills. +- Автоматический выбор candidate skill по ключевым словам и добавление skill summary в context. +- MemoryStore в SQLite. +- MemoryPolicy через LLM role `memory_policy` с fallback в безопасный no-store режим. +- VectorMemory adapter для Qdrant с локальной embedding-моделью или remote embeddings endpoint. +- Recall-фильтрация памяти через `recall` role. +- Reflection через `critic` role. +- ExperienceRecorder и skill update proposals. +- Scripts для llama-server, verification и benchmark. +- Docker compose для Qdrant. +- Smoke tests. -### 2.1. Использование готовых компонентов +## Недавние исправления -| Компонент | Источник | -|-----------|----------| -| LLM inference | `llama-server` (llama.cpp, собранный с Vulkan) | -| Хранение состояния | SQLite (aiosqlite) | -| Векторная память | Qdrant (через docker-compose) | -| HTTP API | FastAPI | -| Web-интерфейс | Jinja2 + ванильный JS | -| Валидация данных | Pydantic | -| Конфигурация | PyYAML + python-dotenv | +- WebChat теперь не выглядит зависшим во время action/planning: backend стримит `runtime_status`. +- В чат больше не выводятся лишние дубли tool-output; tool events показываются компактно. +- Внешние пути за пределами workspace не падают простой ошибкой, а требуют approval. +- Streaming endpoints теперь запускают тот же post-processing, что и обычный `/v1/chat`: + - memory policy + - memory store/vector store + - reflection + - experience records +- Skill candidate selection теперь используется в обычном и streaming chat. -**Не пишется с нуля:** inference server, model scheduler, vector DB, OpenAI API, MCP, песочница, workflow engine. +## Соответствие этапам из Ducklm.md -**Пишется с нуля:** Duck Core (runtime loop, context builder, model client, event store, tool gateway, approvals, skills, experience, memory policy, FastAPI API, WebChat). +| Этап | Статус | Комментарий | +| --- | --- | --- | +| 1. Project skeleton / FastAPI / WebChat | Готово | Проект запускается, WebChat не пустой | +| 2. llama-server roles / ModelClient | Готово | Одна модель может обслуживать все роли | +| 3. Chat/tasks/events/WebChat response | Готово | Есть обычный и streaming chat | +| 4. cognition_response vs action_directive | Готово | Action role structured JSON, thinker отдельно | +| 5. ToolGateway | Готово | File/search/shell/coder tools, event log | +| 6. Approvals | Готово | UI и API approvals, allow_once/forever/deny | +| 7. Skills | Готово | Registry, API/UI, candidate skill injection | +| 8. Reflection/Experience | Готово | Reflection после completed задач, experience records | +| 9. Memory/VectorMemory | Готово частично | SQLite memory готова; Qdrant зависит от запущенного сервиса и embeddings | +| 10. MTP/benchmark | Готово как experimental | MTP script есть, action по умолчанию остаётся на main endpoint | -### 2.2. Web/API first +## Остаточные ограничения -- **WebChat** — интерфейс для человека (порт 8000) -- **HTTP API** — для кодера, тестов и внешних агентов -- CLI не входит в обязательную часть (если понадобится — тонкий клиент поверх HTTP API) +- Qdrant и локальная embedding-модель должны быть доступны отдельно; при ошибках vector memory деградирует без падения runtime. +- Token speed считается приближённо по текущему estimator, а не по tokenizer конкретной модели. +- Skill selection сейчас keyword-based. LLM selection можно добавить позже, если понадобится. +- WebChat остаётся lightweight vanilla JS UI; это не production frontend framework. +- `@app.on_event("startup")` работает, но FastAPI предупреждает, что lifespan API современнее. -### 2.3. Роли моделей — логические, не физические +## Проверка -Роли: `thinker`, `critic`, `coder`, `action`, `summary`, `recall`, `sys_util`. +Последняя полная проверка: -Все роли в текущей конфигурации указывают на одну физическую модель (`local-main` на порту 8081). Различие между ролями задаётся комбинацией: -- system prompt -- temperature -- max_output_tokens -- response_format / structured_output -- memory scope -- tool permissions - -### 2.4. Token budget - -``` -DUCK_CTX_SIZE=4096 (в .env, хотя в коде дефолт 65536) -DUCK_MAX_INPUT_TOKENS=49152 -DUCK_MAX_RECENT_EVENTS_TOKENS=12000 -DUCK_MAX_MEMORY_TOKENS=8000 -DUCK_MAX_SKILL_TOKENS=6000 +```bash +. .venv/bin/activate +ruff check . +pytest tests/smoke -q +git diff --check ``` -Output limits по ролям: -- thinker: 8192 -- critic: 4096 -- coder: 16384 -- action: 2048 -- summary: 4096 +Ожидаемый результат на 2026-05-21: smoke tests проходят. ---- +## Запуск -## 3. Целевая архитектура (из ТЗ) - -``` -┌─────────────┐ -│ WebChat │ ← интерфейс человека -└──────┬──────┘ - │ - ▼ -┌─────────────┐ -│ FastAPI │ ← интерфейс кодера, тестов и агентов -└──────┬──────┘ - │ - ▼ -┌─────────────────────────────────────┐ -│ Duck Core │ -│ RuntimeLoop, TaskState, │ -│ ContextBuilder, ModelClient, │ -│ SkillRegistry, ToolGateway, │ -│ ApprovalService, Reflection, │ -│ MemoryPolicy, ExperienceRecorder │ -└──────┬──────────────┬───────────────┘ - │ │ - ▼ ▼ -┌────────────┐ ┌──────────────────┐ -│llama-server│ │ SQLite/PostgreSQL│ -│OpenAI-comp.│ │ events/tasks/ │ -└────────────┘ │ approvals │ - │ └──────────────────┘ - ▼ -┌────────────┐ -│ Qdrant │ ← semantic memory -└────────────┘ +```bash +. .venv/bin/activate +bash scripts/llama/start_main.sh start +python -m duck_core.api ``` ---- +Открыть WebChat: -## 4. Что реализовано (текущее состояние) +```text +http://127.0.0.1:8000/ +``` -### 4.1. Полностью реализованные компоненты +Проверить API: -#### Конфигурация и настройки -- **`duck_core/config.py`** — `Settings` dataclass, загрузка из `.env`, кэширование через `lru_cache` -- **`config/models.yaml`** — 5 ролей (thinker, critic, coder, action, summary), все на `local-main` (порт 8081) -- **`.env`** / **`.env.example`** — полная конфигурация путей, портов, GPU, Qdrant +```bash +curl --noproxy '*' http://127.0.0.1:8000/health +curl --noproxy '*' http://127.0.0.1:8000/v1/models/roles +``` -#### ModelClient -- **`duck_core/model_client.py`** — ролевая маршрутизация вызовов к llama-server - - `chat()` — синхронный вызов с измерением latency, usage - - `stream_chat()` — streaming через SSE (reasoning_delta + content_delta) - - `ping()` — проверка доступности всех ролей - - Автоматическая подстановка system prompt из файла - - Автоматический `response_format: json_schema` для action-роли +## Что делать следующим -#### Хранение состояния (SQLite) -- **`duck_core/tasks/state.py`** — `TaskState` (Pydantic модель) -- **`duck_core/tasks/store.py`** — `TaskStore`: create, update_status, complete, fail, cancel, waiting_for_approval, get, list -- **`duck_core/events/store.py`** — `EventStore`: append (с авто-increment sequence), list_events, list_by_type -- **`duck_core/conversations/store.py`** — `ConversationStore`: create, ensure, get, list, add_message, list_messages, get_conversation_id_for_task -- **`duck_core/approvals/service.py`** — `ApprovalService`: create_pending, pending, get, allow_once, allow_forever, deny, is_allowed_forever -- **`duck_core/experience/recorder.py`** — `ExperienceRecorder`: record, list_records, get_record, write_skill_update_proposal -- **`duck_core/memory/store.py`** — `MemoryStore`: add, list, search (LIKE), relevant (scope-aware), infer_scope, _normalize_scope - -#### Runtime Loop -- **`duck_core/runtime_loop.py`** — ядро когнитивного цикла: - - `run_chat()` — полный цикл: создание задачи → action loop → thinker → завершение - - `continue_after_approval()` — продолжение после одобрения действия - - `_run_action_loop()` — итеративный цикл вызова инструментов (max 4 итерации) - - `_run_action_tools()` — парсинг action directive от модели → вызов ToolGateway - - `_append_command_audit()` — аудит shell-команд через event store - - Обработка requires_approval → пауза с ожиданием решения пользователя - -#### Context Builder -- **`duck_core/context_builder.py`** — `ContextBuilder.build_basic_messages()`: собирает сообщения из memory records, history и текущего user message - -#### Tools -- **`duck_core/tools/base.py`** — `ToolResult` (Pydantic), `Tool` (Protocol) -- **`duck_core/tools/gateway.py`** — `ToolGateway`: маршрутизация action → конкретный инструмент -- **`duck_core/tools/file_read.py`** — `FileReadTool`: чтение файлов внутри workspace, запрет .env/.ssh/shadow -- **`duck_core/tools/file_write.py`** — `FileWriteTool`: запись внутри workspace, защита от перезаписи -- **`duck_core/tools/list_dir.py`** — `ListDirTool`: листинг директории внутри workspace -- **`duck_core/tools/search_files.py`** — `SearchFilesTool`: текстовый поиск по файлам (glob, case_sensitive) -- **`duck_core/tools/shell_exec_safe.py`** — `ShellExecSafeTool`: allowlist + blocklist + approval -- **`duck_core/tools/command_policy.py`** — `CommandPolicy`: классификация команд (readonly, system, destructive, dangerous fragments) -- **`duck_core/tools/paths.py`** — `resolve_workspace_path()`: защита от path traversal - -#### Approvals -- **`duck_core/approvals/service.py`** — полный цикл согласований: - - Создание pending approval с SHA256-хешем действия - - Решения: allow_once, allow_forever, deny - - Проверка is_allowed_forever по хешу действия - - normalized_action хранится в JSON - -#### Skills -- **`duck_core/skills/registry.py`** — `SkillRegistry`: загрузка из `*/skill.yaml`, парсинг procedure/examples/notes, поиск по ключевым словам -- **`skills/analyze_project/`** — единственный скилл: анализ структуры проекта - -#### Experience & Reflection -- **`duck_core/experience/recorder.py`** — запись результатов задач, предложения по обновлению скиллов -- **`duck_core/reflection.py`** — `Reflection.reflect()`: вызов critic-роли для анализа транскрипта задачи - -#### Memory -- **`duck_core/memory/store.py`** — `MemoryStore`: хранение в SQLite с поддержкой scope (global/workspace/conversation), importance, полнотекстовый поиск (LIKE) -- **`duck_core/memory/policy.py`** — `MemoryPolicy`: заглушка (всегда should_store=False) -- **`duck_core/memory/vector_memory.py`** — `VectorMemory`: интеграция с Qdrant для семантического поиска (требует embeddings endpoint) - -#### FastAPI API -- **`duck_core/api.py`** — полный HTTP API (878 строк): - - `POST /v1/chat` — основной чат с сохранением в conversation - - `POST /v1/chat/stream` — streaming чат через SSE - - `POST /v1/tasks/{task_id}/continue/stream` — продолжение после одобрения - - `POST /v1/tasks/{task_id}/password/stream` — ввод sudo-пароля - - `GET/POST /v1/conversations` — управление диалогами - - `GET /v1/tasks`, `GET /v1/tasks/{task_id}/events` — инспекция задач - - `GET /v1/approvals/pending`, `POST /v1/approvals/{id}/allow_once|allow_forever|deny` - - `GET /v1/skills`, `GET /v1/skills/{skill_id}` - - `GET /v1/experience` - - `POST /v1/memory`, `GET /v1/memory`, `GET /v1/memory/search` - - `GET /v1/models/roles`, `GET /v1/models/ping` - - `GET /health`, `GET /v1/status` - - Веб-страницы: `/`, `/approvals`, `/skills`, `/memory`, `/experience` - -#### WebChat UI -- **`duck_core/web/templates/index.html`** — полноценный WebChat с sidebar, conversation list, activity drawer -- **`duck_core/web/static/app.js`** (997 строк) — клиентская логика: - - SSE streaming с парсингом reasoning_delta, content_delta, tool_call_started/finished, tool_approval_requested, tool_password_requested - - Инлайн-терминалы для отображения вызовов инструментов - - Инлайн-кнопки одобрения/запрета действий - - Форма ввода sudo-пароля - - Activity drawer с вкладками Events/Commands/Memory - - Управление диалогами (create, select, load history) - - Enter для отправки, Shift+Enter для новой строки -- **`duck_core/web/static/style.css`** (1002 строки) — светлая тема, responsive layout - -#### Скрипты -- **`scripts/llama/start_main.sh`** — управление llama-server (start/stop/restart/status/logs) -- **`scripts/llama/start_thinker_mtp_experimental.sh`** — экспериментальный MTP endpoint -- **`scripts/llama/build_vulkan.sh`** — сборка llama.cpp с Vulkan -- **`scripts/llama/healthcheck.sh`** — проверка здоровья llama-server -- **`scripts/verify/`** — 7 верификационных скриптов (basic_chat, file_write_read, tool_blocking, models_roles, skills, experience, memory) -- **`scripts/bench/bench_runtime.py`** — бенчмарк - -#### Тесты -- 18 smoke-тестов в `tests/smoke/`: - - `test_models_config.py`, `test_model_client.py`, `test_api_health.py` - - `test_event_log.py`, `test_action_directive_schema.py` - - `test_tool_gateway.py`, `test_approvals.py` - - `test_skill_registry.py`, `test_experience_recorder.py` - - `test_vector_memory.py`, `test_memory_store.py` - - `test_chat_api.py`, `test_conversations.py` - - `test_runtime_reasoning.py`, `test_runtime_tools.py` - - `test_llama_server_connection.py`, `test_llama_service_script.py` - -#### Документация -- 12 файлов в `docs/`: - - `architecture.md`, `how_to_run.md`, `how_to_test.md` - - `web_api.md`, `model_roles.md`, `tool_gateway.md` - - `memory_architecture.md`, `experience_learning.md`, `skills.md` - - `local_llama_server.md`, `performance_mtp.md` - - `superpowers/plans/2026-05-19-ducklm-runtime.md` — план реализации - -#### Docker -- **`docker-compose.memory.yml`** — Qdrant (порты 6333/6334) - -#### Сборка и запуск -- **`pyproject.toml`** — зависимости: fastapi, uvicorn, httpx, pydantic, pyyaml, jinja2, python-dotenv, jsonschema, aiosqlite, qdrant-client -- **`Makefile`** — цели: duck-up, duck-llama-main, duck-api, duck-dev, duck-smoke, duck-test, duck-verify -- **`data/duck.sqlite3`** — рабочая БД SQLite -- **`workspace/`** — рабочая директория для инструментов - ---- - -## 5. План разработки (из `docs/superpowers/plans/2026-05-19-ducklm-runtime.md`) - -План состоит из 4 задач: - -### Task 1: Tests First -- Написать smoke tests для всех компонентов -- ✅ **Выполнено** — 18 тестов созданы - -### Task 2: Runtime Core -- pyproject.toml, .env.example, config/models.yaml -- config.py, model_client.py, events/store.py, tasks/store.py, tasks/state.py -- context_builder.py, runtime_loop.py, api.py -- ✅ **Выполнено** — все компоненты реализованы - -### Task 3: Stage Adapters -- tools/*, approvals/service.py, skills/registry.py -- experience/recorder.py, reflection.py, memory/* -- schemas/action_directive.schema.json -- ✅ **Выполнено** — все компоненты реализованы - -### Task 4: Project Surface -- scripts/llama/*, scripts/verify/*, scripts/bench/* -- web/templates/*, web/static/* -- skills/analyze_project/* -- docker-compose.memory.yml, Makefile, README.md, docs/* -- ✅ **Выполнено** — все компоненты реализованы - ---- - -## 6. Отступления от плана / дополнительные возможности - -### 6.1. Что добавлено сверх плана - -1. **ConversationStore** — полноценное управление диалогами (conversations + conversation_messages таблицы), чего не было явно в плане Task 2. План упоминал только tasks и events. - -2. **Streaming API** — `POST /v1/chat/stream` с SSE, `POST /v1/tasks/{id}/continue/stream`, `POST /v1/tasks/{id}/password/stream`. В плане не было явно указано streaming. - -3. **Password flow** — полный цикл запроса sudo-пароля: `requires_password` → `tool_password_requested` → `/v1/tasks/{id}/password/stream`. В плане не было детализировано. - -4. **Activity Drawer в WebChat** — боковая панель с вкладками Events/Commands/Memory, инлайн-терминалы для инструментов, инлайн-одобрения. Значительно больше, чем «пустая WebChat-страница» из этапа 1 ТЗ. - -5. **Command Audit** — отдельный тип события `command_audit` для shell-команд с полной метаданной (action_type, risk_level, blocked, approved, returncode). - -6. **Scope-aware Memory** — трёхуровневая система скоупов (global/workspace/conversation) с автоматическим infer_scope. - -7. **Skill update proposals** — автоматическая запредложений по обновлению скиллов в `skills/_proposals/`. - -8. **18 тестов вместо 11 запланированных** — добавлены: `test_chat_api`, `test_conversations`, `test_runtime_reasoning`, `test_runtime_tools`, `test_llama_server_connection`, `test_llama_service_script`, `test_memory_store`. - -### 6.2. Что не реализовано (или реализовано частично) - -1. **MemoryPolicy — заглушка.** `MemoryPolicy.classify()` всегда возвращает `should_store=False`. Нет LLM-классификации для автоматического сохранения памяти. - -2. **ContextBuilder — минимальный.** Нет суммаризации старых events, нет обрезки по token budget, нет приоритизации контекста. Просто склеивает memory + history + user message. - -3. **Critic не вызывается автоматически.** `Reflection.reflect()` есть, но не интегрирован в RuntimeLoop — нет автоматической рефлексии после завершения задачи. - -4. **Summary роль не используется.** Нет автоматической суммаризации контекста при превышении budget. - -5. **Coder роль не используется в основном потоке.** RuntimeLoop вызывает только action и thinker. - -6. **Recall роль не определена в конфиге.** В ТЗ упоминается recall, но в `config/models.yaml` её нет. - -7. **Sys_util роль не определена в конфиге.** Аналогично. - -8. **VectorMemory не интегрирован в RuntimeLoop.** Qdrant-поиск не подключён к основному циклу (MemoryStore использует LIKE-поиск, а не векторный). - -9. **WebChat — светлая тема.** В памяти пользователя указано предпочтение тёмной темы, но CSS реализован светлый (`color-scheme: light`, белый фон). - -10. **Нет CLI.** Упомянуто в ТЗ как необязательное, но если понадобится — нужно делать. - -11. **Нет автоматического применения skill patches.** Предложения пишутся в `skills/_proposals/`, но не применяются автоматически. - -### 6.3. Технические заметки - -- **Модель:** Qwen3.6 35B A3B, два варианта — nonMTP (основной, порт 8081) и MTP (экспериментальный, порт 8085) -- **GPU:** Radeon RX580, Vulkan backend, 20 GPU layers -- **llama-server бинарник:** `./vendor/llama.cpp/build/bin/llama-server` -- **ctx_size в .env:** 4096 (хотя в коде Settings дефолт 65536) -- **reasoning-budget:** 512 в .env, `--reasoning-budget 512 --cache-ram 0` -- **Python:** 3.13 (по путям `__pycache__`) - ---- - -## 7. Структура базы данных (SQLite) - -Таблицы: -- **tasks** — задачи (task_id, status, user_message, workspace, debug, final_response, created_at, updated_at) -- **events** — события (id, task_id, sequence, event_type, payload_json, created_at) -- **conversations** — диалоги (id, conversation_id, title, workspace, created_at, updated_at) -- **conversation_messages** — сообщения диалогов (id, conversation_id, role, content, reasoning_content, task_id, status, created_at) -- **approvals** — согласования (id, approval_id, task_id, action_hash, normalized_action_json, status, decision, created_at, updated_at) -- **experience_records** — записи опыта (id, task_id, skill_id, summary, result, what_worked_json, what_failed_json, reusable_lesson, suggested_skill_patch, confidence, created_at) -- **memories** — память (id, memory_id, text, scope, workspace, conversation_id, memory_type, importance, metadata_json, created_at, updated_at) - ---- - -## 8. Когнитивный цикл (как работает RuntimeLoop) - -1. Пользователь отправляет сообщение → `POST /v1/chat` или `/v1/chat/stream` -2. Создаётся Task + событие `task_created` -3. ContextBuilder собирает сообщения (memory + history + user message) -4. **Action loop** (до 4 итераций): - - Модель `action` генерирует JSON directive (schema: action_directive.schema.json) - - ToolGateway выполняет каждый action через соответствующий инструмент - - Если команда требует approval → пауза, создание Approval, ожидание решения - - Если sudo → запрос пароля - - Результаты собираются как tool_observations -5. Thinker получает все tool_observations и формирует финальный ответ -6. Задача завершена → `task_completed` -7. (Опционально) Reflection через critic — **не автоматизировано** - ---- - -## 9. Статус готовности - -| Компонент | Статус | -|-----------|--------| -| Конфигурация | ✅ Готово | -| ModelClient | ✅ Готово | -| TaskStore / EventStore | ✅ Готово | -| ConversationStore | ✅ Готово | -| RuntimeLoop | ✅ Готово | -| ContextBuilder | ⚠️ Минимальный | -| ToolGateway + Tools | ✅ Готово | -| ApprovalService | ✅ Готово | -| SkillRegistry | ✅ Готово | -| ExperienceRecorder | ✅ Готово | -| Reflection | ⚠️ Не интегрирован в loop | -| MemoryStore (SQLite) | ✅ Готово | -| MemoryPolicy | ✅ LLM-based (Phase 1) | -| VectorMemory (Qdrant) | ✅ Интегрирован (Phase 5) | -| FastAPI API | ✅ Готово | -| WebChat UI | ✅ Готово (светлая тема) | -| Streaming | ✅ Готово | -| Password flow | ✅ Готово | -| Smoke tests | ✅ 74 теста | -| Docs | ✅ 12 файлов | -| Scripts | ✅ Готово | - ---- - -## Архитектура эмбеддингов - -### Локальная модель (основной режим) -- **Модель**: `all-MiniLM-L6-v2` (sentence-transformers, 384 размерности) -- **Расположение**: `./models/all-MiniLM-L6-v2/` (safetensors формат) -- **Библиотека**: `sentence-transformers` (добавлен в pyproject.toml) -- **Использование**: `VectorMemory._local_embed()` — загрузка модели через `SentenceTransformer`, кодирование в thread pool - -### Remote endpoint (fallback) -- **Endpoint**: `/v1/embeddings` на llama-server или OpenAI-совместимом сервере -- **Использование**: `VectorMemory._remote_embed()` — HTTP POST запрос - -### Поток данных -1. Зача завершена → `_run_memory_policy()` → LLM классифицирует → `MemoryDecision` -2. Если `should_store=True` → `MemoryStore.add()` (SQLite) + `VectorMemory.add_memory()` (Qdrant) -3. При следующем запросе → `MemoryStore.relevant()` (SQLite LIKE) + `VectorMemory.search_memory()` (semantic) -4. Recall-роль фильтрует релевантные воспоминания через LLM - -### Зависимости -- `sentence-transformers` — для локальной модели -- `qdrant-client` — для Qdrant (уже был) -- Qdrant запускается через `docker-compose.memory.yml` (порт 6333) -| Docker (Qdrant) | ✅ Готово | - -**Общий вывод:** Все 4 задачи плана реализованы. Система представляет собой работающий skeleton с полным когнитивным циклом. Основные направления для дальнейшего развития: интеграция рефлексии и summary в основной цикл, LLM-based MemoryPolicy, векторная память, тёмная тема, расширение ContextBuilder. +1. Пройти live E2E checklist в WebChat на реальной модели. +2. Если Qdrant нужен постоянно, добавить отдельную health-индикацию vector memory в `/v1/status`. +3. При необходимости заменить keyword skill selection на LLM-based selection. +4. Позже мигрировать FastAPI startup на lifespan. diff --git a/duck_core/api.py b/duck_core/api.py index 4c78b46..3bfb4ea 100644 --- a/duck_core/api.py +++ b/duck_core/api.py @@ -207,6 +207,7 @@ def create_app() -> FastAPI: body.debug, history_messages=history, memory_records=memory_records, + skill_summary=await selected_skill_summary(body.message), ) await conversations.add_message( conversation.conversation_id, @@ -252,6 +253,21 @@ def create_app() -> FastAPI: if record.text ] + async def selected_skill_summary(user_request: str) -> str | None: + candidates = await skills.find_candidate_skills(user_request, limit=1) + if not candidates: + return None + skill = candidates[0].skill + parts = [ + f"{skill.id}: {skill.title}", + skill.description, + ] + if skill.procedure: + parts.append("Procedure:\n" + skill.procedure.strip()) + if skill.success_criteria: + parts.append("Success criteria:\n- " + "\n- ".join(skill.success_criteria)) + return "\n\n".join(part for part in parts if part) + @app.get("/v1/conversations") async def list_conversations() -> list[dict[str, Any]]: return [conversation.model_dump() for conversation in await conversations.list()] @@ -327,7 +343,10 @@ def create_app() -> FastAPI: generation_stats = GenerationStats() try: messages = await runtime.context_builder.build_async_messages( - task, history, memory_records + task, + history, + memory_records, + skill_summary=await selected_skill_summary(body.message), ) yield runtime_status( task.task_id, @@ -454,6 +473,7 @@ def create_app() -> FastAPI: "generation_stats": generation_stats.summary(), }, ) + asyncio.create_task(runtime.complete_postprocessing(task.task_id, content)) yield sse( "done", { @@ -562,7 +582,10 @@ def create_app() -> FastAPI: tool_observation = await runtime._run_approved_or_denied_action( task_id, approval.normalized_action, approval.decision ) - messages = await runtime.context_builder.build_async_messages(task) + messages = await runtime.context_builder.build_async_messages( + task, + skill_summary=await selected_skill_summary(task.user_message), + ) tool_observations = [tool_observation] yield runtime_status( task_id, @@ -682,6 +705,7 @@ def create_app() -> FastAPI: "generation_stats": generation_stats.summary(), }, ) + asyncio.create_task(runtime.complete_postprocessing(task_id, content)) yield sse( "done", { @@ -755,7 +779,10 @@ def create_app() -> FastAPI: approval.decision, password=body.password, ) - messages = await runtime.context_builder.build_async_messages(task) + messages = await runtime.context_builder.build_async_messages( + task, + skill_summary=await selected_skill_summary(task.user_message), + ) tool_observations = [tool_observation] yield runtime_status( task_id, @@ -849,6 +876,7 @@ def create_app() -> FastAPI: "generation_stats": generation_stats.summary(), }, ) + asyncio.create_task(runtime.complete_postprocessing(task_id, content)) yield sse( "done", { diff --git a/duck_core/runtime_loop.py b/duck_core/runtime_loop.py index 2156dc3..efcb8d7 100644 --- a/duck_core/runtime_loop.py +++ b/duck_core/runtime_loop.py @@ -61,6 +61,7 @@ class RuntimeLoop: debug: bool = False, history_messages: list[dict[str, str]] | None = None, memory_records: list[dict[str, str]] | None = None, + skill_summary: str | None = None, reflect: bool = True, ) -> ChatResult: task = await self.task_store.create_task(message, workspace, debug) @@ -71,7 +72,7 @@ class RuntimeLoop: ) try: messages = await self.context_builder.build_async_messages( - task, history_messages, memory_records + task, history_messages, memory_records, skill_summary=skill_summary ) tool_observations = await self._run_action_loop(task.task_id, messages, workspace) if any(observation.get("requires_approval") for observation in tool_observations): @@ -130,9 +131,7 @@ class RuntimeLoop: "reasoning_content": response.reasoning_content, }, ) - await self._run_memory_policy(task.task_id, response.content) - if reflect: - await self._run_reflection(task.task_id) + await self.complete_postprocessing(task.task_id, response.content, reflect=reflect) return ChatResult( task_id=task.task_id, status="completed", @@ -151,6 +150,13 @@ class RuntimeLoop: reasoning_content=None, ) + async def complete_postprocessing( + self, task_id: str, final_response: str, reflect: bool = True + ) -> None: + await self._run_memory_policy(task_id, final_response) + if reflect: + await self._run_reflection(task_id) + async def continue_after_approval(self, task_id: str, approval_id: str) -> ChatResult: if self.approval_service is None: return ChatResult( diff --git a/tests/smoke/test_api_stream_chat.py b/tests/smoke/test_api_stream_chat.py index 2c3d9a5..4d24dc8 100644 --- a/tests/smoke/test_api_stream_chat.py +++ b/tests/smoke/test_api_stream_chat.py @@ -1,6 +1,7 @@ from fastapi.testclient import TestClient import json import re +import time from duck_core.model_client import ModelResponse @@ -55,6 +56,79 @@ def test_stream_chat_endpoint_emits_sse_reasoning_and_content(tmp_path, monkeypa assert "answer" in body +def test_stream_chat_runs_memory_policy_and_reflection_after_completion(tmp_path, monkeypatch): + monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3")) + + async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None): + if role == "action": + content = { + "kind": "action_directive", + "intent": "answer directly", + "risk_level": "none", + "actions": [], + } + elif role == "memory_policy": + content = { + "should_store": True, + "memory_type": "preference", + "summary": "User wants streamed chats to retain memory.", + "importance": 0.8, + "scope": "workspace", + "metadata": {}, + } + elif role == "critic": + return ModelResponse( + role=role, + model="local-main", + content="Task completed. Reusable lesson: streamed tasks need post-processing.", + reasoning_content=None, + raw={}, + latency_ms=1.0, + ) + else: + raise AssertionError(f"unexpected role: {role}") + return ModelResponse( + role=role, + model="local-main", + content=json.dumps(content), + reasoning_content=None, + raw={}, + latency_ms=1.0, + ) + + async def fake_stream_chat(self, role, messages): + assert role == "thinker" + yield {"type": "content_delta", "delta": "streamed answer"} + + monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat) + monkeypatch.setattr("duck_core.model_client.ModelClient.stream_chat", fake_stream_chat) + with TestClient(create_app()) as client: + with client.stream( + "POST", + "/v1/chat/stream", + json={"message": "remember this", "workspace": str(tmp_path), "debug": True}, + ) as response: + body = "".join(response.iter_text()) + task_id = re.search(r'"task_id"\s*:\s*"([^"]+)"', body).group(1) + events = [] + for _ in range(20): + events = client.get(f"/v1/tasks/{task_id}/events").json() + if any(event["event_type"] == "reflection_completed" for event in events): + break + time.sleep(0.05) + event_types = [event["event_type"] for event in events] + memory = client.get("/v1/memory", params={"workspace": str(tmp_path)}).json() + experience = client.get("/v1/experience").json() + + assert response.status_code == 200 + assert "event: done" in body + assert "memory_policy_decision" in event_types + assert "memory_stored" in event_types + assert "reflection_completed" in event_types + assert memory["results"][0]["text"] == "User wants streamed chats to retain memory." + assert len(experience) == 1 + + def test_stream_chat_endpoint_executes_tool_before_streaming_answer(tmp_path, monkeypatch): monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3")) (tmp_path / "note.txt").write_text("stream tool content") @@ -115,6 +189,49 @@ def test_stream_chat_endpoint_executes_tool_before_streaming_answer(tmp_path, mo assert "event: done" in body +def test_stream_chat_injects_candidate_skill_summary(tmp_path, monkeypatch): + monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3")) + captured_messages = [] + + async def fake_chat(self, role, messages, temperature=None, max_output_tokens=None, response_format=None): + assert role == "action" + captured_messages.extend(messages) + return ModelResponse( + role=role, + model="local-main", + content=json.dumps( + { + "kind": "action_directive", + "intent": "answer directly", + "risk_level": "none", + "actions": [], + } + ), + reasoning_content=None, + raw={}, + latency_ms=1.0, + ) + + async def fake_stream_chat(self, role, messages): + yield {"type": "content_delta", "delta": "ok"} + + monkeypatch.setattr("duck_core.model_client.ModelClient.chat", fake_chat) + monkeypatch.setattr("duck_core.model_client.ModelClient.stream_chat", fake_stream_chat) + client = TestClient(create_app()) + + with client.stream( + "POST", + "/v1/chat/stream", + json={"message": "analyze repository structure", "workspace": str(tmp_path), "debug": True}, + ) as response: + body = "".join(response.iter_text()) + + assert response.status_code == 200 + assert "event: done" in body + assert any("Active skill:" in message["content"] for message in captured_messages) + assert any("analyze_project" in message["content"] for message in captured_messages) + + def test_stream_chat_requests_approval_for_directory_outside_workspace(tmp_path, monkeypatch): monkeypatch.setenv("DUCK_DB_PATH", str(tmp_path / "duck.sqlite3")) workspace = tmp_path / "workspace"