ducklm/Ducklm.md

47 KiB
Raw Blame History

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:

  1. сохранить текущий user message;
  2. сохранить active task state;
  3. сохранить selected skill summary;
  4. сохранить последние важные observations;
  5. суммаризировать старые events;
  6. обрезать низкорелевантную memory;
  7. не превышать 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

Требования:

  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. Не требовать уникальности моделей для ролей.

Интерфейс:

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

Правила:

  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 запрещён.

Запрещено:

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

Создать:

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:

  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

Добавить:

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 должна отвечать:

  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

Добавить таблицу:

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:

  1. Если llama-server /v1/embeddings доступен — использовать его.
  2. Если embeddings пока недоступны — сделать явный adapter stub и xfail-test.
  3. Не писать самодельный 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

Описать:

  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:

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. что делать следующим этапом.

Финальные команды запуска должны быть примерно такими:

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