papayu/docs/LLM_PLAN_FORMAT.md
Yuriy f2f33e24d6 Protocol v1 release: golden traces, CI, make/npm, policy
- Golden traces: docs/golden_traces/v1/, format protocol/request/context/result
- trace_to_golden bin, golden_traces_v1_validate test
- Makefile: make golden, make test-protocol
- npm scripts: golden, test-protocol
- CI: .github/workflows/protocol-check.yml
- PROTOCOL_V1.md, PROTOCOL_V2_PLAN.md
- Policy for updating golden traces in README

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-01-31 11:54:33 +03:00

18 KiB
Raw Blame History

Стек papa-yu и JSON-контракт ответа (план)

На чём написан papa-yu

Слой Стек Примечание
Backend Rust (Tauri) Команды, LLM, FS, apply, undo, tx
Frontend TypeScript + React (Vite) UI, запросы к Tauri (invoke)

Не Python/Node/Go — бэкенд полностью на Rust; фронт — React/Vite.


JSON Schema для response_format

Полная схема для response_format (OpenAI Responses API и др.) — см. docs/papa_yu_response_schema.json.

Схема для Chat Completions (response_format: { type: "json_schema", ... }) — src-tauri/config/llm_response_schema.json. Включается через PAPAYU_LLM_STRICT_JSON=1.

Поведение strict / best-effort:

  • strict включён — приложение отправляет response_format в API; при ответе, не проходящем JSON schema, локально отклоняет и выполняет 1 авто-ретрай с repair-подсказкой.
  • strict выключен или провайдер не поддерживает — best-effort парсинг (извлечение из json ... ), затем локальная валидация; при неудаче — тот же repair-ретрай.

Текущий JSON-контракт ответа (план от LLM)

LLM должен вернуть только валидный JSON — либо массив действий, либо объект с полями.

Принимаемые форматы

  1. Массив действий[{ kind, path, content? }, ...]
  2. Объект{ actions?, proposed_changes.actions?, summary?, context_requests?, memory_patch? }

Формат Action (элемент массива)

Поле Тип Обязательность Описание
kind string да Один из: CREATE_FILE, CREATE_DIR, UPDATE_FILE, DELETE_FILE, DELETE_DIR
path string да Относительный путь от корня проекта (без ../, без абсолютных путей, без ~)
content string да для CREATE_FILE, UPDATE_FILE Содержимое файла; макс. ~1MB на файл; для CREATE_DIR/DELETE_* не используется

Ограничения на path: no ../, no абсолютные (/, C:\), no ~. Локальная валидация отклоняет.

DELETE_*: требует подтверждения пользователя в UI (кнопка «Применить»).

Plan→Apply без кнопок:

  • Префиксы: plan: <текст> → режим Plan; apply: <текст> → режим Apply.
  • Триггеры перехода: ok, ок, apply, применяй, да — при наличии сохранённого плана переключают на Apply.
  • По умолчанию: «исправь/почини» → Plan; «создай/сгенерируй» → Apply.

APPLY без изменений (каноничный маркер):

  • Если изменений не требуется — верни actions: [] и summary, начинающийся с NO_CHANGES: (строго).
  • Пример: "summary": "NO_CHANGES: Проверка завершена, правок не требуется."

Конфликты действий:

  • Один path не должен иметь несовместимых действий: CREATE_FILE + UPDATE_FILE, DELETE + CREATE/UPDATE.
  • Порядок применения: CREATE_DIR → CREATE_FILE/UPDATE_FILE → DELETE_FILE → DELETE_DIR.

