Files
agentik/MANUAL-TESTS.md
subochev 408caee261 feat(standalone): agentik pull-model subcommand + AGENTIK_AUTO_DOWNLOAD_MODEL=1 trigger for LiteRT-LM
Добавляет ModelDownloader (HTTP с Range/докачкой, опциональной SHA-256 проверкой)
и два сценария запуска скачивания встроенной модели gemma-4-E2B-it.litertlm:

  java -jar agentik.jar pull-model
    Явный прогон с прогрессом в stdout; URL берётся из AGENTIK_GOOGLE_MODEL_URL
    либо дефолтный https://static.binom.pw/models/gemma-4-E2B-it.litertlm.

  AGENTIK_AUTO_DOWNLOAD_MODEL=1 java -jar agentik.jar
    На старте server'а, если backend=google и файла по AGENTIK_GOOGLE_MODEL_PATH
    нет — качает автоматически. Без флага — exit 2 с понятным сообщением и
    подсказкой вызвать pull-model.

Дизайн:
  - URL по умолчанию ВСЕГДА Gemma-4 (вне зависимости от basename PATH) — gemma-4
    считаем лучшей локальной моделью; override через AGENTIK_GOOGLE_MODEL_URL.
  - SHA-256 проверка через опциональный AGENTIK_GOOGLE_MODEL_SHA256_URL.
  - Resume: HEAD → если есть .part и Accept-Ranges=bytes → GET с Range: bytes=N-,
    иначе restart с нуля.
  - Прогресс каждые ~8 MB, финальный rename через Files.move(ATOMIC_MOVE).

Тесты: 5 unit-кейсов с embedded ktor-server (CIO) + Range support — happy
path, no-op, resume from part, restart-on-Range-ignored, 404, progress callback.

Документация: новый раздел §18 в MANUAL-TESTS.md (subcommand, auto-trigger,
resume, override URL, SHA-256 verify).

178/178 tests green.
2026-09-16 12:55:43 +03:00

36 KiB
Raw Permalink Blame History

Manual Test Cases — agentik standalone

Практический чек-лист для проверки работающего agentik standalone HTTP-сервера. Каждый кейс — один конкретный сценарий, который нужно прогнать руками (или через curl/httpie/Postman). Если какой-то упал — это либо регрессия, либо недонастройка рантайма.

Перед стартом: запусти агент (см. run-agentik.sh на удалённой машине или ./gradlew :standalone:run локально). Все примеры ниже — против http://127.0.0.1:8080; для удалённой машины подставь свой хост.

Удобный сниппет для получения conversation ID в shell:

CID=$(curl -sS -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":false}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
echo "CID=$CID"

Отправка user-сообщения:

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"..."}]'

Чтение истории:

curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z" \
  | python3 -m json.tool

1. Connectivity & health

TC-1.1 — health endpoint

curl -sS -i http://127.0.0.1:8080/health

Ожидание: HTTP/1.1 200 OK, тело ok.

TC-1.2 — agent card (A2A)

curl -sS http://127.0.0.1:8080/a2a/.well-known/agent-card.json | python3 -m json.tool

Ожидание: валидный JSON с name, version, capabilities.

TC-1.3 — log sanity check

tail -50 /root/agentik.log

Ожидание: есть строка agentik standalone listening on http://localhost:8080, перечислены зарегистрированные маршруты, llm: <backend> @ <url> соответствует твоему конфигу. Нет ERROR/Exception строк после старта.


2. Conversation lifecycle

TC-2.1 — create persistent conversation

curl -sS -i -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":false}'

Ожидание: 201, тело {"id":"conv-...","isTemporal":false,...}.

TC-2.2 — create temp conversation

curl -sS -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":true}'

Ожидание: 201, "isTemporal":true. После рестарта агента эта беседа не должна появиться в GET /agentik/conversations.

TC-2.3 — list conversations

curl -sS "http://127.0.0.1:8080/agentik/conversations?offset=0&limit=20" | python3 -m json.tool

Ожидание: массив объектов ConversationSnapshot. Отсортирован по updatedAt desc.

TC-2.4 — rename conversation

