ducklm/Ducklm.md

2184 lines
47 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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/<task_id>/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/<timestamp>_<skill_id>.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
```