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

298 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# Стек 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` если задан).
### Пример ответа (объект)
```json
{
"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-режим)
```json
{
"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-сообщения:
```json
{
"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`):
```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-контракт)
Добавить в промпт явное напоминание формата ответа:
```text
Верни ТОЛЬКО валидный 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-сообщения можно добавить блок:
```text
Память (предпочтения оператора): 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:
```text
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 может вернуть объект:
```json
{
"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`.