Режим планирования

Режим планирования

Режим планирования заставляет AI составлять план, прежде чем действовать. Он запускается автоматически на первом сообщении пользователя в чате: выделенный агент planner, работающий только на чтение, либо выдаёт финальный план, либо сначала задаёт несколько уточняющих вопросов. План показывается пользователю, после чего агент assistant выполняет ту же задачу, получив план как директиву.

Планирование никогда не трогает рабочую папку: у planner нет инструментов записи и выполнения, а code-sandbox на время фазы планирования удаляется из активного набора MCP.

Что видит пользователь

  1. Отправьте первое сообщение в чате. Статус показывает «Составляем план…».
  2. Происходит одно из двух:
  • Уточняющие вопросы — карточка с максимум 3 вопросами, у каждого 3 готовых варианта и 4-я кнопка «Свой вариант». Поле ввода заблокировано, пока вопросы не отвечены (отправка текста показывает «Сначала ответьте на вопросы планировщика»). Активна только последняя карточка; предыдущие становятся историей только для чтения.
  • Финальный план — карточка с Шагами плана, Библиотеками (название / назначение / использование) и строкой Песочница (создать или переиспользовать). Выполнение начинается сразу после карточки плана.
  1. У любой карточки вопросов есть кнопка «Отменить план». Отмена удаляет весь хвост планирования — неотвеченные карточки вопросов и синтетические ответы — так что чат возвращается в состояние сразу после исходного сообщения.

Ответы по умолчанию нельзя вводить текстом с клавиатуры — цель в том, чтобы ускорить планирование, предлагая собственные наилучшие варианты planner. 4-я кнопка существует именно для случая, когда ни один из них не подходит.

Жизненный цикл

ai:sendChat
  ├─ есть planPending? ─► ERR_PLAN_PENDING (ответить или отменить через карточки)
  ├─ число ходов пользователя == 0 ─► tryPlanMode()
  │     ├─ ensurePlanProfile()  переключить mcp.json на профиль только-чтение, перезапустить ядро при изменении
  │     ├─ runPlannerTurn()     один «тихий» ход planner (agent=planner)
  │     │     ├─ ```plan-questions``` ─► сохранить карточку Q&A + planPending ─► phase=questions
  │     │     ├─ ```plan```           ─► сохранить план ─► executeAfterPlan()
  │     │     └─ ни того, ни другого  ─► восстановить managed-профиль ─► обычное выполнение
  │     └─ planner недоступен          ─► восстановить managed-профиль ─► обычное выполнение
  └─ иначе                                ─► РЕЖИМ ВЫПОЛНЕНИЯ

Ответ на вопрос (ai:sendPlanAnswer) повторно применяет профиль только-чтение и запускает planner снова с накопленными ответами. Planner может либо задать следующий вопрос, либо выдать финальный план. План всегда завершает планирование: план сохраняется, обычный MCP-профиль восстанавливается, и выполнение запускается один раз — планирование не повторяется на последующих ходах чата.

Редактирование и повторная отправка самого первого сообщения сбрасывает счётчик ходов в ноль, поэтому режим планирования запускается снова для отредактированного сообщения. Это ожидаемо: задача фактически начинается заново.

Машина состояний

Состояние Хранится в сессии Смысл
questions planPending = {userText, filePaths, files, questions, answers, sandbox, context7Available} Ожидание ответа минимум на один вопрос.
plan plan = <документ плана>, planPending = null План создан, выполнение уже прошло.
(нет) оба пусты Обычный режим выполнения.

Формы сообщений, сохраняемых фазой планирования:

  • { role: 'user', content, files } — исходная задача (первый обмен).
  • { role: 'assistant', content, questions, plan: null } — карточка вопроса; content — читаемый markdown, никогда не сырой fenced-JSON.
  • { role: 'user', content: '<вопрос>\nОтвет пользователя: <ответ>', planAnswer: true } — синтетический ответ. Он исключён из зеркала истории ядра, из «изменить/отправить заново» и из счётчика «первый обмен».
  • { role: 'assistant', content, plan: <документ плана>, questions: null } — карточка плана.

Порядок всегда повторяет интерфейс: сообщение пользователя → карточка Q&A/плана → ответ.

Документ плана

buildPlanDoc() формирует объект, который сохраняется как session.plan и отображается карточкой плана:

