47 KiB
DuckLM — техническое задание на разработку локальной агентной системы
0. Назначение проекта
DuckLM — локальная агентная система, которая работает как самостоятельный runtime поверх локальных языковых моделей.
Система должна уметь:
- принимать сообщения от человека через WebChat;
- принимать задачи от внешних агентов и тестов через HTTP API;
- использовать локальные LLM через
llama-server; - вести состояние задач;
- записывать события выполнения;
- безопасно запускать инструменты;
- работать с навыками;
- сохранять опыт;
- использовать память;
- анализировать собственные ошибки;
- постепенно улучшать поведение через опыт и предложения по обновлению навыков.
Главная идея:
DuckLM — это не inference server.
DuckLM — это когнитивный runtime:
состояние → контекст → мышление → намерение → действие → наблюдение → рефлексия → память → опыт.
1. Архитектурные принципы
1.1. Использовать готовые компоненты
DuckLM должна использовать готовые решения там, где это разумно.
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
Основные интерфейсы:
WebChat → для человека
HTTP API → для кодера, тестов и внешних агентов
CLI в обязательную часть не входит.
Если позже понадобится CLI, он должен быть тонким клиентом поверх HTTP API.
1.3. Роли моделей логические
Роли моделей:
thinker
critic
coder
action
recall
summary
sys_util
являются логическими ролями, а не обязательно разными физическими моделями.
Одна физическая модель может использоваться сразу для всех ролей:
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.
Пример:
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
Код не должен предполагать, что разные роли используют разные модели.
Правильно:
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 по конфигу решает:
какой 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.
Пример:
8081 local-main обычный
8085 local-main-mtp экспериментальный
MTP/speculative decoding не включать по умолчанию для action JSON endpoint.
3. Token budget и context budget
Нужно явно разделять:
ctx_size
общий размер контекстного окна модели
max_output_tokens
сколько модель может сгенерировать за один вызов
max_input_tokens
сколько токенов можно собрать во входной prompt
recent_events_tokens
сколько истории событий можно включить
memory_tokens
сколько памяти можно включить
skill_tokens
сколько текста skill/procedure/examples можно включить
Пример .env.example:
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:
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:
- сохранить текущий user message;
- сохранить active task state;
- сохранить selected skill summary;
- сохранить последние важные observations;
- суммаризировать старые events;
- обрезать низкорелевантную memory;
- не превышать context window молча.
5. Целевая архитектура
┌─────────────────────────────────────────────┐
│ 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. Структура проекта
Создать структуру:
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
Минимальные зависимости:
[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
Создать:
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, в логах должно быть предупреждение:
WARNING: DuckLM API is listening on 0.0.0.0. This may expose local tool execution endpoints.
7.4. config/models.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
Создать:
scripts/llama/start_main.sh
#!/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
Создать:
scripts/llama/healthcheck.sh
#!/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
Создать:
duck_core/model_client.py
Требования:
- Читать
config/models.yaml. - Вызывать модель по логической роли.
- Работать через OpenAI-compatible API.
- Поддерживать role-specific
system_prompt. - Поддерживать role-specific
temperature. - Поддерживать role-specific
max_output_tokens. - Поддерживать
response_format. - Логировать latency.
- Логировать usage tokens, если backend их возвращает.
- Корректно обрабатывать ошибки соединения.
- Не требовать уникальности моделей для ролей.
Интерфейс:
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. Цель
Сделать минимальный живой вертикальный срез:
человек пишет в 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.
Минимальные таблицы:
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);
Минимальные статусы задач:
running
completed
failed
cancelled
Минимальные события:
task_created
model_call_started
cognition_response
model_call_finished
task_completed
task_failed
9.3. RuntimeLoop
Создать:
duck_core/runtime_loop.py
Минимальный цикл:
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
Создать:
duck_core/api.py
Минимальные endpoints:
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 — основной человекоподобный вход.
Пример запроса:
{
"message": "Скажи коротко, что ты DuckLM",
"workspace": "./workspace",
"debug": true
}
Пример ответа:
{
"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. Проверка этапа
Запуск:
cp .env.example .env
# прописать DUCK_MAIN_MODEL_PATH
bash scripts/llama/start_main.sh
Во втором терминале:
python -m duck_core.api
Проверка:
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
Запуск задачи:
curl -X POST http://127.0.0.1:8000/v1/chat \
-H "Content-Type: application/json" \
-d '{
"message": "Скажи коротко, что ты DuckLM",
"workspace": "./workspace",
"debug": true
}'
Проверить events:
curl http://127.0.0.1:8000/v1/tasks/<task_id>/events
Ожидаемые события:
task_created
model_call_started
cognition_response
model_call_finished
task_completed
10. Этап 4 — cognition/action split
10.1. Цель
Разделить свободное мышление и машинное намерение.
cognition_response
свободный текст, понимание задачи, план, риски
action_directive
строгий JSON для ToolGateway
Модель не должна думать в JSON.
JSON используется только как форма внешнего действия.
10.2. Action directive schema
Создать:
duck_core/schemas/action_directive.schema.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
Правила:
action_directiveгенерируется через structured output, если backend это поддерживает.- Если backend не поддерживает JSON schema, явно записать это в event log.
- Fallback на plain JSON допускается только если включён в config.
- После генерации directive валидируется локально.
- Разрешён максимум один retry.
- Retry чинит только directive.
- Бесконечный JSON repair loop запрещён.
Запрещено:
while not valid_json:
call_model_to_fix_json()
11. Этап 5 — ToolGateway
11.1. Цель
Добавить безопасное выполнение действий через tools.
Модель не запускает инструменты напрямую.
Модель создаёт action_directive.
ToolGateway:
- принимает action directive;
- проверяет tool;
- проверяет risk level;
- нормализует действие;
- проверяет permissions;
- выполняет разрешённое действие;
- пишет observation в event log;
- возвращает результат в runtime loop.
11.2. Tool interface
Создать:
duck_core/tools/base.py
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
Создать:
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:
pwd
ls
cat
head
tail
grep
find
python -m pytest
pytest
git status
git diff
git log
Blocklist:
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
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
);
Статусы задачи:
running
waiting_for_approval
completed
failed
cancelled
Если действие требует approval:
- создать pending approval;
- перевести task в
waiting_for_approval; - показать approval в Web UI;
- позволить approve/deny через API;
- после allow_once/allow_forever продолжить задачу через
/continue.
12.3. Approval API
Добавить:
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
Инвариант:
Allow forever = только exact normalized action hash.
Это не широкое разрешение на похожие действия.
12.4. Approval UI
Web UI должен показывать pending approval:
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
Создать:
skills/analyze_project/
skill.yaml
procedure.md
examples.md
notes.md
Пример skill.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
Создать:
duck_core/skills/registry.py
Интерфейс:
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
Добавить:
GET /v1/skills
GET /v1/skills/{skill_id}
Web UI:
/skills
14. Этап 8 — Experience и Reflection
14.1. Цель
Добавить самоулучшение через опыт.
Не через автоматическое изменение кода.
А через:
task
↓
reflection
↓
experience record
↓
skill update proposal
↓
human approval later
14.2. Reflection
Создать:
duck_core/reflection.py
Reflection должна отвечать:
- Что пытались сделать?
- Получилось ли?
- Что сработало?
- Что не сработало?
- Были ли лишние model calls?
- Были ли лишние tool calls?
- Застревала ли система?
- Была ли проблема с JSON/action directive?
- Нужно ли что-то запомнить?
- Нужно ли предложить изменение skill?
Reflection использует роль critic.
critic может быть той же физической моделью, что и thinker.
14.3. ExperienceRecord
Добавить таблицу:
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
);
Формат:
{
"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 надо улучшить, создать файл:
skills/_proposals/<timestamp>_<skill_id>.patch.md
Формат:
# 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
Добавить:
GET /v1/experience
GET /v1/experience/{id}
Web UI:
/experience
15. Этап 9 — Semantic memory
15.1. Цель
Добавить semantic memory через готовый vector store.
15.2. Qdrant compose
Создать:
docker-compose.memory.yml
services:
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_storage:/qdrant/storage
volumes:
qdrant_storage:
15.3. VectorMemory adapter
Создать:
duck_core/memory/vector_memory.py
Интерфейс:
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:
- Если
llama-server /v1/embeddingsдоступен — использовать его. - Если embeddings пока недоступны — сделать явный adapter stub и xfail-test.
- Не писать самодельный embedding algorithm.
15.4. MemoryPolicy
Создать:
duck_core/memory/policy.py
Типы памяти:
event
semantic_fact
preference
procedure
experience
skill_update_candidate
Пример результата:
{
"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.
Не делать жёстких эвристик вида:
if "remember" in text:
...
15.5. Memory API
Добавить:
GET /v1/memory/search?q=...
Web UI:
/memory
16. Этап 10 — Performance и MTP experiments
16.1. Цель
Добавить экспериментальные режимы ускорения inference.
MTP/speculative decoding — уровень inference backend, а не Duck Core.
16.2. MTP script
Создать:
scripts/llama/start_thinker_mtp_experimental.sh
#!/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
Создать:
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.
Тестовые задачи:
1. "Скажи коротко, что ты DuckLM."
2. "Создай tmp/duck_test_note.md с текстом hello duck и прочитай его обратно."
3. "Посмотри структуру проекта и кратко опиши модули."
4. "Найди TODO/FIXME в проекте."
5. "Запусти тесты и кратко объясни результат."
Бенчмарк должен выводить:
role -> base_url/model
17. Verification scripts
Создать:
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:
#!/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:
#!/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
Создать:
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
Создать:
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 должны пропускаться, если:
DUCK_SKIP_LIVE_LLM_TESTS=1
20. Документация
Создать:
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
Описать:
- как установить зависимости;
- как указать путь к GGUF-модели;
- как запустить
llama-server; - как запустить DuckLM API;
- как открыть WebChat;
- как отправить первую задачу;
- как смотреть task events;
- как смотреть approvals;
- как остановить сервисы.
docs/model_roles.md
Описать:
- роль модели — логическая роль;
- thinker/critic/coder/action могут использовать одну модель;
- разные роли могут отличаться prompt/temperature/schema/context;
- как настроить одну модель на все роли;
- как настроить разные модели на разные роли;
- какие параметры request-level;
- какие параметры backend-level.
docs/web_api.md
Описать endpoints:
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. Что запрещено
Запрещено:
- превращать DuckLM в обычный workflow-runner;
- заменять когнитивный цикл набором if/else эвристик;
- писать самописный inference server;
- писать самописный model scheduler;
- писать самописную vector database;
- делать бесконечный JSON repair loop;
- давать модели прямой shell без ToolGateway;
- включать MTP/speculative для action JSON endpoint по умолчанию;
- делать self-modifying code без approval;
- смешивать cognition_response и action_directive;
- считать, что thinker/critic/coder/action — обязательно разные модели;
- считать, что каждая роль требует отдельный llama-server;
- хардкодить пути к моделям в коде;
- делать CLI обязательной частью системы;
- делать сложный frontend раньше рабочего Web/API loop.
23. Финальный отчёт исполнителя
В конце работы по каждому этапу исполнитель должен предоставить:
- что реализовано;
- что не реализовано и почему;
- список изменённых файлов;
- как запустить
llama-server; - как запустить DuckLM API;
- как открыть WebChat;
- как отправить первую задачу через WebChat;
- как отправить задачу через curl;
- как посмотреть task events;
- как проверить одну модель на все роли;
- как проверить разные модели на разные роли;
- как проверить file_write/file_read;
- как проверить блокировку опасной команды;
- как проверить approvals;
- как запустить smoke tests;
- как запустить verification scripts;
- какие ограничения остались;
- что делать следующим этапом.
Финальные команды запуска должны быть примерно такими:
cp .env.example .env
# прописать DUCK_MAIN_MODEL_PATH
bash scripts/llama/start_main.sh
Во втором терминале:
python -m duck_core.api
Проверка:
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
Запуск задачи:
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:
состояние
контекст
модельное мышление
намерение
действие
наблюдение
рефлексия
память
опыт
навыки
Первый результат должен быть маленьким, но живым:
WebChat
↓
FastAPI
↓
Duck Core
↓
llama-server
↓
SQLite event timeline
↓
WebChat показывает ответ и ход выполнения
После этого постепенно добавляются:
tools
approvals
skills
experience
semantic memory
MTP
benchmark
hardening