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>
285 lines
13 KiB
Markdown
285 lines
13 KiB
Markdown
# План 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 должен иметь 1–3 строки контекста до/после изменения.
|
||
- Не делай массовых форматирований и 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 |
|