# DuckLM — техническое задание на разработку локальной агентной системы ## 0. Назначение проекта `DuckLM` — локальная агентная система, которая работает как самостоятельный runtime поверх локальных языковых моделей. Система должна уметь: - принимать сообщения от человека через WebChat; - принимать задачи от внешних агентов и тестов через HTTP API; - использовать локальные LLM через `llama-server`; - вести состояние задач; - записывать события выполнения; - безопасно запускать инструменты; - работать с навыками; - сохранять опыт; - использовать память; - анализировать собственные ошибки; - постепенно улучшать поведение через опыт и предложения по обновлению навыков. Главная идея: ```text DuckLM — это не inference server. DuckLM — это когнитивный runtime: состояние → контекст → мышление → намерение → действие → наблюдение → рефлексия → память → опыт. ``` --- # 1. Архитектурные принципы ## 1.1. Использовать готовые компоненты DuckLM должна использовать готовые решения там, где это разумно. ```text llama-server → inference SQLite/PostgreSQL → события, задачи, approvals, experience records Qdrant → semantic memory FastAPI → HTTP API WebChat → интерфейс человека ToolGateway → безопасный запуск инструментов Duck Core → когнитивный цикл ``` Не писать с нуля: - LLM inference server; - model scheduler; - vector database; - OpenAI-compatible API; - MCP-протокол; - production-grade sandbox; - сложный workflow engine; - бесконечный JSON repair loop. Писать с нуля: - Duck Core; - ModelClient; - ContextBuilder; - RuntimeLoop; - EventStore; - TaskStore; - ToolGateway; - ApprovalService; - SkillRegistry; - ExperienceRecorder; - MemoryPolicy; - FastAPI API; - WebChat; - verification scripts; - smoke tests; - документацию. --- ## 1.2. Web/API first Основные интерфейсы: ```text WebChat → для человека HTTP API → для кодера, тестов и внешних агентов ``` CLI в обязательную часть не входит. Если позже понадобится CLI, он должен быть тонким клиентом поверх HTTP API. --- ## 1.3. Роли моделей логические Роли моделей: ```text thinker critic coder action recall summary sys_util ``` являются логическими ролями, а не обязательно разными физическими моделями. Одна физическая модель может использоваться сразу для всех ролей: ```text thinker = local-main critic = local-main coder = local-main action = local-main recall = local-main summary = local-main ``` Различие между ролями задаётся комбинацией: - system prompt; - temperature; - max_output_tokens; - response_format; - structured_output; - memory scope; - tool permissions; - context builder mode; - inference endpoint. Пример: ```text thinker — свободное рассуждение, temperature 0.4 critic — проверка и рефлексия, temperature 0.1 coder — code-oriented prompt, temperature 0.2 action — strict JSON schema, temperature 0.0 summary — сжатие контекста, temperature 0.1 ``` Код не должен предполагать, что разные роли используют разные модели. Правильно: ```python await model_client.chat(role="thinker", ...) await model_client.chat(role="critic", ...) await model_client.chat(role="coder", ...) await model_client.chat(role="action", response_format=...) ``` `ModelClient` по конфигу решает: ```text какой base_url использовать какое имя модели передать какую температуру поставить какой system prompt применить какой max_output_tokens поставить нужен ли response_format ``` --- # 2. Параметры модели ## 2.1. Request-level параметры Эти параметры можно менять на каждый запрос без перезапуска модели: - system prompt; - messages; - temperature; - top_p; - top_k; - min_p; - max_output_tokens; - stop; - response_format; - JSON schema; - tool definitions. Одна загруженная модель в одном `llama-server` может обслуживать разные роли с разными prompt, temperature и output limits. --- ## 2.2. Backend-level параметры Эти параметры обычно требуют отдельного запуска сервера: - путь к GGUF-модели; - ctx-size; - GPU layers / offload; - flash-attn; - KV cache configuration; - speculative decoding / MTP; - server port / host; - parallel slots; - chat template startup config; - quant/offload mode. Пример: ```text 8081 local-main обычный 8085 local-main-mtp экспериментальный ``` MTP/speculative decoding не включать по умолчанию для `action` JSON endpoint. --- # 3. Token budget и context budget Нужно явно разделять: ```text ctx_size общий размер контекстного окна модели max_output_tokens сколько модель может сгенерировать за один вызов max_input_tokens сколько токенов можно собрать во входной prompt recent_events_tokens сколько истории событий можно включить memory_tokens сколько памяти можно включить skill_tokens сколько текста skill/procedure/examples можно включить ``` Пример `.env.example`: ```env DUCK_CTX_SIZE=65536 DUCK_MAX_INPUT_TOKENS=49152 DUCK_MAX_RECENT_EVENTS_TOKENS=12000 DUCK_MAX_MEMORY_TOKENS=8000 DUCK_MAX_SKILL_TOKENS=6000 ``` Рекомендуемые output limits: ```text thinker: 8192 critic: 4096 coder: 16384 action: 2048 recall: 2048 summary: 4096 ``` `action` может иметь небольшой output limit, потому что action directive должен быть коротким. `thinker` и `coder` должны иметь более крупный output limit. --- # 4. ContextBuilder `ContextBuilder` не должен бездумно добавлять всю историю общения в каждый запрос. Контекст должен собираться из: - текущего user message; - active task state; - selected skill; - compact task summary; - recent relevant events; - relevant tool observations; - retrieved memory; - system prompt текущей роли. Если контекст превышает budget: 1. сохранить текущий user message; 2. сохранить active task state; 3. сохранить selected skill summary; 4. сохранить последние важные observations; 5. суммаризировать старые events; 6. обрезать низкорелевантную memory; 7. не превышать context window молча. --- # 5. Целевая архитектура ```text ┌─────────────────────────────────────────────┐ │ WebChat │ │ интерфейс человека к DuckLM │ └─────────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ FastAPI │ │ интерфейс кодера, тестов и агентов │ └─────────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────────┐ │ Duck Core │ │ │ │ RuntimeLoop │ │ TaskState │ │ ContextBuilder │ │ ModelClient │ │ SkillRegistry │ │ ToolGateway │ │ ApprovalService │ │ Reflection │ │ MemoryPolicy │ │ ExperienceRecorder │ └───────────────┬───────────────┬─────────────┘ │ │ ▼ ▼ ┌───────────────────────┐ ┌────────────────────────┐ │ llama-server │ │ SQLite/PostgreSQL │ │ OpenAI-compatible API │ │ events/tasks/approvals │ └───────────────────────┘ └────────────────────────┘ │ ▼ ┌───────────────────────┐ │ Qdrant / Vector Store │ │ semantic memory │ └───────────────────────┘ ``` --- # 6. Структура проекта Создать структуру: ```text ducklm/ duck_core/ __init__.py api.py config.py model_client.py runtime_loop.py context_builder.py events/ __init__.py store.py tasks/ __init__.py store.py state.py tools/ __init__.py base.py gateway.py file_read.py file_write.py shell_exec_safe.py approvals/ __init__.py service.py skills/ __init__.py registry.py experience/ __init__.py recorder.py memory/ __init__.py vector_memory.py policy.py schemas/ action_directive.schema.json web/ templates/ index.html task.html approvals.html skills.html memory.html experience.html static/ app.js style.css prompts/ roles/ thinker.md action.md critic.md coder.md summary.md skills/ analyze_project/ skill.yaml procedure.md examples.md notes.md config/ models.yaml scripts/ llama/ start_main.sh start_thinker_mtp_experimental.sh healthcheck.sh verify/ verify_basic_chat.sh verify_file_write_read.sh verify_tool_blocking.sh verify_models_roles.sh verify_skills.sh verify_experience.sh verify_memory.sh bench/ bench_runtime.py tests/ smoke/ docs/ data/ workspace/ .env.example docker-compose.memory.yml Makefile pyproject.toml README.md ``` --- # 7. Этап 1 — базовый проект и конфигурация ## 7.1. Цель Создать запускаемый skeleton проекта с конфигурацией, зависимостями, `.env.example`, `config/models.yaml`, базовым FastAPI и пустой WebChat-страницей. --- ## 7.2. pyproject.toml Минимальные зависимости: ```toml [project] name = "ducklm" version = "0.1.0" description = "Local agent runtime with WebChat, API, tools, memory and experience" requires-python = ">=3.11" dependencies = [ "fastapi", "uvicorn", "httpx", "pydantic", "pyyaml", "jinja2", "python-dotenv", "jsonschema", "aiosqlite", "qdrant-client" ] [project.optional-dependencies] dev = [ "pytest", "pytest-asyncio", "ruff" ] ``` --- ## 7.3. .env.example Создать: ```env DUCK_LLAMA_SERVER_BIN=/usr/local/bin/llama-server DUCK_MAIN_MODEL_PATH=/models/main.gguf DUCK_MAIN_PORT=8081 DUCK_CTX_SIZE=65536 DUCK_N_GPU_LAYERS=99 DUCK_HOST=127.0.0.1 DUCK_API_HOST=127.0.0.1 DUCK_API_PORT=8000 DUCK_WORKSPACE=./workspace DUCK_DB_PATH=./data/duck.sqlite3 DUCK_MAX_INPUT_TOKENS=49152 DUCK_MAX_RECENT_EVENTS_TOKENS=12000 DUCK_MAX_MEMORY_TOKENS=8000 DUCK_MAX_SKILL_TOKENS=6000 QDRANT_URL=http://127.0.0.1:6333 DUCK_SKIP_LIVE_LLM_TESTS=0 ``` По умолчанию API и `llama-server` должны слушать только `127.0.0.1`. Если пользователь явно указывает `0.0.0.0`, в логах должно быть предупреждение: ```text WARNING: DuckLM API is listening on 0.0.0.0. This may expose local tool execution endpoints. ``` --- ## 7.4. config/models.yaml Создать: ```yaml default_provider: llama_server models: thinker: provider: llama_server base_url: http://127.0.0.1:8081/v1 model: local-main purpose: free_cognition structured_output: false temperature: 0.4 max_output_tokens: 8192 system_prompt: prompts/roles/thinker.md critic: provider: llama_server base_url: http://127.0.0.1:8081/v1 model: local-main purpose: reflection structured_output: false temperature: 0.1 max_output_tokens: 4096 system_prompt: prompts/roles/critic.md coder: provider: llama_server base_url: http://127.0.0.1:8081/v1 model: local-main purpose: code_generation structured_output: false temperature: 0.2 max_output_tokens: 16384 system_prompt: prompts/roles/coder.md action: provider: llama_server base_url: http://127.0.0.1:8081/v1 model: local-main purpose: action_directive structured_output: true temperature: 0.0 max_output_tokens: 2048 system_prompt: prompts/roles/action.md response_schema: duck_core/schemas/action_directive.schema.json summary: provider: llama_server base_url: http://127.0.0.1:8081/v1 model: local-main purpose: context_summary structured_output: false temperature: 0.1 max_output_tokens: 4096 system_prompt: prompts/roles/summary.md ``` --- # 8. Этап 2 — llama-server integration и ModelClient ## 8.1. Скрипт запуска llama-server Создать: ```text scripts/llama/start_main.sh ``` ```bash #!/usr/bin/env bash set -euo pipefail : "${DUCK_MAIN_MODEL_PATH:?DUCK_MAIN_MODEL_PATH is required}" "${DUCK_LLAMA_SERVER_BIN:-llama-server}" \ -m "${DUCK_MAIN_MODEL_PATH}" \ --alias local-main \ --host "${DUCK_HOST:-127.0.0.1}" \ --port "${DUCK_MAIN_PORT:-8081}" \ -c "${DUCK_CTX_SIZE:-65536}" \ -ngl "${DUCK_N_GPU_LAYERS:-99}" \ --flash-attn on \ --cache-prompt \ --metrics ``` Создать: ```text scripts/llama/healthcheck.sh ``` ```bash #!/usr/bin/env bash set -euo pipefail BASE_URL="${1:-http://127.0.0.1:8081/v1}" curl -fsS "${BASE_URL}/models" >/dev/null echo "OK: ${BASE_URL}" ``` --- ## 8.2. ModelClient Создать: ```text duck_core/model_client.py ``` Требования: 1. Читать `config/models.yaml`. 2. Вызывать модель по логической роли. 3. Работать через OpenAI-compatible API. 4. Поддерживать role-specific `system_prompt`. 5. Поддерживать role-specific `temperature`. 6. Поддерживать role-specific `max_output_tokens`. 7. Поддерживать `response_format`. 8. Логировать latency. 9. Логировать usage tokens, если backend их возвращает. 10. Корректно обрабатывать ошибки соединения. 11. Не требовать уникальности моделей для ролей. Интерфейс: ```python from dataclasses import dataclass from typing import Any @dataclass class ModelResponse: role: str model: str content: str raw: dict[str, Any] latency_ms: float prompt_tokens: int | None = None completion_tokens: int | None = None total_tokens: int | None = None class ModelClient: def __init__(self, config_path: str = "config/models.yaml"): ... async def chat( self, role: str, messages: list[dict[str, str]], temperature: float | None = None, max_output_tokens: int | None = None, response_format: dict | None = None, ) -> ModelResponse: ... ``` --- # 9. Этап 3 — Web/API runtime loop ## 9.1. Цель Сделать минимальный живой вертикальный срез: ```text человек пишет в WebChat ↓ FastAPI создаёт task ↓ Duck Core вызывает llama-server ↓ ответ пишется в SQLite event log ↓ WebChat показывает ответ и event timeline ``` На этом этапе не делать: - tools; - approvals; - skills; - experience; - Qdrant; - MTP. --- ## 9.2. SQLite schema Создать EventStore и TaskStore. Минимальные таблицы: ```sql create table if not exists tasks ( task_id text primary key, status text not null, user_message text not null, workspace text, debug integer not null default 0, final_response text, created_at text not null, updated_at text not null ); create table if not exists events ( id integer primary key autoincrement, task_id text not null, sequence integer not null, event_type text not null, payload_json text not null, created_at text not null ); create unique index if not exists idx_events_task_sequence on events(task_id, sequence); ``` Минимальные статусы задач: ```text running completed failed cancelled ``` Минимальные события: ```text task_created model_call_started cognition_response model_call_finished task_completed task_failed ``` --- ## 9.3. RuntimeLoop Создать: ```text duck_core/runtime_loop.py ``` Минимальный цикл: ```text POST /v2/chat ↓ create task ↓ write task_created ↓ build basic context ↓ call thinker ↓ write cognition_response ↓ save final_response ↓ write task_completed ↓ return response ``` --- ## 9.4. FastAPI endpoints Создать: ```text duck_core/api.py ``` Минимальные endpoints: ```text GET /health GET /v1/status GET /v1/models/roles GET /v1/models/ping POST /v1/chat POST /v1/tasks GET /v1/tasks GET /v1/tasks/{task_id} GET /v1/tasks/{task_id}/events GET /v1/tasks/{task_id}/stream ``` `POST /v1/chat` — основной человекоподобный вход. Пример запроса: ```json { "message": "Скажи коротко, что ты DuckLM", "workspace": "./workspace", "debug": true } ``` Пример ответа: ```json { "task_id": "task_20260519_001", "status": "completed", "final_response": "Я DuckLM, локальная агентная система с Web/API-интерфейсом." } ``` --- ## 9.5. WebChat Сделать минимальный WebChat. Допустимо: - FastAPI templates; - static HTML; - простой JS через `fetch`; - SSE для event timeline. Главная страница `/` должна содержать: - поле сообщения; - поле workspace; - checkbox debug; - кнопку Run; - блок final response; - блок event timeline. --- ## 9.6. Проверка этапа Запуск: ```bash cp .env.example .env # прописать DUCK_MAIN_MODEL_PATH bash scripts/llama/start_main.sh ``` Во втором терминале: ```bash python -m duck_core.api ``` Проверка: ```bash curl http://127.0.0.1:8000/health curl http://127.0.0.1:8000/v1/models/roles curl http://127.0.0.1:8000/v1/models/ping ``` Запуск задачи: ```bash curl -X POST http://127.0.0.1:8000/v1/chat \ -H "Content-Type: application/json" \ -d '{ "message": "Скажи коротко, что ты DuckLM", "workspace": "./workspace", "debug": true }' ``` Проверить events: ```bash curl http://127.0.0.1:8000/v1/tasks//events ``` Ожидаемые события: ```text task_created model_call_started cognition_response model_call_finished task_completed ``` --- # 10. Этап 4 — cognition/action split ## 10.1. Цель Разделить свободное мышление и машинное намерение. ```text cognition_response свободный текст, понимание задачи, план, риски action_directive строгий JSON для ToolGateway ``` Модель не должна думать в JSON. JSON используется только как форма внешнего действия. --- ## 10.2. Action directive schema Создать: ```text duck_core/schemas/action_directive.schema.json ``` ```json { "type": "object", "required": ["kind", "intent", "risk_level", "actions"], "additionalProperties": false, "properties": { "kind": { "type": "string", "enum": ["action_directive"] }, "intent": { "type": "string", "minLength": 1 }, "risk_level": { "type": "string", "enum": ["none", "low", "medium", "high", "critical"] }, "actions": { "type": "array", "minItems": 0, "items": { "type": "object", "required": ["tool", "args"], "additionalProperties": false, "properties": { "tool": { "type": "string", "minLength": 1 }, "args": { "type": "object" }, "reason": { "type": "string" } } } }, "memory_hints": { "type": "array", "items": { "type": "string" } }, "expected_observations": { "type": "array", "items": { "type": "string" } }, "stop_reason": { "type": "string" } } } ``` --- ## 10.3. Structured output и retry Правила: 1. `action_directive` генерируется через structured output, если backend это поддерживает. 2. Если backend не поддерживает JSON schema, явно записать это в event log. 3. Fallback на plain JSON допускается только если включён в config. 4. После генерации directive валидируется локально. 5. Разрешён максимум один retry. 6. Retry чинит только directive. 7. Бесконечный JSON repair loop запрещён. Запрещено: ```python while not valid_json: call_model_to_fix_json() ``` --- # 11. Этап 5 — ToolGateway ## 11.1. Цель Добавить безопасное выполнение действий через tools. Модель не запускает инструменты напрямую. Модель создаёт `action_directive`. `ToolGateway`: 1. принимает action directive; 2. проверяет tool; 3. проверяет risk level; 4. нормализует действие; 5. проверяет permissions; 6. выполняет разрешённое действие; 7. пишет observation в event log; 8. возвращает результат в runtime loop. --- ## 11.2. Tool interface Создать: ```text duck_core/tools/base.py ``` ```python from typing import Protocol, Any from pydantic import BaseModel, Field class ToolResult(BaseModel): ok: bool output: str | None = None error: str | None = None metadata: dict[str, Any] = Field(default_factory=dict) class Tool(Protocol): name: str risk_level: str async def run(self, args: dict[str, Any]) -> ToolResult: ... ``` --- ## 11.3. Минимальные tools Создать: ```text duck_core/tools/file_read.py duck_core/tools/file_write.py duck_core/tools/shell_exec_safe.py ``` ### file_read Требования: - читать только внутри workspace; - запретить path traversal; - запретить чтение `/etc/shadow`; - запретить чтение `~/.ssh` без explicit approval; - запретить чтение `.env` без explicit approval; - ограничить максимальный размер файла. ### file_write Требования: - писать только внутри workspace; - запретить path traversal; - не перезаписывать существующий файл без backup или approval; - создавать каталоги только внутри workspace; - возвращать metadata: path, bytes_written, created/updated. ### shell_exec_safe Allowlist: ```text pwd ls cat head tail grep find python -m pytest pytest git status git diff git log ``` Blocklist: ```text rm sudo su dd mkfs mount umount chmod -R chown -R curl | sh wget | sh shutdown reboot poweroff systemctl service apt install apt remove pacman -S pacman -R pip install npm install -g ``` Команды вне allowlist требуют approval. --- # 12. Этап 6 — approvals и resume ## 12.1. Цель Добавить подтверждение рискованных действий и продолжение задачи после решения пользователя. --- ## 12.2. Таблица approvals ```sql create table if not exists approvals ( id integer primary key autoincrement, approval_id text not null unique, task_id text not null, action_hash text not null, normalized_action_json text not null, status text not null, decision text, created_at text not null, updated_at text not null ); ``` Статусы задачи: ```text running waiting_for_approval completed failed cancelled ``` Если действие требует approval: 1. создать pending approval; 2. перевести task в `waiting_for_approval`; 3. показать approval в Web UI; 4. позволить approve/deny через API; 5. после allow_once/allow_forever продолжить задачу через `/continue`. --- ## 12.3. Approval API Добавить: ```text GET /v1/approvals/pending POST /v1/approvals/{approval_id}/allow_once POST /v1/approvals/{approval_id}/allow_forever POST /v1/approvals/{approval_id}/deny POST /v1/tasks/{task_id}/continue POST /v1/tasks/{task_id}/cancel ``` Инвариант: ```text Allow forever = только exact normalized action hash. ``` Это не широкое разрешение на похожие действия. --- ## 12.4. Approval UI Web UI должен показывать pending approval: ```text DuckLM хочет выполнить действие: tool: shell_exec_safe command: pytest tests/smoke -v risk: low reason: Need to run tests [Allow once] [Allow forever for exact action] [Deny] ``` --- # 13. Этап 7 — Skills ## 13.1. Цель Добавить процедурную память. Skill — это не if/else-автомат. Skill — это описание способа решения типа задач: - какие tools нужны; - какие риски есть; - какие шаги обычно полезны; - какие критерии успеха; - какие ошибки уже известны; - какие примеры есть. --- ## 13.2. Структура skill Создать: ```text skills/analyze_project/ skill.yaml procedure.md examples.md notes.md ``` Пример `skill.yaml`: ```yaml id: analyze_project title: Analyze project structure description: Inspect repository structure and summarize architecture. version: 1 tags: - code - repository - analysis required_tools: - file_read - shell_exec_safe risk_level: low inputs: - workspace_path outputs: - architecture_summary - risks - suggested_next_steps success_criteria: - repository structure inspected - major modules identified - no destructive commands executed - summary is grounded in actual files ``` --- ## 13.3. SkillRegistry Создать: ```text duck_core/skills/registry.py ``` Интерфейс: ```python class SkillRegistry: def load_skills(self) -> list[Skill]: ... def get_skill(self, skill_id: str) -> Skill | None: ... async def find_candidate_skills( self, user_request: str, limit: int = 3, ) -> list[SkillCandidate]: ... ``` На первом этапе допустимо: - keyword prefilter по title/tags/description; - LLM selection через thinker/action. Не делать огромный if/else-router. --- ## 13.4. Skills API Добавить: ```text GET /v1/skills GET /v1/skills/{skill_id} ``` Web UI: ```text /skills ``` --- # 14. Этап 8 — Experience и Reflection ## 14.1. Цель Добавить самоулучшение через опыт. Не через автоматическое изменение кода. А через: ```text task ↓ reflection ↓ experience record ↓ skill update proposal ↓ human approval later ``` --- ## 14.2. Reflection Создать: ```text duck_core/reflection.py ``` Reflection должна отвечать: 1. Что пытались сделать? 2. Получилось ли? 3. Что сработало? 4. Что не сработало? 5. Были ли лишние model calls? 6. Были ли лишние tool calls? 7. Застревала ли система? 8. Была ли проблема с JSON/action directive? 9. Нужно ли что-то запомнить? 10. Нужно ли предложить изменение skill? Reflection использует роль `critic`. `critic` может быть той же физической моделью, что и `thinker`. --- ## 14.3. ExperienceRecord Добавить таблицу: ```sql create table if not exists experience_records ( id integer primary key autoincrement, task_id text not null, skill_id text, summary text not null, result text not null, what_worked_json text, what_failed_json text, reusable_lesson text, suggested_skill_patch text, confidence real, created_at text not null ); ``` Формат: ```json { "task_id": "...", "skill_id": "optional", "summary": "What was attempted", "result": "success/failure/partial", "what_worked": ["..."], "what_failed": ["..."], "reusable_lesson": "...", "suggested_skill_patch": "optional", "confidence": 0.7 } ``` --- ## 14.4. Skill update proposals Если reflection считает, что skill надо улучшить, создать файл: ```text skills/_proposals/_.patch.md ``` Формат: ```markdown # Skill update proposal Skill: analyze_project ## Reason ... ## Proposed changes ... ## Evidence Task id: ... ## Risk Low / medium / high. ## Requires human approval Yes. ``` Запрещено автоматически применять skill patch без approval. --- ## 14.5. Experience API Добавить: ```text GET /v1/experience GET /v1/experience/{id} ``` Web UI: ```text /experience ``` --- # 15. Этап 9 — Semantic memory ## 15.1. Цель Добавить semantic memory через готовый vector store. --- ## 15.2. Qdrant compose Создать: ```text docker-compose.memory.yml ``` ```yaml services: qdrant: image: qdrant/qdrant:latest ports: - "6333:6333" - "6334:6334" volumes: - qdrant_storage:/qdrant/storage volumes: qdrant_storage: ``` --- ## 15.3. VectorMemory adapter Создать: ```text duck_core/memory/vector_memory.py ``` Интерфейс: ```python from typing import Any class VectorMemory: async def add_memory( self, text: str, metadata: dict[str, Any] | None = None, ) -> str: ... async def search_memory( self, query: str, limit: int = 5, ) -> list[dict[str, Any]]: ... ``` Embeddings: 1. Если `llama-server /v1/embeddings` доступен — использовать его. 2. Если embeddings пока недоступны — сделать явный adapter stub и xfail-test. 3. Не писать самодельный embedding algorithm. --- ## 15.4. MemoryPolicy Создать: ```text duck_core/memory/policy.py ``` Типы памяти: ```text event semantic_fact preference procedure experience skill_update_candidate ``` Пример результата: ```json { "should_store": true, "memory_type": "experience", "summary": "The action directive schema failed because reasoning and JSON were mixed.", "importance": 0.8, "metadata": { "task_id": "...", "source": "reflection" } } ``` Допустима LLM-классификация через `action` role со structured JSON. Не делать жёстких эвристик вида: ```python if "remember" in text: ... ``` --- ## 15.5. Memory API Добавить: ```text GET /v1/memory/search?q=... ``` Web UI: ```text /memory ``` --- # 16. Этап 10 — Performance и MTP experiments ## 16.1. Цель Добавить экспериментальные режимы ускорения inference. MTP/speculative decoding — уровень inference backend, а не Duck Core. --- ## 16.2. MTP script Создать: ```text scripts/llama/start_thinker_mtp_experimental.sh ``` ```bash #!/usr/bin/env bash set -euo pipefail : "${DUCK_MAIN_MODEL_PATH:?DUCK_MAIN_MODEL_PATH is required}" LLAMA_BIN="${DUCK_LLAMA_SERVER_BIN:-llama-server}" if ! "${LLAMA_BIN}" --help | grep -qi "spec"; then echo "This llama-server build does not expose speculative/MTP flags." exit 1 fi "${LLAMA_BIN}" \ -m "${DUCK_MAIN_MODEL_PATH}" \ --alias local-main-mtp \ --host "${DUCK_HOST:-127.0.0.1}" \ --port "${DUCK_MAIN_MTP_PORT:-8085}" \ -c "${DUCK_CTX_SIZE:-65536}" \ -ngl "${DUCK_N_GPU_LAYERS:-99}" \ --flash-attn on \ --cache-prompt \ --metrics \ ${DUCK_MTP_FLAGS:-} ``` MTP не включать по умолчанию для action JSON endpoint. --- ## 16.3. Benchmark Создать: ```text scripts/bench/bench_runtime.py ``` Метрики: - total runtime seconds; - LLM calls count; - latency per LLM call; - prompt tokens; - completion tokens; - total tokens; - tool calls count; - JSON directive validity; - retry count; - memory writes count; - experience record created yes/no; - selected skill; - model role mapping. Тестовые задачи: ```text 1. "Скажи коротко, что ты DuckLM." 2. "Создай tmp/duck_test_note.md с текстом hello duck и прочитай его обратно." 3. "Посмотри структуру проекта и кратко опиши модули." 4. "Найди TODO/FIXME в проекте." 5. "Запусти тесты и кратко объясни результат." ``` Бенчмарк должен выводить: ```text role -> base_url/model ``` --- # 17. Verification scripts Создать: ```text scripts/verify/ verify_basic_chat.sh verify_file_write_read.sh verify_tool_blocking.sh verify_models_roles.sh verify_skills.sh verify_experience.sh verify_memory.sh ``` Скрипты должны использовать HTTP API, а не CLI. Пример `verify_basic_chat.sh`: ```bash #!/usr/bin/env bash set -euo pipefail BASE_URL="${DUCK_API_URL:-http://127.0.0.1:8000}" curl -fsS "${BASE_URL}/health" curl -fsS -X POST "${BASE_URL}/v1/chat" \ -H "Content-Type: application/json" \ -d '{ "message": "Скажи коротко, что ты DuckLM", "debug": true }' ``` Пример `verify_file_write_read.sh`: ```bash #!/usr/bin/env bash set -euo pipefail BASE_URL="${DUCK_API_URL:-http://127.0.0.1:8000}" RESPONSE="$(curl -fsS -X POST "${BASE_URL}/v1/chat" \ -H "Content-Type: application/json" \ -d '{ "message": "Создай tmp/duck_test_note.md с текстом hello duck и прочитай его обратно", "workspace": "./workspace", "debug": true }')" echo "${RESPONSE}" ``` --- # 18. Makefile Создать: ```makefile duck-up: docker compose -f docker-compose.memory.yml up -d @echo "Memory services started." @echo "Start llama-server:" @echo "bash scripts/llama/start_main.sh" duck-llama-main: bash scripts/llama/start_main.sh duck-llama-health: bash scripts/llama/healthcheck.sh http://127.0.0.1:8081/v1 duck-api: python -m duck_core.api duck-dev: docker compose -f docker-compose.memory.yml up -d @echo "Start llama-server in another terminal:" @echo "bash scripts/llama/start_main.sh" @echo "Then run:" @echo "make duck-api" @echo "Open:" @echo "http://127.0.0.1:8000/" duck-open: @echo "Open web UI:" @echo "http://127.0.0.1:8000/" duck-smoke: python -m pytest tests/smoke -v duck-test: python -m pytest -v duck-verify: bash scripts/verify/verify_basic_chat.sh bash scripts/verify/verify_file_write_read.sh bash scripts/verify/verify_tool_blocking.sh bash scripts/verify/verify_models_roles.sh ``` --- # 19. Smoke tests Создать: ```text tests/smoke/test_models_config.py tests/smoke/test_model_client.py tests/smoke/test_llama_server_connection.py tests/smoke/test_api_health.py tests/smoke/test_chat_api.py tests/smoke/test_event_log.py tests/smoke/test_action_directive_schema.py tests/smoke/test_tool_gateway.py tests/smoke/test_approvals.py tests/smoke/test_skill_registry.py tests/smoke/test_experience_recorder.py tests/smoke/test_vector_memory.py ``` Live LLM tests должны пропускаться, если: ```text DUCK_SKIP_LIVE_LLM_TESTS=1 ``` --- # 20. Документация Создать: ```text docs/architecture.md docs/how_to_run.md docs/how_to_test.md docs/local_llama_server.md docs/model_roles.md docs/web_api.md docs/tool_gateway.md docs/skills.md docs/experience_learning.md docs/memory_architecture.md docs/performance_mtp.md ``` ## docs/how_to_run.md Описать: 1. как установить зависимости; 2. как указать путь к GGUF-модели; 3. как запустить `llama-server`; 4. как запустить DuckLM API; 5. как открыть WebChat; 6. как отправить первую задачу; 7. как смотреть task events; 8. как смотреть approvals; 9. как остановить сервисы. ## docs/model_roles.md Описать: 1. роль модели — логическая роль; 2. thinker/critic/coder/action могут использовать одну модель; 3. разные роли могут отличаться prompt/temperature/schema/context; 4. как настроить одну модель на все роли; 5. как настроить разные модели на разные роли; 6. какие параметры request-level; 7. какие параметры backend-level. ## docs/web_api.md Описать endpoints: ```text GET /health GET /v1/status GET /v1/models/roles GET /v1/models/ping POST /v1/chat POST /v1/tasks GET /v1/tasks GET /v1/tasks/{task_id} GET /v1/tasks/{task_id}/events GET /v1/tasks/{task_id}/stream GET /v1/approvals/pending POST /v1/approvals/{approval_id}/allow_once POST /v1/approvals/{approval_id}/allow_forever POST /v1/approvals/{approval_id}/deny GET /v1/skills GET /v1/skills/{skill_id} GET /v1/experience GET /v1/experience/{id} GET /v1/memory/search?q=... ``` --- # 21. Критерии готовности по этапам ## Этап 1 готов, если: - создана структура проекта; - есть `pyproject.toml`; - есть `.env.example`; - есть `config/models.yaml`; - есть базовый FastAPI; - есть пустая WebChat-страница; - проект запускается без синтаксических ошибок. ## Этап 2 готов, если: - `llama-server` запускается через `scripts/llama/start_main.sh`; - `/v1/models` отвечает; - `ModelClient` читает `config/models.yaml`; - одна модель может быть назначена на все роли; - `GET /v1/models/roles` показывает роли; - `GET /v1/models/ping` проверяет доступность backend-а. ## Этап 3 готов, если: - `POST /v1/chat` работает; - WebChat позволяет отправить сообщение; - task создаётся; - events пишутся в SQLite; - task timeline отображается в WebChat; - final response отображается в WebChat. ## Этап 4 готов, если: - `cognition_response` отделён от `action_directive`; - action directive schema создана; - action directive валидируется; - бесконечного JSON repair loop нет; - разрешён максимум один retry. ## Этап 5 готов, если: - ToolGateway существует; - file_read работает внутри workspace; - file_write работает внутри workspace; - shell_exec_safe работает для allowlist; - опасные команды блокируются; - tool observations пишутся в event log. ## Этап 6 готов, если: - approvals table создана; - waiting_for_approval status работает; - pending approvals видны в Web UI; - allow_once работает; - allow_forever работает только для exact normalized action hash; - deny работает; - `/continue` продолжает задачу после approval. ## Этап 7 готов, если: - каталог `skills/` существует; - SkillRegistry грузит skills; - Runtime выбирает candidate skill; - Skills API работает; - Web UI показывает skills. ## Этап 8 готов, если: - Reflection работает через critic role; - ExperienceRecord создаётся после задачи; - Experience API работает; - Web UI показывает experience records; - skill update proposals создаются; - proposals не применяются автоматически. ## Этап 9 готов, если: - Qdrant поднимается через docker-compose; - VectorMemory adapter существует; - add_memory работает или явно xfail, если embeddings недоступны; - search_memory работает или явно xfail; - MemoryPolicy существует; - Memory API работает; - Web UI имеет memory page. ## Этап 10 готов, если: - MTP experimental script есть; - MTP не включён по умолчанию для action JSON endpoint; - benchmark script есть; - benchmark показывает role → base_url/model; - benchmark считает LLM calls, latency, retries, tool calls. --- # 22. Что запрещено Запрещено: 1. превращать DuckLM в обычный workflow-runner; 2. заменять когнитивный цикл набором if/else эвристик; 3. писать самописный inference server; 4. писать самописный model scheduler; 5. писать самописную vector database; 6. делать бесконечный JSON repair loop; 7. давать модели прямой shell без ToolGateway; 8. включать MTP/speculative для action JSON endpoint по умолчанию; 9. делать self-modifying code без approval; 10. смешивать cognition_response и action_directive; 11. считать, что thinker/critic/coder/action — обязательно разные модели; 12. считать, что каждая роль требует отдельный llama-server; 13. хардкодить пути к моделям в коде; 14. делать CLI обязательной частью системы; 15. делать сложный frontend раньше рабочего Web/API loop. --- # 23. Финальный отчёт исполнителя В конце работы по каждому этапу исполнитель должен предоставить: 1. что реализовано; 2. что не реализовано и почему; 3. список изменённых файлов; 4. как запустить `llama-server`; 5. как запустить DuckLM API; 6. как открыть WebChat; 7. как отправить первую задачу через WebChat; 8. как отправить задачу через curl; 9. как посмотреть task events; 10. как проверить одну модель на все роли; 11. как проверить разные модели на разные роли; 12. как проверить file_write/file_read; 13. как проверить блокировку опасной команды; 14. как проверить approvals; 15. как запустить smoke tests; 16. как запустить verification scripts; 17. какие ограничения остались; 18. что делать следующим этапом. Финальные команды запуска должны быть примерно такими: ```bash cp .env.example .env # прописать DUCK_MAIN_MODEL_PATH bash scripts/llama/start_main.sh ``` Во втором терминале: ```bash python -m duck_core.api ``` Проверка: ```bash curl http://127.0.0.1:8000/health curl http://127.0.0.1:8000/v1/models/roles curl http://127.0.0.1:8000/v1/models/ping ``` Запуск задачи: ```bash curl -X POST http://127.0.0.1:8000/v1/chat \ -H "Content-Type: application/json" \ -d '{ "message": "Скажи коротко, что ты DuckLM", "workspace": "./workspace", "debug": true }' ``` --- # 24. Главная мысль проекта DuckLM должна быть не набором скриптов и не inference-сервером. DuckLM должна быть локальным когнитивным runtime: ```text состояние контекст модельное мышление намерение действие наблюдение рефлексия память опыт навыки ``` Первый результат должен быть маленьким, но живым: ```text WebChat ↓ FastAPI ↓ Duck Core ↓ llama-server ↓ SQLite event timeline ↓ WebChat показывает ответ и ход выполнения ``` После этого постепенно добавляются: ```text tools approvals skills experience semantic memory MTP benchmark hardening ```