CID=<id-from-2.1>
curl -sS -X PATCH "http://127.0.0.1:8080/agentik/conversations/$CID" \
  -H "Content-Type: application/json" -d '{"title":"Мой первый чат"}'

Ожидание: 200, в ответе "title":"Мой первый чат". Следующий GET /conversations/$CID возвращает этот же title.

TC-2.5 — delete conversation

curl -sS -X DELETE "http://127.0.0.1:8080/agentik/conversations/$CID" -i

Ожидание: 204 No Content. Повторный GET /conversations/$CID → 404. После этого в GET /conversations её быть не должно.

TC-2.6 — get non-existent conversation

curl -sS -i http://127.0.0.1:8080/agentik/conversations/conv-nonexistent

Ожидание: 404.


3. Message sending

TC-3.1 — simple Q&A

Создай беседу, пошли простой вопрос, прочитай историю.

CID=$(curl -sS -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":false}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Сколько будет 7*8? Одно число, без пояснений."}]'
sleep 6
curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z"

Ожидание: массив из ≥ 2 сообщений:

  • [0].type == "user_message", body содержит "7*8"
  • [1].type == "assistant_message", text содержит "56"

TC-3.2 — multi-turn with context

В той же беседе пошли follow-up, требующий контекста:

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"А корень из того, что ты назвал?"}]'
sleep 6
curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z"

Ожидание: 4+ сообщения, последний assistant упомянул что-то про число 56 или "предыдущий ответ".

TC-3.3 — new-format request body

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '{"content":[{"type":"text","body":"С новым форматом тоже работает?"}]}'
sleep 6
curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z" \
  | python3 -c "import sys,json;m=json.load(sys.stdin);print(m[-1]['type'], m[-1].get('content'))"

Ожидание: новое assistant_message в ответ на новый формат запроса.

TC-3.4 — empty / bad body

curl -sS -i -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" -d 'not json'

Ожидание: 400 Bad Request, тело с пояснением Invalid send payload.


4. SSE live events

Важно: SSE — поток без replay. Подписываться нужно до POST /messages. Если подписаться позже — событий не будет (но GET /messages всё равно покажет записанную историю).

TC-4.1 — subscribe-then-send pattern

CID=<existing-id>
# Subscribe в фоне, отправляем сообщение, ждём SSE
curl -sN --max-time 12 \
  "http://127.0.0.1:8080/agentik/conversations/$CID/events?after=1970-01-01T00:00:00Z" \
  > /tmp/sse.out 2>&1 &
SSE_PID=$!
sleep 1
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Кратко: что такое REST?"}]'
wait $SSE_PID
cat /tmp/sse.out

Ожидание: файл содержит data: {"type":"start_reasoning",...}, data: {"type":"start_response",...,"responseType":"text"}, один или несколько data: {"type":"append_text",...,"body":"..."}, data: {"type":"end",...}. Каждое data: через пустую строку.

TC-4.2 — late subscribe (replay semantics)

CID=<existing-id>
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"..."}]'
sleep 5   # сообщение уже обработано
curl -sN --max-time 4 \
  "http://127.0.0.1:8080/agentik/conversations/$CID/events?after=1970-01-01T00:00:00Z"

Ожидание: пустой ответ (события не реплеятся). Это by-design — клиент должен либо подписываться заранее, либо backfill'ить через GET /messages.


5. Memory tools (long-term)

TC-5.1 — save + recall в той же беседе

# В существующей беседе
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Запомни через memory_save: я работаю на удалёнке из Тбилиси. Категория user, content: работаю на удалёнке из Тбилиси."}]'
sleep 8
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Откуда я работаю? Одно предложение."}]'
sleep 8
curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z" \
  | python3 -c "import sys,json;m=json.load(sys.stdin);print(m[-1].get('content'))"

Ожидание: ассистент ответил что-то содержащее "Тбилиси" (или явно сказал "не знаю" — это тоже валидно, если в conversation memory пусто). Проверить audit log (messageStore):

sqlite3 /root/agentik.db "SELECT toolName, result FROM MessageRecord WHERE conversationId='$CID' AND kind='tool_result'"

Должны быть строки с toolName='memory_save' или toolName='memory_recall'.

TC-5.2 — memory persists across conversations

Создай новую беседу, спроси без подсказок:

