papayu/docs/PROTOCOL_V2_PLAN.md
Yuriy 764003fc09 Commit X4: Auto inject Online Research summary into plan context
This commit implements fully automatic injection of online research results into the LLM prompt without user clicks.

## Backend

### Environment Variables
- Added `PAPAYU_ONLINE_AUTO_USE_AS_CONTEXT=1` (default: 0) to enable automatic injection of online research results into subsequent `proposeActions` calls.
- Added `is_online_auto_use_as_context()` helper function in `online_research/mod.rs`.

### Command Changes
- **`propose_actions` command**: Added `online_fallback_reason: Option<String>` parameter to track the error code that triggered online fallback.
- **`llm_planner::plan` function**: Added `online_fallback_reason: Option<&str>` parameter for tracing.
- **Trace Enhancements**: Added `online_fallback_reason` field to trace when `online_fallback_executed` is true.

### Module Exports
- Made `extract_error_code_prefix` public in `online_research/fallback.rs` for frontend use.

## Frontend

### Project Settings
- Added `onlineAutoUseAsContext` state (persisted in `localStorage` as `papa_yu_online_auto_use_as_context`).
- Initialized from localStorage or defaults to `false`.
- Auto-saved to localStorage on change.

### Auto-Chain Flow
- When `plan.ok === false` and `plan.online_fallback_suggested` is present:
  - If `onlineAutoUseAsContext === true` and not already attempted for this goal (cycle protection via `lastGoalWithOnlineFallbackRef`):
    - Automatically calls `researchAnswer(query)`.
    - Truncates result to `8000` chars and `10` sources (frontend-side limits).
    - Immediately calls `proposeActions` again with:
      - `online_context_md`
      - `online_context_sources`
      - `online_fallback_executed: true`
      - `online_fallback_reason: error_code`
      - `online_fallback_attempted: true`
    - Displays the new plan/error without requiring "Use as context" button click.
  - If `onlineAutoUseAsContext === false` or already attempted:
    - Falls back to manual mode (shows online research block with "Use as context (once)" button).

### Cycle Protection
- `lastGoalWithOnlineFallbackRef` tracks the last goal that triggered online fallback.
- If the same goal triggers fallback again, auto-chain is skipped to prevent infinite loops.
- Maximum 1 auto-chain per user query.

### UI Enhancements
- **Online Research Block**:
  - When `onlineAutoUseAsContext === true`: displays "Auto-used ✓" badge.
  - Hides "Use as context (once)" button when auto-use is enabled.
  - Adds "Disable auto-use" button (red) to disable auto-use for the current project.
  - When disabled, shows system message: "Auto-use отключён для текущего проекта."

### API Updates
- **`proposeActions` in `tauri.ts`**: Added `onlineFallbackReason?: string | null` parameter.

## Tests

- **`online_context_auto_test.rs`**: Added unit tests for:
  - `test_is_online_auto_use_disabled_by_default`
  - `test_is_online_auto_use_enabled_when_set`
  - `test_extract_error_code_prefix_timeout`
  - `test_extract_error_code_prefix_schema`
  - `test_extract_error_code_prefix_empty_when_no_prefix`

All tests pass.

## Documentation

### README.md
- Added "Auto-use (X4)" subsection under "Online Research":
  - Describes `PAPAYU_ONLINE_AUTO_USE_AS_CONTEXT=1` env var (default: 0).
  - Explains cycle protection: maximum 1 auto-chain per goal.
  - Documents UI behavior: "Auto-used ✓" badge and "Disable auto-use" button.

## Behavior Summary

**Without auto-use (default):**
1. `proposeActions` → error + `online_fallback_suggested`
2. UI calls `researchAnswer`
3. UI displays online research block with "Use as context (once)" button
4. User clicks button → sets `onlineContextPending` → next `proposeActions` includes context

**With auto-use enabled (`PAPAYU_ONLINE_AUTO_USE_AS_CONTEXT=1`):**
1. `proposeActions` → error + `online_fallback_suggested`
2. UI calls `researchAnswer` automatically
3. UI displays online research block with "Auto-used ✓" badge
4. UI immediately calls `proposeActions` again with online context → displays new plan
5. If still fails → no retry (cycle protection)

## Build Status

-  Backend: `cargo build --lib` (2 warnings about unused code for future features)
-  Frontend: `npm run build`
-  Tests: `cargo test online_context_auto_test --lib` (5 passed)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-01-31 14:39:40 +03:00