Пути:

  • Запрещены: абсолютные (/, //), Windows drive (C:/), UNC (//server/share), ~, сегменты .. и ., пустой или только ..
  • Лимиты: max_path_len=240, max_actions=200, max_total_content_bytes=5MB.

ERR_UPDATE_WITHOUT_BASE:

  • В режиме APPLY каждый UPDATE_FILE должен ссылаться на файл, прочитанный в Plan (FILE[path]: или === path === в plan_context).

Protected paths (denylist):

  • .env, *.pem, *.key, *.p12, id_rsa*, **/secrets/** — запрещены для UPDATE/DELETE.

Content:

  • Запрещён NUL (\0), >10% non-printable = ERR_PSEUDO_BINARY.

EOL:

  • PAPAYU_NORMALIZE_EOL=lf — нормализовать \r\n→\n, trailing newline.

Наблюдаемость:

  • Каждый propose имеет trace_id (UUID). Лог-ивенты в stderr: LLM_REQUEST_SENT (token_budget, input_chars), LLM_RESPONSE_OK, VALIDATION_FAILED, LLM_REQUEST_TIMEOUT, LLM_RESPONSE_FORMAT_FALLBACK.
  • PAPAYU_TRACE=1 — трасса в .papa-yu/traces/<trace_id>.json. По умолчанию raw_content не сохраняется (риск секретов); PAPAYU_TRACE_RAW=1 — сохранять с маскировкой sk-/Bearer. В трассе — config_snapshot.

Параметры генерации: temperature=0, max_tokens=16384 (авто-кэп: при input>80k → 4096), top_p=1, presence_penalty=0, frequency_penalty=0. PAPAYU_LLM_TIMEOUT_SEC=90. Capability detection: при ошибке response_format — retry без него.

Версия схемы: LLM_PLAN_SCHEMA_VERSION=1 — в system prompt и trace; для будущей поддержки v1/v2 при расширении kinds/полей. x_schema_version в llm_response_schema.json. schema_hash (sha256) в config_snapshot.

Кеш контекста: read_file/search/logs/env кешируются в пределах plan-цикла. Логи: CONTEXT_CACHE_HIT, CONTEXT_CACHE_MISS.

Контекст-диета: см. раздел «Контекст-диета» ниже.

Trace: при PAPAYU_TRACE=1 в трассу добавляются context_stats (context_files_count, context_files_dropped_count, context_total_chars, context_logs_chars, context_truncated_files_count) и cache_stats (hits/misses по типам env/logs/read/search, hit_rate).

Fix-plan режим (user.output_format)

  • PLAN (plan): actions пустой массив [], summary обязателен (диагноз + шаги + команды проверки), при необходимости — context_requests.
  • APPLY (apply): actions непустой, если нужны изменения; иначе пустой + summary «изменений не требуется». summary — что сделано и как проверить (используй project.default_test_command если задан).

Пример ответа (объект)

{
  "actions": [
    { "kind": "CREATE_FILE", "path": "README.md", "content": "# Project\n\n## Run\n\n`make run`\n" },
    { "kind": "CREATE_DIR", "path": "src" }
  ],
  "summary": "Созданы README и папка src.",
  "context_requests": [],
  "memory_patch": {}
}

Пример Fix-plan (plan-режим)

{
  "actions": [],
  "summary": "Диагноз: ...\nПлан:\n1) ...\n2) ...\nПроверка: pytest -q",
  "context_requests": [
    { "type": "read_file", "path": "src/app.py", "start_line": 1, "end_line": 240 }
  ],
  "memory_patch": { "user.output_format": "plan" }
}

Как приложение обрабатывает ответ

  1. Парсит JSON из ответа (извлекает из json ... при наличии).
  2. Берёт actions из корня или proposed_changes.actions.
  3. Валидирует: path (no ../, no absolute), content обязателен для CREATE_FILE/UPDATE_FILE.
  4. summary используется если есть; иначе формируется в коде.
  5. context_requests — выполняется в следующем раунде (до MAX_CONTEXT_ROUNDS).
  6. memory_patch — применяется только ключи из whitelist.


Контекст-диета (поведение рантайма)

Контекст может быть урезан для контроля стоимости токенов и стабильности ответов.

Env-переменные лимитов:

Переменная По умолчанию Описание
PAPAYU_CONTEXT_MAX_FILES 8 Макс. число FILE/SEARCH/LOGS/ENV блоков в FULFILLED_CONTEXT
PAPAYU_CONTEXT_MAX_FILE_CHARS 20000 Макс. символов на один файл (read_file)
PAPAYU_CONTEXT_MAX_TOTAL_CHARS 120000 Макс. символов всего блока FULFILLED_CONTEXT
PAPAYU_CONTEXT_MAX_LOG_CHARS 12000 Резерв для логов (в текущей реализации не используется)

Порядок урезания: при нехватке budget — search hits, logs; FILE-блоки (запрошенные read_file) — последними; для priority=0 файлов гарантируется минимум 4k chars даже при нехватке total budget.

Truncation: при превышении MAX_FILE_CHARS — head+tail (60/40) с маркером ...[TRUNCATED N chars]....

Лог: CONTEXT_DIET_APPLIED files=N dropped=M truncated=T total_chars=C при dropped>0 или truncated>0.

Trace: в context_stats — context_files_count, context_files_dropped_count, context_total_chars, context_logs_chars, context_truncated_files_count.


context_requests (типы запросов)

type Обязательные поля Описание
read_file path Прочитать файл (опционально start_line, end_line)
search query Поиск по проекту (опционально glob)
logs source Логи (приложение ограничено)
env Информация об окружении

Автосбор контекста (до первого вызова LLM)

Эвристики по содержимому user_goal и отчёта:

  • Traceback / Exception → извлекаются пути и номера строк, читаются файлы ±80 строк вокруг
  • ImportError / ModuleNotFoundError / cannot find module → добавляются ENV + содержимое pyproject.toml, requirements.txt, package.json, poetry.lock

Типы в Rust (справочно)

  • Action: { kind: ActionKind, path: String, content: Option<String> }
  • ActionKind: enum CreateFile | CreateDir | UpdateFile | DeleteFile | DeleteDir (сериализуется в SCREAMING_SNAKE_CASE)
  • AgentPlan: { ok: bool, summary: String, actions: Vec<Action>, error?: String, error_code?: String }

memory_patch + whitelist + пример промпта (под этот контракт)

1) memory_patch (что подставлять в промпт как контекст «памяти»)

Хранить в приложении (файл/БД/локальное хранилище) и подставлять в system или в начало user-сообщения:

{
  "preferred_style": "коротко, по делу",
  "default_language": "python",
  "test_command": "pytest -q",
  "lint_command": "ruff check .",
  "format_command": "ruff format .",
  "project_root_hint": "src/ — код, tests/ — тесты"
}

В промпте: один абзац, например:
«Память: стиль — коротко по делу; язык по умолчанию — python; тесты — pytest -q; линт — ruff check .; структура — src/, tests/.»

2) whitelist (разрешённые пути для действий)

При парсинге плана и перед apply проверять: все path должны быть относительно корня проекта и не выходить за его пределы. Дополнительно можно ограничить типы файлов/папок.

Пример whitelist (Rust/конфиг):

  • Разрешены расширения для CREATE_FILE/UPDATE_FILE: .py, .ts, .tsx, .js, .jsx, .json, .md, .yaml, .yml, .toml, .css, .html, .sql, .sh, .env.example, без расширения — только известные имена: README, Makefile, .gitignore, .editorconfig, Dockerfile.
  • Запрещены пути: содержащие .., абсолютные пути, .env (без .example), *.key, *.pem, каталоги node_modules/, .git/, __pycache__/.

Файл конфига (например config/llm_whitelist.json):

{
  "allowed_extensions": [".py", ".ts", ".tsx", ".js", ".jsx", ".json", ".md", ".yaml", ".yml", ".toml", ".css", ".html", ".sql", ".sh"],
  "allowed_no_extension": ["README", "Makefile", ".gitignore", ".editorconfig", "Dockerfile", "LICENSE"],
  "forbidden_paths": [".env", "*.key", "*.pem", "node_modules", ".git", "__pycache__"],
  "forbidden_prefixes": ["..", "/"]
}

3) Пример промпта (фрагмент под твой JSON-контракт)

Добавить в промпт явное напоминание формата ответа:

Верни ТОЛЬКО валидный JSON — массив действий, без markdown и пояснений.
Формат каждого элемента: { "kind": "CREATE_FILE" | "CREATE_DIR" | "UPDATE_FILE" | "DELETE_FILE" | "DELETE_DIR", "path": "относительный/путь", "content": "опционально для CREATE_FILE/UPDATE_FILE" }.
Пример: [{"kind":"CREATE_FILE","path":"README.md","content":"# Project\n"},{"kind":"CREATE_DIR","path":"src"}]

Это уже есть в build_prompt; при добавлении memory_patch в начало user-сообщения можно добавить блок:

Память (предпочтения оператора): preferred_style=коротко по делу; default_language=python; test_command=pytest -q; lint_command=ruff check .; format_command=ruff format .; структура проекта: src/, tests/.

После применения whitelist при apply отклонять действия с path вне whitelist и возвращать в AgentPlan error с кодом FORBIDDEN_PATH.


Инженерная память (Engineering Memory)

Память разделена на три слоя; в промпт подставляется только устойчивый минимум (~12 KB).

A) User prefs (оператор)

  • Расположение: app_data_dir()/papa-yu/preferences.json (локально).
  • Поля: preferred_style (brief|normal|verbose), ask_budget (0..2), risk_tolerance (low|medium|high), default_language, output_format (patch_first|plan_first).

B) Project prefs (для репо)

  • Расположение: в репо .papa-yu/project.json (шарится между машинами при коммите).
  • Поля: default_test_command, default_lint_command, default_format_command, package_manager, build_command, src_roots, test_roots, ci_notes.

C) Session state

  • В памяти процесса (не в файлах): current_task_goal, current_branch, recent_files, recent_errors. В текущей реализации не подставляется в промпт.

MEMORY BLOCK в промпте

Добавляется в system message после основного system prompt:

ENGINEERING_MEMORY (trusted by user; update only when user requests):
{"user":{"preferred_style":"brief","ask_budget":1,"risk_tolerance":"medium","default_language":"python"},"project":{"default_test_command":"pytest -q","default_lint_command":"ruff check .","default_format_command":"ruff format .","src_roots":["src"],"test_roots":["tests"]}}

Use ENGINEERING_MEMORY as defaults. If user explicitly asks to change — suggest updating memory and show new JSON.

Ответ с memory_patch

Если пользователь просит «запомни, что тесты запускать так-то», LLM может вернуть объект:

{
  "actions": [],
  "memory_patch": {
    "project.default_test_command": "pytest -q",
    "user.preferred_style": "brief"
  }
}

Приложение применяет только ключи из whitelist и сохраняет в preferences.json / .papa-yu/project.json.

Безопасность memory_patch: при парсинге удаляются все ключи не из whitelist; валидируются типы (ask_budget int, src_roots массив строк и т.д.). Рекомендуется применять patch только при явной просьбе пользователя.

Whitelist memory_patch (ключи через точку)

  • user.preferred_style, user.ask_budget, user.risk_tolerance, user.default_language, user.output_format
  • project.default_test_command, project.default_lint_command, project.default_format_command, project.package_manager, project.build_command, project.src_roots, project.test_roots, project.ci_notes

Примеры файлов: см. docs/preferences.example.json и docs/project.example.json.