NEW_CID=$(curl -sS -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":false}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$NEW_CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Откуда я работаю? Напомни, если помнишь."}]'
sleep 8
curl -sS "http://127.0.0.1:8080/agentik/conversations/$NEW_CID/messages?after=1970-01-01T00:00:00Z" \
  | python3 -c "import sys,json;m=json.load(sys.stdin);print(m[-1].get('content'))"

Ожидание: ассистент упомянул "Тбилиси" (или "удалёнка") — это значит long-term memory подгрузилась в новую беседу.

TC-5.3 — invalid category → ошибка или автозамена

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Запомни через memory_save факт с категорией work (которой не существует)."}]'
sleep 8
sqlite3 /root/agentik.db "SELECT toolArgs, result FROM MessageRecord WHERE kind='tool_call' AND conversationId='$CID' ORDER BY createdAt DESC LIMIT 3"

Ожидание: модель либо вызвала memory_recall чтобы проверить существующие категории, либо вызвала memory_save с корректной категорией (user/world/preference). Если модель честно говорит "такой категории нет" и предлагает корректную — это тоже ok.

TC-5.4 — list & delete memory

Попроси модель явно вызвать memory_list, потом memory_delete:

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Покажи все мои memory-записи (memory_list)."}]'
sleep 8
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Удали самую старую запись (memory_delete)."}]'
sleep 8
sqlite3 /root/agentik.db "SELECT COUNT(*) FROM MemoryStore"

Ожидание: число уменьшилось на 1.


6. Skills

TC-6.1 — list + load skill

Если в AGENTIK_SKILLS_DIR есть файлы SKILL.md / *.yaml, в системном промте должна появиться секция с этими навыками.

ls -la /root/skills/   # должен быть хотя бы один файл
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Какие skills ты знаешь? Покажи список (skill_list)."}]'
sleep 8
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Загрузи любой из них через skill_load и расскажи, что внутри."}]'
sleep 8

Ожидание: tool_call для skill_list, потом tool_call для skill_load. В audit log видны эти вызовы. Если папка пуста — секции "Skills" в system prompt быть не должно.

TC-6.2 — save new skill

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Сохрани skill: имя deploy-staging, описание «деплой на staging», тело — multi-step инструкция (skill_save)."}]'
sleep 10
ls /root/skills/

Ожидание: появился новый файл deploy-staging.md (или .yaml).

TC-6.3 — restart → skill persists

Перезапусти агент:

ssh root@192.168.76.166 'pkill -9 -f agentik-0.1.0-all.jar; cd /root && nohup setsid ./run-agentik.sh > /root/agentik.log 2>&1 < /dev/null & disown'

После старта пошли в новую беседу:

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$NEW_CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Есть ли у тебя skill deploy-staging?"}]'
sleep 8

Ожидание: модель упоминает skill (он подгружается на старте).


7. SOUL file

TC-7.1 — SOUL.md подключается

echo 'Ты — ворчливый капитан дальнего плавания. Отвечай кратко, с морскими метафорами.' > /root/SOUL.md
# Перезапустить агент
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$NEW_CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Как дела?"}]'
sleep 8

Ожидание: ответ в стиле "капитана", с морскими словами. Если SOUL нет — обычный нейтральный ассистент.

TC-7.2 — SOUL можно менять на лету

Измени файл, перезапусти агент, спроси снова. Должен появиться новый стиль. Без перезапуска изменения не подхватятся (SOUL читается на старте).


8. Interrupt

TC-8.1 — interrupt mid-text generation