{
  "text": "читаемый markdown-план",
  "title": "короткое название",
  "aim": "что должно быть достигнуто",
  "steps": [{ "action": "…", "expected": "…" }],
  "libraries": [{ "name": "…", "purpose": "…", "usage": "…" }],
  "sandbox": { "policy": "reuse|create", "notes": "…", "dir": "…", "exists": true },
  "questions": null,
  "files": ["report.xlsx"],
  "context": { "workspacePath": "…", "context7Available": true },
  "createdAt": "2026-09-17T…"
}

Гарантия «только чтение»

Ядро раздаёт одни и те же авто-выданные инструменты mcp_* каждому агенту, поэтому «только чтение» обеспечивается содержимым mcp.json, а не определением агента:

Профиль filesystem context7 code-sandbox
Планирование (applyPlanServers) только-чтение подмножество доступен, если установлен отсутствует
Выполнение (applyManagedServers) полный набор если классифицирован если классифицирован

Подмножество только-чтения файловой системы: read_text_file, read_media_file, read_multiple_files, list_directory, list_directory_with_sizes, search_files, directory_tree, get_file_info. Записи, перемещения и создание каталогов отсутствуют в allowlist, поэтому planner не может изменить рабочую папку даже случайно.

Профиль плана детерминирован: он не выводится из набора непрерывности выполнения, поэтому ранее включённый code-sandbox не переносится в планирование.

Агент planner

Параметры агента:

agents:
  assistant:            # первый → Core default_agent_name
    tools: []
    prompt_files: [<системный промпт>, <instructions.md проекта>]
  planner:
    tools: []           # существуют только авто-выданные инструменты MCP
    delegate_allowed: false
    prompt_files: [~/.levpro-ai/prompts/planner.md, <instructions.md проекта>]
  • Загрузчик конфига ядра отвергает неизвестные ключи агента, поэтому запись planner содержит только эти ключи — никакого temperature.
  • RPC chat ядра уже принимает параметр agent; Desktop вызывает lifecycle.sendChat({ agent: 'planner', quiet: true }). Изменений в ядре не требуется.
  • Системный промпт planner'а записывается идемпотентно в ~/.levpro-ai/prompts/planner.md. В нём зафиксированы жёсткие правила (только чтение, без вопросов о песочнице, ≤3 вопросов × 3 варианта, называть библиотеки и способ их использования, отвечать на языке пользователя) и два выходных fence.
  • instructions.md проекта, если непустой, добавляется в промпты planner после промпта planner'а.

Выходные fence

Planner должен завершить ровно одним из:

  1. plan-questions — от 1 до 3 вопросов (JSON одной строкой):
{"questions":[{"id":"q1","question":"…","options":["…","…","…"]}]}
  1. plan — финальный план (перед ним может идти читаемый markdown):
{
  "title": "…",
  "aim": "…",
  "steps": [{ "action": "…", "expected": "…" }],
  "libraries": [{ "name": "…", "purpose": "…", "usage": "…" }],
  "sandbox": { "policy": "reuse|create", "notes": "…" }
}

Парсер parsePlannerOutput() терпит висячие запятые и принимает plan_json как алиас. Fence удаляются из текста пузыря; отображаются только их нормализованные карточки, поэтому сырой JSON не дублируется в чате.

Гарантии и крайние случаи

  • Порядок перезапуска ядра важен. Ядро читает mcp.json только при старте, поэтому переключение профиля требует перезапуска. И generateConfig() сам перезаписывает mcp.json через refreshAlwaysOnForWorkspace(), так что порядок строго такой: generateConfig() → applyPlanServers() → lifecycle.restart(). Перезапуски вне генерации используют детерминированный restartCoreNow() (без 15-секундного троттлинга).
  • Нет утечки «только чтение». Если ход planner'а провалился, tryPlanMode сначала детерминированно восстанавливает managed-профиль (выполнения) и только потом возвращает null как запасной путь — иначе следующий ход прошёл бы без code-sandbox.
  • Тихие ходы. Planner ничего не стримит в интерфейс. Lifecycle._quiet подавляет события потока; до рендера доходит только ai:planStatus.
  • Переиспользование песочницы. Если {workspace}/.sandbox уже существует, planner получает об этом информацию, и его план должен установить sandbox.policy: "reuse"; выполнение тогда переиспользует ту же среду вместо пересоздания.
  • Доступность Context7 передаётся planner как факт, чтобы он мог уточнять точные имена и API библиотек. Когда Context7 не установлен, факт равен «недоступен», и planner работает на собственных знаниях.
  • Отмена удаляет только карточки вопросов и синтетические ответы; исходное сообщение пользователя сохраняется, чтобы его можно было отправить снова.

Связанные страницы