285 lines
13 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.

# План Protocol v2
Минимальный набор изменений для v2 — без «воды».
---
## Diff v1 → v2 (схема)
| v1 | v2 |
|----|-----|
| `oneOf` (root array \| object) | всегда **объект** |
| `proposed_changes.actions` | только `actions` в корне |
| `UPDATE_FILE` с `content` | `PATCH_FILE` с `patch` + `base_sha256` (по умолчанию) |
| 5 kinds | 6 kinds (+ PATCH_FILE) |
| `content` для CREATE/UPDATE | `content` для CREATE/UPDATE; `patch`+`base_sha256` для PATCH |
Добавлено: `patch`, `base_sha256` (hex 64), взаимоисключающие правила (content vs patch/base).
---
## Главная цель v2
Снизить риск/стоимость «UPDATE_FILE целиком» и улучшить точность правок:
- частичные патчи,
- «операции редактирования» вместо полной перезаписи.
---
## Минимальный набор изменений
### A) Новый action kind: `PATCH_FILE`
Вместо полного `content`, передаётся unified diff:
```json
{ "kind": "PATCH_FILE", "path": "src/app.py", "patch": "@@ -1,3 +1,4 @@\n..." }
```
- Валидация патча локально.
- Применение патча транзакционно.
- Preview diff становится тривиальным.
### B) Новый action kind: `REPLACE_RANGE`
Если unified diff сложен:
```json
{
"kind": "REPLACE_RANGE",
"path": "src/app.py",
"start_line": 120,
"end_line": 180,
"content": "новый блок"
}
```
Плюсы: проще валидировать. Минусы: зависит от line numbers (хрупко при изменениях).
### C) «Base hash» для UPDATE/PATCH
Исключить race (файл изменился между plan/apply):
```json
{ "kind": "PATCH_FILE", "path": "...", "base_sha256": "...", "patch": "..." }
```
Если hash не совпал → Err и переход в PLAN.
---
## Совместимость v1/v2
- `schema_version=1` → нынешний формат (UPDATE_FILE, CREATE_FILE, …).
- `schema_version=2` → допускает `PATCH_FILE` / `REPLACE_RANGE` и расширенные поля.
В коде:
- Компилировать обе схемы: `llm_response_schema.json` (v1), `llm_response_schema_v2.json`.
- Выбор активной по env: `PAPAYU_PROTOCOL_DEFAULT` или `PAPAYU_PROTOCOL_VERSION` (default 2).
- Валидация/парсер: сначала проверить schema v2 (если включена), иначе v1.
---
## Порядок внедрения v2 без риска
1. Добавить v2 schema + валидаторы + apply engine.
2. Добавить «LLM prompt v2» (рекомендовать PATCH_FILE вместо UPDATE_FILE).
3. Golden traces v2.
4. **v2 default** с автоматическим fallback на v1 (реализовано).
---
## v2 default + fallback (реализовано)
- **PAPAYU_PROTOCOL_DEFAULT** (или PAPAYU_PROTOCOL_VERSION): default 2.
- **PAPAYU_PROTOCOL_FALLBACK_TO_V1**: default 1 (включён). При ошибках v2 (ERR_PATCH_APPLY_FAILED, ERR_NON_UTF8_FILE, ERR_V2_UPDATE_EXISTING_FORBIDDEN) — автоматический retry с v1.
- Fallback только для APPLY (plan остаётся по выбранному протоколу).
- Trace: `protocol_default`, `protocol_attempts`, `protocol_fallback_reason`.
- Лог: `[trace] PROTOCOL_FALLBACK from=v2 to=v1 reason=ERR_...`
**Compatibility:** Default protocol — v2. Apply может fallback на v1 при специфичных кодах ошибок (ERR_PATCH_APPLY_FAILED, ERR_NON_UTF8_FILE, ERR_V2_UPDATE_EXISTING_FORBIDDEN).
### Метрики для анализа (grep по trace / логам)
- `fallback_rate = fallback_count / apply_count`
- `fallback_rate_excluding_non_utf8` — исключить ERR_NON_UTF8_FILE (не провал v2, ограничение данных)
- Распределение причин fallback:
- ERR_PATCH_APPLY_FAILED
- ERR_NON_UTF8_FILE
- ERR_V2_UPDATE_EXISTING_FORBIDDEN
Trace-поля: `protocol_repair_attempt` (0|1), `protocol_fallback_stage` (apply|preview|validate|schema).
Цель: понять, что мешает v2 стать единственным.
### Graduation criteria (когда отключать fallback / v2-only)
За последние 100 APPLY:
- `fallback_rate < 1%`
- **ERR_PATCH_APPLY_FAILED** < 1% и чаще лечится repair, чем fallback
- **ERR_V2_UPDATE_EXISTING_FORBIDDEN** стремится к 0 (после tightening/repair)
- **ERR_NON_UTF8_FILE** не считается «провалом v2» (ограничение формата; можно отдельно)
- Для честной оценки v2 использовать `fallback_rate_excluding_non_utf8`
Тогда: `PAPAYU_PROTOCOL_FALLBACK_TO_V1=0` и, при необходимости, v2-only.
**protocol_fallback_stage** (где произошло падение): `apply` (сейчас), `preview` (если preview patch не применился), `validate` (семантика), `schema` (валидация JSON) добавить при расширении.
### Fallback: однократность и repair-first
- **Однократность:** в одном APPLY нельзя зациклиться; если v1 fallback тоже не помог Err.
- **Repair-first:** для ERR_PATCH_APPLY_FAILED и ERR_V2_UPDATE_EXISTING_FORBIDDEN сначала repair v2, потом fallback. Для ERR_NON_UTF8_FILE fallback сразу.
- **Trace:** `protocol_repair_attempt` (0|1), `protocol_fallback_attempted`, `protocol_fallback_stage` (apply|preview|validate|schema).
### Еженедельный отчёт (grep/jq)
Пример пайплайна для анализа трасс (trace JSON в одной строке на файл):
```bash
# APPLY count
grep -l '"event":"LLM_PLAN_OK"' traces/*.json 2>/dev/null | wc -l
# fallback_count (protocol_fallback_attempted)
grep '"protocol_fallback_attempted":true' traces/*.json 2>/dev/null | wc -l
# breakdown по причинам
grep -oh '"protocol_fallback_reason":"[^"]*"' traces/*.json 2>/dev/null | sort | uniq -c
# repair_success (protocol_repair_attempt=0 и нет fallback в следующей трассе) — требует связки
jq -s '[.[] | select(.event=="LLM_PLAN_OK" and .protocol_repair_attempt==0)] | length' traces/*.json 2>/dev/null
# top paths по repair_injected_sha256
grep -oh '"repair_injected_paths":\[[^]]*\]' traces/*.json 2>/dev/null | sort | uniq -c | sort -rn | head -20
```
**System prompt v2** (`FIX_PLAN_SYSTEM_PROMPT_V2`): жёсткие правила PATCH_FILE, base_sha256, object-only, NO_CHANGES. Включается при `PAPAYU_PROTOCOL_VERSION=2` и режиме fix-plan/fixit.
**Формат FILE-блока v2:**
```
FILE[path/to/file.py] (sha256=7f3f2a0c9f8b1a0c9b4c0f9e3d8a4b2d8c9e7f1a0b3c4d5e6f7a8b9c0d1e2f3a):
<content>
```
sha256 от полного содержимого файла; **не обрезается** при context-diet. Модель копирует его в `base_sha256` для PATCH_FILE.
### Prompt rules (оптимизация v2)
- Патч должен быть **минимальным** меняй только нужные строки, не форматируй файл целиком.
- Каждый `@@` hunk должен иметь 13 строки контекста до/после изменения.
- Не делай массовых форматирований и EOL-изменений.
- Если файл не UTF-8 или слишком большой/генерируемый верни PLAN (actions=[]) и запроси альтернативу.
**Авто-эскалация при ERR_PATCH_APPLY_FAILED** (опционально): при repair retry добавить «Увеличь контекст hunks до 3 строк, не меняй соседние блоки
---
## PATCH_FILE engine (реализовано)
- **Модуль `patch`:** sha256_hex, is_valid_sha256_hex, looks_like_unified_diff, apply_unified_diff_to_text (diffy)
- **tx::apply_patch_file_impl:** проверка base_sha256 применение diff EOL нормализация запись
- **Preview:** preview_patch_file проверяет base_sha256 и применимость, возвращает patch в DiffItem
- **Коды ошибок:** ERR_PATCH_NOT_UNIFIED, ERR_BASE_MISMATCH, ERR_PATCH_APPLY_FAILED, ERR_BASE_SHA256_INVALID, ERR_NON_UTF8_FILE
- **Repair hints:** REPAIR_ERR_* для repair flow / UI
---
## ERR_NON_UTF8_FILE и ERR_V2_UPDATE_EXISTING_FORBIDDEN
**ERR_NON_UTF8_FILE:** PATCH_FILE работает только по UTF-8 тексту. Для бинарных/не-UTF8 файлов только CREATE_FILE (если явно нужно), иначе отказ/PLAN. Сообщение для UI: «Файл не UTF-8. PATCH_FILE недоступен. Перейди в PLAN и выбери другой подход
**ERR_V2_UPDATE_EXISTING_FORBIDDEN:** В v2 UPDATE_FILE запрещён для существующих файлов. Семантический гейт: если UPDATE_FILE и файл существует ошибка. Repair: «Сгенерируй PATCH_FILE вместо UPDATE_FILE».
---
## Рекомендации для v2
- В v2 модификация существующих файлов **по умолчанию** через `PATCH_FILE`.
- `base_sha256` обязателен для `PATCH_FILE` и проверяется приложением.
- При `ERR_BASE_MISMATCH` требуется новый PLAN (файл изменился).
- В APPLY отсутствие изменений оформляется через `NO_CHANGES:` и `actions: []`.
---
## Примеры v2 ответов
### PLAN (v2): план без изменений
```json
{
"actions": [],
"summary": "Диагноз: падает из-за неверной обработки None.\nПлан:\n1) Прочитать src/parser.py вокруг функции parse().\n2) Добавить проверку на None и поправить тест.\nПроверка: pytest -q",
"context_requests": [
{ "type": "read_file", "path": "src/parser.py", "start_line": 1, "end_line": 260 },
{ "type": "read_file", "path": "tests/test_parser.py", "start_line": 1, "end_line": 200 }
],
"memory_patch": {}
}
```
### APPLY (v2): PATCH_FILE на существующий файл
`base_sha256` должен совпасть с хэшем текущего файла.
```json
{
"actions": [
{
"kind": "PATCH_FILE",
"path": "src/parser.py",
"base_sha256": "7f3f2a0c9f8b1a0c9b4c0f9e3d8a4b2d8c9e7f1a0b3c4d5e6f7a8b9c0d1e2f3a",
"patch": "--- a/src/parser.py\n+++ b/src/parser.py\n@@ -41,6 +41,10 @@ def parse(value):\n- return value.strip()\n+ if value is None:\n+ return \"\"\n+ return value.strip()\n"
},
{
"kind": "PATCH_FILE",
"path": "tests/test_parser.py",
"base_sha256": "0a1b2c3d4e5f60718293a4b5c6d7e8f90123456789abcdef0123456789abcdef0",
"patch": "--- a/tests/test_parser.py\n+++ b/tests/test_parser.py\n@@ -10,7 +10,7 @@ def test_parse_none():\n- assert parse(None) is None\n+ assert parse(None) == \"\"\n"
}
],
"summary": "Исправлено: parse(None) теперь возвращает пустую строку. Обновлён тест.\nПроверка: pytest -q",
"context_requests": [],
"memory_patch": {}
}
```
### APPLY (v2): создание файлов (как в v1)
```json
{
"actions": [
{ "kind": "CREATE_DIR", "path": "src" },
{
"kind": "CREATE_FILE",
"path": "README.md",
"content": "# My Project\n\nRun: `make run`\n"
}
],
"summary": "Созданы папка src и README.md.",
"context_requests": [],
"memory_patch": {}
}
```
### APPLY (v2): NO_CHANGES
```json
{
"actions": [],
"summary": "NO_CHANGES: Код уже соответствует требованиям, правки не нужны.\nПроверка: pytest -q",
"context_requests": [],
"memory_patch": {}
}
```
---
## Ошибки движка v2
| Код | Когда | Действие |
|-----|-------|----------|
| `ERR_BASE_MISMATCH` | Файл изменился между PLAN и APPLY, sha256 не совпал | Вернуться в PLAN, перечитать файл, обновить base_sha256 |
| `ERR_PATCH_APPLY_FAILED` | Hunks не применились (контекст не совпал) | Вернуться в PLAN, запросить более точный контекст, перегенерировать патч |
| `ERR_PATCH_NOT_UNIFIED` | LLM прислал не unified diff | Repair-ретрай с требованием unified diff |