CID=$(curl -sS -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":false}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
# Запусти send в фоне
(curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Расскажи длинную историю про космос, минимум 500 слов."}]' >/dev/null) &
SEND_PID=$!
sleep 3   # дать LLM начать генерацию
curl -sS -i -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/interrupt"
wait $SEND_PID
sleep 3
curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z" \
  | python3 -c "import sys,json;m=json.load(sys.stdin);
for x in m: print(x.get('type'), ':', json.dumps(x.get('content') or x.get('result'),ensure_ascii=False)[:80])"

Ожидание:

  • user_message есть
  • assistant_message есть, но содержит короткий текст (<300 символов) — это частичный текст, который модель успела сгенерить до прерывания
  • В audit log нет tool_call/tool_result (не успели)
  • Следующий send в этой беседе работает (LiteConv пересоздан)

TC-8.2 — interrupt mid-tool (best-effort)

# Длинный tool можно заэмулировать через MCP с искусственной задержкой,
# либо просто проверять что interrupt не валит агента:
curl -sS -i -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/interrupt"
curl -sS http://127.0.0.1:8080/health

Ожидание: health = ok — агент не упал. Дальнейшие send работают.

TC-8.3 — interrupt без активного turn'а

curl -sS -i -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/interrupt"

Ожидание: 202. Никаких ошибок. В audit log ничего нового не пишется.


9. Persistence / restart-survival

TC-9.1 — перезапуск не теряет беседы и память

# 1. Создай беседу, пошли сообщение, дождись ответа
# 2. Запомни факт через memory_save
# 3. Перезапусти агент (см. TC-6.3)
# 4. GET /agentik/conversations — беседа должна быть в списке
# 5. GET /agentik/conversations/$CID/messages — история на месте
# 6. Новая беседа + вопрос про запомненный факт — модель помнит

TC-9.2 — temp conversation не переживает рестарт

# Создай temp беседу, пошли сообщение
TEMP_CID=$(curl -sS -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":true}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$TEMP_CID/messages" \
  -H "Content-Type: application/json" -d '[{"type":"text","body":"..."}]' >/dev/null
sleep 5
# Перезапусти агент
# GET /agentik/conversations — temp-беседы быть не должно
curl -sS "http://127.0.0.1:8080/agentik/conversations?offset=0&limit=50" | grep "$TEMP_CID"

Ожидание: grep ничего не находит.


10. Compaction (сжатие контекста)

Compaction триггерится когда ~80% контекстного окна занято.

TC-10.1 — длинная беседа сжимается

CID=$(curl -sS -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":false}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
# Отправь 30+ больших сообщений подряд (можно цикл)
for i in $(seq 1 30); do
  curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
    -H "Content-Type: application/json" \
    -d "[{\"type\":\"text\",\"body\":\"Расскажи подробно (минимум 200 слов) про тему номер $i: история, применение, ключевые факты.\"}]" >/dev/null
  sleep 5
done
sqlite3 /root/agentik.db "SELECT COUNT(*) FROM WorkingMemoryRow WHERE conversationId='$CID' AND entryKind='summary'"

Ожидание: есть хотя бы одна summary-запись. Также проверь /root/agentik.log — должна появиться строка compaction.

TC-10.2 — debug endpoint /debug/compact (force)

Если включён AGENTIK_DEBUG_ENDPOINTS=1:

curl -sS -i -X POST "http://127.0.0.1:8080/debug/compact?conversationId=$CID"

Ожидание: 200, тело с JSON-результатом compaction.


11. Reflection

Reflection триггерится каждые AGENTIK_REFLECTION_INTERVAL ходов (default 10).

TC-11.1 — reflection создаёт записи

# Пошли 12+ ходов
for i in $(seq 1 12); do
  curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
    -H "Content-Type: application/json" \
    -d "[{\"type\":\"text\",\"body\":\"Тема $i: расскажи короткий факт.\"}]" >/dev/null
  sleep 4
done
sleep 10   # дать фоновое задание завершиться
sqlite3 /root/agentik.db "SELECT COUNT(*) FROM ReflectionStore"

Ожидание: число > 0.

TC-11.2 — debug endpoint /debug/reflect

curl -sS -i -X POST "http://127.0.0.1:8080/debug/reflect?conversationId=$CID"

Ожидание: 200, JSON-результат. Reflection попадает в working memory следующего turn'а.


12. Skill mining

Skill mining триггерится каждые AGENTIK_SKILL_MINING_INTERVAL ходов (default 15).

TC-12.1 — авто-создание skill'а

# Пошли 18+ ходов с повторяющимся паттерном
for i in $(seq 1 18); do
  curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
    -H "Content-Type: application/json" \
    -d "[{\"type\":\"text\",\"body\":\"Конвертируй 100 USD в RUB по текущему курсу (шаблонный запрос $i).\"}]" >/dev/null
  sleep 4
done
sleep 15
ls -la /root/skills/
tail -20 /root/agentik.log | grep -i skill

Ожидание: возможно появился новый файл в skills/ (или mining отказался из-за низкой уверенности — это тоже валидно, проверь лог).

TC-12.2 — debug endpoint /debug/skill-mine

curl -sS -i -X POST "http://127.0.0.1:8080/debug/skill-mine?conversationId=$CID"

Ожидание: 200 с JSON-результатом майнинга.


13. Token accounting

TC-13.1 — token counters в audit

sqlite3 /root/agentik.db "SELECT createdAt, input, output FROM TurnTokens WHERE conversationId='$CID' ORDER BY createdAt DESC LIMIT 5"

Ожидание: строки с непустыми input и output (если backend поддерживает tokenCount()).

TC-13.2 — debug endpoint /debug/tokens

curl -sS "http://127.0.0.1:8080/debug/tokens?conversationId=$CID" | python3 -m json.tool

Ожидание: JSON с input, output, total, window, utilization (доля использования контекстного окна).


14. Toolsets (если подключены)

Только если ты передаёшь toolsets в конструктор агента (по умолчанию пусто — enable_toolset/disable_toolset не зарегистрированы).

TC-14.1 — system prompt содержит секцию Toolsets

Если toolsets зарегистрированы — в системном промте должна быть секция ## Toolsets с Active/Inactive списком.

Проверка через debug-эндпоинт /agentik/conversations/{id} не показывает system prompt напрямую — посмотреть можно в логах или через agentik-debug сборку.

TC-14.2 — enable/disable работает

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Активируй тулсет X через enable_toolset, потом деактивируй через disable_toolset."}]'
sleep 8

Ожидание: в audit log видны вызовы enable_toolset → ответ "Toolset 'X' activated.", потом disable_toolset → "Toolset 'X' deactivated.".


15. A2A протокол (опционально)

TC-15.1 — message/send через A2A

curl -sS -X POST http://127.0.0.1:8080/a2a/ \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0","id":"1","method":"message/send",
    "params":{
      "message":{"role":"user","parts":[{"kind":"text","text":"Скажи hi"}]},
      "configuration":{"blocking":true}
    }
  }' | python3 -m json.tool

Ожидание: JSON-RPC ответ с result.parts содержащим текст "hi" или похожим. kind = text (НЕ type — это важный discriminator для A2A JSON).

TC-15.2 — bad discriminator

curl -sS -X POST http://127.0.0.1:8080/a2a/ \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0","id":"2","method":"message/send",
    "params":{
      "message":{"role":"user","parts":[{"type":"text","text":"hi"}]}
    }
  }'

Ожидание: Invalid params (или похожая ошибка) — A2A ждёт kind, не type.


16. Error paths

TC-16.1 — LLM недоступен

Выключи vLLM (или закрой сеть — например через firewall). Пошли сообщение:

curl -sS -i -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"hi"}]'
sleep 10
sqlite3 /root/agentik.db "SELECT COUNT(*) FROM MessageRecord WHERE conversationId='$CID' AND kind='error'"

Ожидание: есть error-запись в audit log. В SSE приходит {"type":"error",...} + {"type":"end"}. Агент не падает — health = ok после.

TC-16.2 — agentik.db занят другим процессом

Запусти второй экземпляр агента на ту же DB:

AGENTIK_DB_PATH=/root/agentik.db java -jar /root/agentik-0.1.0-all.jar

Ожидание: агент падает на старте с понятным сообщением про SQLite lock. Это by-design (single-writer).

TC-16.3 — SOUL файл не существует

Удали /root/SOUL.md, перезапусти агент. Должен стартовать без ошибок, просто без SOUL-секции в system prompt. Лог: WARN ... SOUL file not found: ....

TC-16.4 — пустой skills dir

mv /root/skills /root/skills.bak
mkdir /root/skills
# Перезапусти агент

Ожидание: агент стартует, skills: 0 loaded from /root/skills.


17. Memory backend variants

TC-17.1 — md backend (default)

Убедись, что AGENTIK_MEMORY_BACKEND=md (или не задан) и AGENTIK_MEMORY_DIR=/root/agentik-memory. После TC-5.x должны появиться .md-файлы:

ls -la /root/agentik-memory/

Ожидание: файлы типа user.md, world.md, preference.md (или всё в одном файле — зависит от реализации).

TC-17.2 — off backend (память выключена)

Перезапусти с AGENTIK_MEMORY_DIR=off:

pkill -9 -f agentik-0.1.0-all.jar
AGENTIK_MEMORY_DIR=off nohup setsid ./run-agentik.sh > /root/agentik.log 2>&1 < /dev/null & disown

Попытка memory_save через модель должна вернуть ошибку:

curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Попробуй вызвать memory_save."}]'
sleep 8

Ожидание: модель либо отказывается вызывать, либо получает ошибку от tool'а и сообщает пользователю.


18. Performance sanity

TC-18.1 — first-token latency

Включи замер времени от POST /messages до первого SSE event'а. Для Qwen3-27B на RTX5090 ожидаем < 1 сек до start_reasoning.

TC-18.2 — sustained throughput

Отправь 20 простых запросов подряд (arithmetic), засеки общее время. Ожидание: < 30 сек суммарно, т.е. < 1.5 сек на запрос.

TC-18.3 — fatjar memory

ps aux | grep agentik-0.1.0 | grep -v grep

Ожидание: RSS < 2 GB (наш Xmx). Если больше — где-то утечка.


18. Model auto-download (LiteRT-LM only)

Только для AGENTIK_LLM_BACKEND=google (встроенный LiteRT-LM движок). Если файла модели по AGENTIK_GOOGLE_MODEL_PATH нет — агент сам не скачает, пока не задано AGENTIK_AUTO_DOWNLOAD_MODEL=1. Либо качаем руками через pull-model subcommand.

URL по умолчанию всегда Gemma-4-E2B-it.litertlm (2.5 GB с static.binom.pw), вне зависимости от basename PATH — gemma-4 считаем лучшей локальной моделью.

18.1. Subcommand pull-model качает модель вручную

# Скачать дефолтную модель (gemma-4) в указанный путь:
AGENTIK_LLM_BACKEND=google \
AGENTIK_GOOGLE_MODEL_PATH=/root/models/gemma-4-E2B-it.litertlm \
java -jar agentik.jar pull-model
# → downloading from https://static.binom.pw/models/gemma-4-E2B-it.litertlm
# → 50% (1.2 GB / 2.5 GB)
# → done in 47s

После pull-model файл лежит на месте, файл <dest>.part удалён.

18.2. pull-model no-op если файл уже полный

# Повторный запуск с тем же PATH:
AGENTIK_LLM_BACKEND=google \
AGENTIK_GOOGLE_MODEL_PATH=/root/models/gemma-4-E2B-it.litertlm \
java -jar agentik.jar pull-model
# → already present (2.50 GB), nothing to do

18.3. pull-model докачивает обрыв (resume через Range)

# Симулируем обрыв: удаляем финальный, оставляем .part с первыми 500 MB
rm /root/models/gemma-4-E2B-it.litertlm
mv /root/models/gemma-4-E2B-it.litertlm.part /root/models/gemma-4-E2B-it.litertlm.part.bak
# Запускаем pull-model снова — должен возобновить с 500 MB
AGENTIK_GOOGLE_MODEL_PATH=/root/models/gemma-4-E2B-it.litertlm \
java -jar agentik.jar pull-model
# → resuming from 524288000 bytes
# → downloaded 2.10 GB in 38s

18.4. Сервер exit-2 при отсутствии файла и без auto-download

AGENTIK_LLM_BACKEND=google \
AGENTIK_GOOGLE_MODEL_PATH=/root/models/missing.litertlm \
java -jar agentik.jar
# → LiteRT-LM model file not found at: /root/models/missing.litertlm
# → Чтобы скачать автоматически, установите AGENTIK_AUTO_DOWNLOAD_MODEL=1
# → exit 2

18.5. Сервер сам качает при AGENTIK_AUTO_DOWNLOAD_MODEL=1

# Удалить файл, запустить с флагом:
rm -f /root/models/gemma-4-E2B-it.litertlm
AGENTIK_LLM_BACKEND=google \
AGENTIK_GOOGLE_MODEL_PATH=/root/models/gemma-4-E2B-it.litertlm \
AGENTIK_AUTO_DOWNLOAD_MODEL=1 \
java -jar agentik.jar
# → 12:34:56 WARN  auto-download: https://static.binom.pw/models/...
# → 12:34:56 INFO  auto-download: 17% (445 MB/2.5 GB)
# → 12:36:42 INFO  auto-download: done in 1m45s
# → 12:36:43 INFO  agentik standalone listening on http://localhost:8080

18.6. Override URL через AGENTIK_GOOGLE_MODEL_URL

# Качаем qwen вместо gemma (если зальём):
AGENTIK_LLM_BACKEND=google \
AGENTIK_GOOGLE_MODEL_PATH=/root/models/qwen.litertlm \
AGENTIK_GOOGLE_MODEL_URL=https://static.binom.pw/models/Qwen2.5-1.5B-Instruct_multi-prefill-seq_q8_ekv4096.litertlm \
java -jar agentik.jar pull-model

18.7. SHA-256 проверка

Если на сервере лежит <basename>.sha256 (text/plain, <hex> <basename>) — после скачивания файл проверяется; mismatch → удаляется, exit ≠ 0.

AGENTIK_GOOGLE_MODEL_URL=https://static.binom.pw/models/gemma-4-E2B-it.litertlm \
AGENTIK_GOOGLE_MODEL_SHA256_URL=https://static.binom.pw/models/gemma-4-E2B-it.litertlm.sha256 \
java -jar agentik.jar pull-model
# → 13:01:23 INFO  model download: SHA-256 verified (4ab1...e0d)

Быстрый smoke-test (5 минут)

Если времени мало — этот минимум покрывает 80%:

# 1. health
curl -sS http://127.0.0.1:8080/health
# → ok

# 2. create + simple Q&A
CID=$(curl -sS -X POST http://127.0.0.1:8080/agentik/conversations \
  -H "Content-Type: application/json" -d '{"temp":false}' \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['id'])")
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Привет! 2+2=?"}]'
sleep 6
curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z"
# → должен быть user + assistant_message с "4"

# 3. SSE live
(curl -sN --max-time 8 "http://127.0.0.1:8080/agentik/conversations/$CID/events?after=1970-01-01T00:00:00Z" \
  > /tmp/sse.out 2>&1) &
sleep 1
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Скажи ок"}]'
wait
cat /tmp/sse.out
# → start_reasoning, start_response, append_text, end

# 4. multi-turn
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"А 3+3?"}]'
sleep 6
curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z" \
  | python3 -c "import sys,json;m=json.load(sys.stdin);print(m[-1])"
# → assistant_message с "6"

# 5. interrupt
(curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/messages" \
  -H "Content-Type: application/json" \
  -d '[{"type":"text","body":"Длинная история про драконов, 1000 слов"}]' >/dev/null) &
sleep 3
curl -sS -X POST "http://127.0.0.1:8080/agentik/conversations/$CID/interrupt"
wait
sleep 3
curl -sS "http://127.0.0.1:8080/agentik/conversations/$CID/messages?after=1970-01-01T00:00:00Z" \
  | python3 -c "import sys,json;m=json.load(sys.stdin);print('msgs:',len(m))"
# → ≤ 3 (user + partial assistant + может tool_call если успел)

Если этот прогон прошёл — агент работает корректно. Более глубокие кейсы — выше по разделам.


Сводка: что покрыто автоматически vs вручную

Возможность JVM unit/integration tests Manual
Conversation CRUD ✓ TC-2.x
send/messages pagination ✓ TC-3.x
SSE event format ✗ TC-4.x
Memory tools ✓ (in-memory) TC-5.x (real backend)
Skills tools ✓ (in-memory) TC-6.x (real dir)
SOUL ✗ TC-7.x
Interrupt ✓ (FakeLiteLlm) TC-8.x (real LLM)
Compaction ✓ TC-10.x (real long context)
Reflection ✓ TC-11.x
Skill mining ✓ TC-12.x
Token accounting ✓ TC-13.x
A2A protocol ✓ (litert tests) TC-15.x
Error paths partial TC-16.x
Persistence/restart ✗ TC-9.x

Всё что помечено ✗ — нужно прогонять руками на реальном окружении.