Skip to content

Latest commit

 

History

History

LakeCode.Core

README.md

LakeCode.Core

Независимое от SharpConsoleUI ядро. CLI ссылается на Core, обратной зависимости нет.

MCP с lazy loading

/mcp настраивает stdio и Streamable HTTP серверы. В Build агент начинает с search_mcp_tools; каталог хранится в приложении, а схемы найденных функций добавляются в текущий цикл MAF по запросу. Раскрытие сохраняется до конца сессии и при её открытии. Plan, Research и сабагенты не получают MCP. MCP-вызовы и eval bridge используют общий каталог, события, отмену и политику подтверждения сервера. Секретные настройки отделены от истории и persisted queue.

Настройка, формат mcp.json, ограничения и ручная проверка: docs/mcp.md.

Генерация изображений через Responses

/models → Прочее → секция Картинки задаёт отдельные подключение OpenAI Responses, чат-модель-посредник (carrier) и произвольный image model ID. Carrier передаётся в верхний model, image model ID — в tools[].model. Назначение хранится в settings.json как ImageGeneration и не наследует основную модель, планирование или сабагентов. Сохранение настройки не делает запросов к API. Наличие carrier в каталоге не подтверждает поддержку hosted image_generation на сервере. Секция свёрнута; её заголовок показывает текущее назначение (image ID · через carrier). Первым в секции показан список «подключение · image-модель», под ним — компактный dropdown «Посредник». Carrier выбирается явно из того же подключения, без автоматического выбора первой модели; ранее выбранный carrier сохраняется в черновике для каждого подключения. Кнопка «Ручной ID» открывает отдельный диалог для image ID вне discovery. Ручной ID не подменяет выбор основного списка при обновлении discovery. ModelCatalog.ImageModels содержит известные GPT Image ID без выдуманных чат-лимитов. ModelType различает Chat, ImageGeneration и Unknown; тип определяется точным совпадением с каталогом, не префиксом или общим серверным type: model. gpt-image-2.5 — известный gateway alias, не обещание поддержки конкретного API. Image-модели не предлагаются как carrier и отображаются в /providers без требования чат-характеристик. В image-списке показываются только модели типа ImageGeneration; чат-модели и неизвестные ID не включаются даже при совпадении с текущим вводом. Отсутствие ID в списке не удаляет его и не запрещает явное ручное image-назначение через диалог «Ручной ID».

generate_image(prompt, output_path, input_paths?) — локальная функция основного агента в Build и Research, включая eval bridge. В Plan и read-only сабагентах она запрещена общим фильтром. В Research путь output_path и все input_paths ограничены временной папкой сессии. Аргументов модели, провайдера, URL или credentials у инструмента нет. Системные инструкции запрещают заменять этот путь скриптом и самостоятельно менять назначение. Как и другие инструменты, это не OS sandbox. Без input_paths выполняется генерация, с ними — редактирование с оригинальными PNG/JPEG-референсами. Маски, batch, превью и автоматическое вложение результата в контекст основного агента не поддерживаются.

ConversationService фиксирует назначение и неизменяемый профиль при принятии сообщения, включая состояние недоступности. Очередь и фоновые продолжения сохраняют этот снимок в памяти. В PendingMessage.ImageGeneration записываются только три ID; runtime-профиль имеет JsonIgnore. После перезапуска восстанавливается сохранённое назначение через актуальный профиль того же ID, а не глобальная новая настройка. Старые очереди без поля означают «генерация не настроена». Недоступное назначение не блокирует обычный чат, но вызов инструмента завершается ошибкой без сети. Форматы settings/queue остаются прежними: добавлены необязательные поля, старые данные читаются.

Providers/ResponsesImageGenerationClient.cs делает один изолированный /responses запрос: store: false, stream: true, принудительный image_generation с выбранной моделью и PNG-выводом. Передаются только prompt и референсы, не история и не AGENTS.md. Авторизация берётся из выбранного подключения; OAuth Codex и запуск CLI не используются. HTTP redirects, routing fallback и автоматические повторы запрещены. Таймаут 3 минуты, размер распакованного ответа ограничен 48 MiB. SSE разбирается стандартным SseParser; итоговый completed-output имеет приоритет над item-done, без двойной записи. Обрыв до подтверждённого завершения, отказ, пустой результат, несколько изображений и неверные данные — явные ошибки. Сырые ответы сервера и base64 не включаются в результат инструмента.

Референсы используют ограничения ImageAttachments: до 4 файлов, до 8 MiB каждый и 24 MiB суммарно; декодер проверяет PNG/JPEG и лимиты пикселей. Внешние пути требуют обычного разрешения чтения. Выходной путь — новый относительный .png внутри workspace, без символических ссылок и перезаписи. Выход тоже проходит декодирование и ограничения ImageAttachments; серверный WebP не переименовывается в PNG. WorkspaceFiles использует общий атомарный commit для текста и изображения, сохраняя прежний текстовый лимит 1 MiB. Ошибка или отмена до commit не оставляет частичный файл. После commit Stop/rewind не удаляют результат. Ошибка сохранения после генерации не приводит к повторному платному запросу.

Карточка инструмента содержит путь, MIME, фактические размеры, байты, подключение, carrier, запрошенную и сообщённую сервером image-модели. При несовпадении либо отсутствии сообщённой модели PNG сохраняется с предупреждением; несовпадение видно также в заголовке карточки. Сообщённая модель — метаданные сервера, не независимое доказательство внутренней маршрутизации. История сохраняет текст результата и путь, но не копию изображения; удалённый вручную файл из истории не восстанавливается.

Ручная проверка изображений

После сборки пользователь запускает приложение (автоматические тесты не создаются и не выполняются):

  1. В /models назначить Responses carrier и image ID; сохранение не должно обращаться к сети. Сменить основную модель: назначение изображений остаётся прежним. Отмена окна сохраняет старые настройки.
  2. В Build сгенерировать PNG по новому пути, открыть его вручную. Проверить размеры и metadata карточки. Проверить сервер, игнорирующий image ID: файл сохраняется, предупреждение видно в карточке и истории.
  3. Отредактировать PNG/JPEG с новым output_path, в том числе с несколькими референсами. Проверить внешний путь с разрешением/отказом, неподдерживаемый формат, лимиты, существующий output и выход за workspace.
  4. В Plan и у сабагента инструмент недоступен; eval bridge не обходит ограничения. Без назначения и после удаления подключения обычный чат работает, генерация сообщает причину без fallback.
  5. Поставить сообщение в очередь и изменить назначение/ключ: принятый запрос использует прежний снимок. Перезапустить: очередь на паузе, назначение восстанавливается по сохранённым ID, ключ в pending/session отсутствует. Старые очереди не подхватывают новое глобальное image-назначение. Проверить фоновые продолжения и /implement.
  6. Проверить HTTP-ошибку, отказ, обрыв SSE, отмену, отсутствие картинки, дубли item-done/completed и ошибку сохранения: нет автоматического повтора или частичного файла. После commit Stop/rewind файл остаётся.
  7. Проверить PNG-вывод и редактирование на используемом шлюзе отдельно: прежняя проверка WebP-генерации через curl не подтверждает эти режимы. Если шлюз возвращает не PNG, инструмент сообщает ошибку.

Три контракта

  • Команды (Conversations/ConversationCommand.cs) — намерения пользователя. ConversationService.Execute возвращает принятие команды, а не завершение генерации. Отправка фиксирует неизменяемый профиль подключения, модель, reasoning и собственную копию оригинальных байтов вложений.
  • События (Conversations/ConversationEvent.cs) — произошедшие факты: начало ответа, текстовые фрагменты, вызовы инструментов, завершение, ошибки и запросы решения. EventOccurred передаёт envelope с последовательным номером, ID сессии и запроса. Подписка не запускает агент и не воспроизводит старые события.
  • Состояние (Conversations/ConversationState.cs) — текущие флаги выполнения, очередь, доступность операций, заголовок и статистика. State доступен сразу; StateChanged уведомляет о новом снимке. GetConversation() возвращает отдельный снимок истории и восстановленного черновика.

Сервис один раз читает поток MAF и владеет сессией, очередью, отменой, rewind и сохранением. Изменения состояния и публикация уведомлений сериализованы внутренней блокировкой. Обработчики уведомлений должны только ставить обновления в очередь своего представления: не блокировать, не бросать исключения и не выполнять команды синхронно из обработчика. CLI переводит уведомления на UI-поток в ChatController.

Ответы на вопросы и разрешения — отдельные команды с ID взаимодействия. Они не ожидают завершения генерации. Повторные и запоздалые решения не принимаются; отмена снимает ожидание и публикует его завершение.

Запрос пользователя сохраняется до внешнего обращения. Следующий запрос начинается после финального сохранения предыдущего. FinishAsync ожидает работу и записи очереди, включая финальное сохранение при shutdown. Выполненные побочные эффекты не откатываются при отмене или rewind. Восстановленная очередь видна, но остаётся на паузе до явного запуска.

Attachments содержит оригинальные байты и общую проверку изображения; clipboard, выбор файлов, PixelBuffer и превью остаются в CLI. Используется MAF 1.24.0. Формат сессий версии 5 дополнительно хранит checkpoint компактизации и каталог ссылок в AgentState; версия 4 хранит режим запросов и планы вместе с ProviderId и метаданными выбранной модели; версии 1–3 читаются с режимом Build по умолчанию, версии 1 и 2 — как сессии исходного WikiVibe-подключения. Rx-пакеты и универсальный event bus не используются.

Организация Conversations

ConversationService — единый координатор без partial-классов. Он принимает команды, владеет очередью, текущей агентской сессией, cancellation и задачами, один раз читает основной поток MAF, сериализует изменения состояния и публикацию событий. Следующий ход начинается только после финального сохранения. Компоненты не получают ссылку на сервис или общий изменяемый ConversationContext.

  • Messages/MessageSubmissionPreparer разрешает настройки, проверяет вложения и фиксирует принятый PendingMessage. Разрешение модели выполняется до проверок очереди, проверка вложений — после них; постановка в очередь, Clarify/Interrupt и сохранение остаются в сервисе.
  • Messages/ConversationMessageProcessor применяет отдельные contents к одному ходу и возвращает факты и статистику. Он не читает поток и не публикует события; время первого токена фиксируется до state lock, скорость — до сериализации сессии.
  • Interactions/ConversationQuestions и ConversationApprovals хранят регистрации вопросов и двух типизированных видов разрешений. InteractionGate сериализует их показ; PendingInteraction связывает ожидание с отменой. Регистрации и решения обрабатываются под lock сервиса, ожидание — вне него. Классификация исполнения и файловая политика остаются в своих подсистемах.
  • SideQuestions/SideQuestionConversation владеет историей и revision /btw, бюджетом и обработкой ответа. Сервис владеет задачей и cancellation, учитывает её в shutdown и отбрасывает обновления очищенного запроса.
  • Compaction/ConversationCompaction готовит, сохраняет и активирует checkpoint. Между успешным сохранением и активацией нет дополнительного await. Сервис проверяет доступность, фиксирует модель, запускает операцию и применяет результат; предупреждение о вспомогательных метаданных не отменяет commit.
  • Rewind/ConversationRewind рассчитывает точки и план отката, выполняет сохранение и применение истории, восстанавливает исходное состояние при ошибке. Узкие функции restore/rewind сохраняют сервисный порядок сброса eval и делегирования. Ошибка восстановления явно блокирует дальнейшую отправку; файлы не откатываются.
  • Projection/ConversationSnapshotMapper копирует snapshot и строит immutable transcript views. CreateState остаётся в координаторе. Файловое хранение по-прежнему реализует Sessions/SessionStorage.

Ручная проверка после сборки:

  1. Очередь, Clarify, Interrupt и Stop: следующий ход начинается после сохранения предыдущего.
  2. Вопросы, внешнее чтение и подтверждение исполнения, включая вложенный eval: отмена, поздний/повторный ответ, переключатель разрешений; одновременно показывается одно взаимодействие.
  3. /btw во время основного хода: отмена, очистка, поздние фрагменты и shutdown не портят основной разговор.
  4. /compact: отмена и ошибка записи не активируют новый checkpoint; предупреждение после commit не отменяет его.
  5. Rewind до пользователя, после ответа и после tool batch: draft, план, компактизация и восстановление при ошибке.
  6. Открытие сессии, фоновое продолжение и выход: очередь восстановлена на паузе, все операции и записи дожидаются завершения.

Организация Agent

  • Role/Build, Role/Plan, Role/Research содержат композицию инструментов, сборщики промптов и инструкции режимов. Каждый SystemPromptBuilder явно перечисляет собственные и общие секции через Prompt/SystemPromptComposer; общего готового шаблона, маркера режима и Replace нет. Порядок секций ставит общие для Build и Plan тексты первыми, а ролевые (Instructions, PlanTemplate, Mcp) — после них, чтобы общий префикс запроса совпадал и провайдер мог переиспользовать кэш. Sections/ToolUse разделён по темам и собирается по роли: ToolUse.Text (Build), ToolUse.ForPlan (без write/edit/image) и ToolUse.ForSubagent (только чтение). Контекст среды — отдельная секция. Prompt/InstructionsReader один раз читает AGENTS.md; те же снимки используют роли и сабагенты. Роль и параметры хода применяет Execution/AgentRunPreparer; AgentLoop только координирует подготовку и streaming-запуск.
  • Tools/Registration/AgentTools — единый эффективный каталог функций текущего хода. Все регистрации в ролях используют agent.AddTool(...), включая группу функций из SearchTools.AgentTools(), историю, делегирование и submit_plan; при необходимости дополняются .Policy(...) и .EvalAllowed(...). AgentToolServices создаёт экземпляры инструментов и связывает зависимости при создании сессии, а роли только выбирают набор. Готовые AIFunction и прямые вызовы AIFunctionFactory в роли не передаются. Отдельные ToolsBuilder и ролевые ToolPolicy не используются.
  • Policy(ToolExecutionPolicy.RequireApproval) включает процедуру классификации/подтверждения конкретного вызова; требуется непустой description. Она применяется общим Tools/Execution/ToolInvocationRunner и не заменяет проверки аргументов и файлового доступа. В Plan так зарегистрированы bash и eval.
  • EvalAllowed по умолчанию true. Bridge и Tools.ListToolsAsync() используют тот же каталог с фильтром EvalAllowed, включая функции skills, делегирования и submit_plan. Для самого eval задано false: рекурсивный eval запрещён. run_skill_script исключается и из контекста модели, и из каталога bridge: исполнение skill scripts не включено.
  • Skills/RoleAwareContextProvider сохраняет в каталоге фактические функции после обогащения контекста. Прямые вызовы и eval проходят одинаковые проверки, публикуют события и учитывают завершение хода; отправка плана через eval завершает внешний вызов штатно через MAF Terminate и блокирует дальнейшие bridge-вызовы этого хода.
  • Tools/Files/WorkspaceFiles использует общий FileSettings с неизменяемыми снимками доступа и видимости. Переключение внешнего чтения не пересоздаёт инструменты. Фильтр .git/bin/obj проверяется по мере обхода list_directory и поисковых инструментов; скрытие не запрещает чтение по явному пути. Нового UI для списка папок нет.
  • Выключение общего разрешения не прерывает открытое чтение и не удаляет сохранённые решения директорий. Включение сохраняет существующее разрешение ожидающих запросов; автоматическое согласие не запоминается как разрешение директории. Research по-прежнему не получает внешний доступ независимо от переключателя.
  • Composition/AgentFactory явно собирает основной агент и read-only сабагенты. AgentComponents владеет ресурсами сессии и освобождает делегирование, eval и клиент; компоненты получают конкретные зависимости, а не общий service locator. AgentLoop — внутренний исполнитель без прежних конструкторов и прокси к истории; публичные команды, события и состояние ConversationService не меняются.
  • Execution/AgentRunRequest содержит принятые параметры хода; AgentRunPreparer выбирает роль, сбрасывает состояние хода, проверяет вход и историю, настраивает подключение и формирует PreparedAgentRun. ReasoningSummaryFormatter потоково разделяет независимые reasoning summary, не накапливая весь ответ. Собственного plugin framework и универсальных lifecycle hooks нет.
  • Session/AgentSessionState владеет MAF-сессией и её сериализацией. Session/History/AgentHistory выполняет rewind, восстановление прерванной истории, нормализацию и операции компактизации; InterruptedHistoryChatClient только захватывает запросы и ответы. История не владеет eval или делегированием: ConversationService явно сбрасывает их перед restore/rewind. Формат сохранений и пространства имён перемещённых файлов истории сохраняются.
  • Tools/Execution/ToolInvocationRunner остаётся общим путём для MAF и eval bridge; ToolExecutionApproval выполняет классификацию и ручное подтверждение с паузой timeout вложенного eval. Штатный function middleware — адаптер к этому пути, а не отдельный набор проверок только для MAF.
  • Сабагенты находятся в Subagents, не в Role: SubagentRunner получает отдельную композицию через AgentFactory.CreateSubagent и использует тот же базовый loop. Побочные вопросы находятся в SideQuestions; Titles/SessionTitleGenerator вызывается сервисом разговора отдельно от основного агента.

Структурированный поиск и политика bash

LakeCode.Core ссылается на самостоятельный LakeCode.Search через ProjectReference. Прежний grep tool удалён. Tools/Files/Search/SearchTools регистрирует search_text (точный literal/regex-поиск), find_files (ранжирование путей), outline_file (синтаксическая структура) и trace_code (эвристические регионы). Все четыре доступны в Build/Plan/Research и read-only субагентах, через тот же каталог и eval bridge, когда eval доступен. Research остаётся ограничен временной папкой сессии.

WorkspaceSearchSource — адаптер DLL к общему WorkspaceFiles: разрешение внешнего пути запрашивает ToolInvocationRunner, каждое фактическое чтение повторно проходит ResolveRead/ReadTextAsync; обход использует текущий фильтр скрытых директорий, не следует по ссылкам и ограничивает число записей. Пути в выдаче приводятся к путям LakeCode. Инструменты ограничивают текстовый вывод, помечают усечение/пропущенные файлы; превышение рабочего бюджета — Error, а не пустой результат. Outlines и trace не заменяют проверку исходников и не являются семантическим графом вызовов. Managed/native-зависимости Tree-sitter распространяются транзитивно с приложением.

Tools/Bash/BashCommandPolicy проверяет прямые команды до процедуры подтверждения и повторно перед запуском процесса. Общий путь применяет эту политику и к bash через eval. Запрещены команды поиска/чтения файлов, Python/JS/другие интерпретаторы, вложенные shell и типовые wrappers, подстановки, перенаправления, shell-конструкции и фоновые команды. Кавычки/экранирование нормализуются при проверке имени команды. Простые цепочки с &&/||/; и допустимыми командами сохраняются. Bash запускается без профилей, BASH_ENV/ENV и импортированных функций. Это фильтр, не sandbox: содержимое произвольного executable или легитимного build/npm-скрипта не проверяется. Промпт запрещает использовать эту границу, MCP или прямые filesystem/process API eval для обхода retrieval tools. Для жёсткой изоляции потребуется отдельная политика запуска/песочница.

Ручная проверка после сборки (приложение и тесты агентом не запускаются):

  1. Переключить Build → Plan → Research → Build: проверить наборы tools, сохранение истории, изоляцию Research и сброс eval при входе в Plan.
  2. В Build/Plan запросить каталог через eval: доступны те же функции, включая skills и делегирование; eval и run_skill_script отсутствуют. В Plan недоступны write_file/edit_file/generate_image.
  3. В Plan проверить отдельное подтверждение eval и вложенного bash, отказ и Stop. Отправить план через eval: последующие bridge-вызовы отклоняются, внешний вызов завершается и план сохраняется.
  4. Во время вопроса о внешнем чтении включить переключатель, затем выключить: инструменты не пересоздаются, ранее выданные разрешения директорий сохраняются, Research не получает доступ к проекту.
  5. Проверить сабагента с ask_parent, продолжение и отмену; затем сохранение/открытие сессии, rewind, /compact и /btw.

Уточнения сабагентам

Основной агент использует send_subagent_message(taskId, message) в Build/Plan напрямую или через eval. Для Running текст ставится в FIFO-очередь и доставляется перед следующим запросом к модели, после завершения инструментов. Текущий инструмент и поток ответа не прерываются; если уточнение пришло во время финального ответа, после него начинается продолжение. Граница использует тот же PreserveInterruptedHistory, что и уточнения основному агенту: завершённые tool call/result сохраняются, сессия запускается строго последовательно.

Completed можно продолжить в той же сессии с исходными моделью, подключением и reasoning. Завершённые сессии сохраняются в памяти; при заполнении хранилища доставленные завершённые задачи очищаются. Stop, смена сессии, rewind и перезапуск также прекращают возможность продолжения. Failed/Cancelled не возобновляются. Для NeedsParentInput нужен прежний ответ через delegate(taskId, questionId, answer); уточнение, принятое до появления вопроса, остаётся в очереди до ответа родителя. Уточнения отображаются в существующем чате сабагента. Инструмент возвращает Queued при постановке в очередь, не подтверждение прочтения. Размер отдельного сообщения — до 16000 символов. Права сабагента не расширяются.

Ручная проверка после сборки: отправить уточнение во время инструмента, проверить порядок call/result → уточнение и отсутствие повторного выполнения; отправить несколько уточнений; продолжить Completed; проверить ask_parent, Stop и устаревший taskId.

Ручная компактизация /compact

В /models → «Сжатие» явно назначается отдельная чат-модель и reasoning. Без назначения команда отклоняется; наследования или скрытой замены модели нет. Назначение сохраняется в settings.json (CompactionModel). История передаётся выбранному подключению только по /compact; это платный запрос. Настройки фиксируются при принятии команды, ключи в checkpoint не записываются.

Команда доступна между ходами, без очереди, активного /btw, работающих/ожидающих сабагентов и недоставленных уведомлений. Это отдельная операция ConversationService, не пользовательский ход и не запуск основного агента. Stop (двойной Esc) и shutdown отменяют её; таймаут — пять минут. Суммаризатор не получает инструменты. Один streaming-запрос возвращает JSON с единственным обязательным полем text: свободный текст summary (можно Markdown). Категорий пунктов, обязательных цитат и диапазонов строк нет; необязательный preserveVerbatim выбирает дополнительные точные артефакты; нет второго LLM-проверяющего, автоматической разбивки, повторов или обрезания входа. Если история не помещается по приблизительной оценке, нужна модель с большим окном. Промпт просит начинать summary с текущей задачи: желаемый результат, известная причина и состояние работы, а не перечень действий. Согласованные решения, сообщения о реализации и подтверждённые проверки разделяются; успешная сборка не означает проверку поведения. Незакрытые вопросы не становятся разрешением на действия. Допустимы несколько подписанных ID ключевых блоков прямо в тексте: это необязательные навигационные подсказки, не проверенные цитаты и не новое поле схемы. Изменённый промпт применяется при следующем /compact, уже сохранённый текст summary автоматически не переписывается. Бюджет входа — min(MaxInputTokens, ContextWindowTokens), без вычитания выходных токенов. Лимит генерации в API-запросе не меняется; ограничения общего окна окончательно проверяет провайдер. Оценка входных токенов — (байты UTF-8 промпта и входа + 1024 байта запаса) / 4 с округлением вверх. Это не точный токенизатор: оценка может как завышать, так и занижать фактический размер.

Agent/Session/History/Compaction хранит производную карточку и каталог блоков с диапазонами сообщений и SHA-256 их канонического публичного представления. Исходники остаются в MAF-истории; дополнительной полной копии архива нет. Tool call/result группируются атомарно. UUID блоков сохраняются между сжатиями, при rewind удалённые ID не переиспользуются. Каталог содержит ВСЕ покрытые блоки независимо от того, какие из них LLM упомянула. Проверяются непустой текст, лимит размера, необязательные ID артефактов, целостность каталога, хеши и уменьшение текстовой проекции. Произвольные ссылки в тексте summary не считаются проверенными доказательствами. Новые checkpoint имеют PromptVersion 2; старые структурированные summary версии 1 продолжают читаться с прежней проверкой ссылок и диапазонов. Логическая истинность пересказа этими проверками не доказывается.

Последний настоящий пользовательский запрос, финальный ответ после инструментов (если есть), актуальный план и группы с оригинальными изображениями сохраняются. LLM может дополнительно выбрать preserveVerbatim; оригиналы копирует код. Изображения суммаризатору не передаются: только метаданные, без выдуманных описаний. Неизвестные типы содержимого приводят к явному отказу, а не к потере данных. Private provider metadata и encrypted reasoning не входят в summary.

ProvideOutputMessageFilter меняет только контекст, выдаваемый модельному клиенту. Полная история, HistoryStart, UI и границы rewind остаются архивными. InterruptedHistoryChatClient хранит отдельно фактический запрос для /btw и восстановленный полный запрос для отмены. После commit старый префикс /btw инвалидируется; новый появится после следующего обычного запроса. При откате внутрь покрытого диапазона checkpoint сбрасывается, после него — сохраняется. Выполненные действия не отменяются.

read_history(blockId, startLine, lineCount, startColumn) читает сохранённый публичный текст блока, проверяя его хеш. Строки — с 1, колонка — UTF-16 с 0; длинные строки возвращают NextLine/NextColumn. list_history(offset, count) постранично показывает весь каталог, в том числе нецитированные блоки. Описания формируются без LLM: роль и начало текста либо имена инструментов с путём/командой/фрагментом аргументов, без служебных CallId. Канонический текст, хеши и нумерация строк от этого не меняются. search_history(query, offset = 0, count = 20) ищет буквальный текст без учёта регистра по этому же каталогу, без индекса, кэша, regex и доступа к другим сессиям. Новые сообщения вне каталога не входят в поиск. На каждый найденный блок возвращаются ID, строка первого совпадения, колонка совпадения MatchColumn и ограниченный фрагмент Text с начальной колонкой Column. Line и Column можно передать в read_history как startLine/startColumn. Строки — с 1, колонки — UTF-16 с 0; фрагмент может пересекать строки. Хеш найденного блока проверяется до выдачи. count ограничивает число блоков (1–50), дополнительно ограничен размер сериализованных результатов; Truncated означает, что после фрагмента есть ещё текст блока. NextOffset — позиция в исходном каталоге, не число совпадений: продолжать с тем же query до null (последняя страница может быть пустой). Поиск поддерживает отмену между блоками. Все три инструмента доступны основному агенту в Build/Plan (включая eval bridge), но не сабагентам; другие сессии, произвольные пути и удалённая rewind-ветка недоступны. История — данные, не новые разрешения.

Повторный /compact читает полный исходный архив, не summary от summary. Checkpoint сохраняется атомарно в session.json до активации. Сбой обновления вспомогательного summary.json после commit сообщается отдельно; он не отменяет уже сохранённое сжатие. Старые сессии 1–4 читаются без checkpoint, старые клиенты не читают новую схему 5. Формат pending.json остаётся 4. Полный архив не уменьшается ни на диске, ни в RAM: функция уменьшает отправляемый контекст, а не размер хранения.

В CLI после завершения появляется раскрываемая карточка с текстом summary и отдельным usage. Она восстанавливается при открытии сессии; это не ответ агента и не разрешение выполнения плана. Размер до/после — UTF-8 публичной текстовой проекции, не токены, не стоимость и не полный payload.

Ручная проверка после сборки (приложение и тесты агентом не запускаются):

  1. Назначить модель, открыть длинную сессию, выполнить /compact. Раскрыть карточку; проверить числа, оговорки и запреты, дословность последнего запроса/ответа и сохранение полного UI-транскрипта.
  2. Попросить агента list_history, затем search_history по известной фразе с другим регистром и read_history по найденным координатам. Проверить длинные строки, Unicode, пагинацию, отсутствие совпадений и отмену. Описания каталога должны показывать текст/инструменты, а не служебные ID. Проверить несуществующий ID, пустой поисковый запрос и неправильный диапазон: ошибка, а не выдуманный результат.
  3. Продолжить несколько tool-циклов; остановить после инструмента и продолжить. Архив не должен заменяться проекцией, результаты и предупреждения о побочных эффектах должны сохраниться.
  4. Перезапустить, открыть сессию, откатиться до/внутри/после checkpoint и после группы инструментов. Проверить, что будущие сведения и ID исчезли, а новые сообщения не переиспользуют удалённые ID.
  5. Проверить Plan, реализацию сохранённого плана, /btw, смену провайдера и историю с изображениями.
  6. Без назначения, с малым окном, ошибкой API/JSON/ID артефактов, отменой и ошибкой записи предыдущая проекция остаётся действующей. Повторное сжатие без уменьшения не применяется.
  7. Изменить назначение во время /compact: принятая операция продолжает использовать прежний снимок.

Планирование и подтверждаемая диагностика

/plan выбирает Plan для следующих сообщений, /build — Build, /research — Research. Справа в футере отображается только выбранный режим: plan, build или research. Переключение не запускает запрос и не меняет активный ход или принятую очередь. Режим сохраняется в очереди и истории, наследуется фоновыми продолжениями. При открытии истории CLI выбирает режим последнего хода.

Prompt/InstructionsReader один раз читает глобальный и проектный AGENTS.md при создании сессии. Сборщики в Agent/Role/Build, Plan и Research формируют собственные промпты из этих снимков и общих секций. Каждый запуск получает инструкции выбранной роли и её набор функций через run options: на уровне MAF-агента инструменты не задаются, поскольку MAF объединяет агентские и request tools. Plan задаёт исследование причин, проверку гипотез, конкретную последовательность правок и критерии проверки; не разрешает реализацию даже при запросе «исправь». Skills не расширяют права режима.

Research — общение и веб-ресёрч без доступа к репозиторию. Промпт собирается Role/Research/SystemPromptBuilder без проектного AGENTS.md, ProjectInvestigation и Delegation; глобальный ~/.agents/AGENTS.md остаётся. Инструменты: read_file, list_directory, search_text, find_files, outline_file, trace_code, write_file, generate_image, web_search, read_web, ask_tool и history tools. bash, eval, edit_file, сабагенты, skills и submit_plan отсутствуют, в том числе через eval bridge. Корень файловых инструментов — временная папка сессии ~/.lakecode/sessions/<id>/workspace, которую создаёт SessionStorage.EnsureWorkspace; WorkspaceFiles для Research создаётся с allowExternalReads: false, поэтому пути вне неё отклоняются без запроса разрешения, а состояние сессии (session.json, pending.json, summary.json) недостижимо для записи. AgentSkillsProvider обёрнут в Skills/RoleAwareContextProvider, который в Research не добавляет инструкций и tools skills. Созданные файлы и PNG возвращаются абсолютным путём (WorkspaceFiles.ReportAbsolutePaths), потому что папка лежит вне проекта. Research использует основную модель и reasoning, а не PlanModel; фоновые уведомления сабагентов в него не доставляются.

В Plan прямые write_file/edit_file отсутствуют и блокируются общим перехватчиком, в том числе через C# bridge. bash/eval доступны с обязательным description (действие, цель, ожидаемые побочные эффекты) в обоих режимах. UI показывает описание в карточке. В Plan каждый вызов получает отдельное решение: ручное подтверждение либо оценку Jev при включённом классификаторе (см. ниже). Без «разрешить всё» и без повторного использования разрешений. Описание — текст модели, не проверка безопасности. Подтверждённый код исполняется с правами пользователя: это не sandbox.

ConversationService сериализует ExecutionApprovalRequested с вопросами и внешними разрешениями через общий interaction gate; ResolveExecutionApproval относится только к одному ID. Отказ возвращает явный невыполненный результат; Stop отменяет ожидание, поздние ответы не принимаются. Перед согласием bash не запускается, eval не отправляет код и не сбрасывает worker. Вложенный bash через eval требует своего решения; время классификации и ручного ожидания исключено из execution timeout родительского eval. При входе в Plan между ходами eval-worker сбрасывается, его переменные не переносятся из Build (это указано в UI и промпте).

submit_plan(markdown) сохраняет неизменяемый план с ID и ревизией в текущем ходе и штатно завершает stream через MAF Terminate. План выводится как Markdown в истории; он не является файлом проекта. Кнопка «Реализовать план vN» и /implement отправляют ExecutePlan с конкретным ID/ревизией и выбранными настройками модели. Принимается только актуальный завершённый план после сохранения, когда основной агент, пользовательская очередь и исследования свободны. Новый Build-запрос содержит копию плана, явное разрешение реализации и уведомление о возвращённых инструментах. Повторное нажатие не создаёт второй запрос. Новый пользовательский запрос инвалидирует прежнюю цель кнопки; частичный rewind удаляет план из сокращённого хода. Полные старые планы остаются в истории, но не дают разрешения записи.

Сессии имеют SchemaVersion 5 (режимы появились в версии 4); pending.json содержит объект версии 4 с Messages. Старые массивы очереди читаются. Старый клиент не сможет молча загрузить новые Plan-запросы как Build. Неизвестные режимы/версии отклоняются; восстановленная очередь по-прежнему остаётся на паузе. Research использует то же поле AgentMode, поэтому версия схемы не меняется: старый клиент отклонит неизвестное значение вместо тихой загрузки как Build. Временная папка сессии не удаляется и не входит в WorkspaceRoot, поэтому список сессий и проверка каталога проекта не меняются.

Ручная проверка Research

  1. /research, затем вопрос по сети: web_search/read_web работают, репозиторий не читается.
  2. read_file с абсолютным путём в проект и с ../: отказ без запроса разрешения; в футере research.
  3. write_file и generate_image создают файлы в ~/.lakecode/sessions/<id>/workspace; session.json/pending.json не меняются, перезапись PNG запрещена, input_paths вне папки отклоняются.
  4. bash, eval, edit_file, delegate, load_skill в Research недоступны; /implement неактивен.
  5. /build возвращает инструменты репозитория; созданные в Research файлы остаются на диске.
  6. Открыть сессию после перезапуска: режим последнего хода восстановлен, очередь Research-запросов сохранена и остаётся на паузе. Старые Build/Plan-сессии открываются без изменений.

Классификатор разрешений Jev

Подключение TypeSafe создаётся через /providers: протокол TypeSafe · System One, Base URL https://api.typesafe.ai/v1, API-ключ и вкладка «Модели». Discovery использует GET {BaseUrl}/models и читает models[].name; алиасы видны, но не назначаются классификатору. Фиксированный ID, например jev-1.13.0, можно добавить вручную. Ручные ID хранятся в ModelOverrides с Manual: true, без чат-характеристик и reasoning. Чат-модели и модели классификации не взаимозаменяемы.

В /models → Прочее секция Классификатор содержит включение, пару провайдер/модель, таймаут и пороги; она свёрнута, а заголовок показывает режим и выбранный ID. Одновременно раскрыта не более одной секции. По умолчанию выключен, модель не назначена, таймаут 20 секунд. Отключение сохраняет выбор модели. Ключ и URL хранятся только в providers.json; назначение и политика — в settings.json (Classifier). Отдельного конфига классификатора, импорта и чтения старых настроек нет. Сохранение не выполняет пробных API-вызовов. Снимок настроек и подключения фиксируется при принятии сообщения и наследуется фоновым продолжением; ключ не попадает в очередь и сессию. После перезапуска очередь остаётся на паузе и использует текущие настройки. Удалённое/несовместимое подключение отображается как недоступное: другая модель не подставляется, включённый классификатор не отключается; при вызове инструмента причина недоступности выводится в ручном подтверждении согласно общей политике ошибок оценки.

В Plan общий перехватчик bash/eval вызывает POST {BaseUrl}/systemone выбранного подключения. Передаются только точные аргументы, описание, сведения о среде и фиксированная семантика инструментов. История, задача, AGENTS.md и дополнительные файлы не отправляются. Включение разрешает платную передачу этого кода выбранному провайдеру System One; запрос классификации не считается эффектом проверяемой команды. Локальная эвристика обнаружения credentials и проверка известных ключей переводят подозрительный вызов на ручное решение без отправки. Это не полный детектор секретов; не помещайте ключи в код.

Критерий Решение при риске
Несоответствие описания действию Запрет
Локальные изменения, включая временные файлы Ручное подтверждение
Внешние изменения Ручное подтверждение
Балансы, платежи, резервы, подписки, платные квоты Ручное подтверждение
Отправка секретов или непубличных данных наружу Запрет
Недостаточно понятное действие Ручное решение

Jev возвращает бинарный Choice (safe, risk) по каждому критерию, без категории unknown. Для показателей используется правило: «нет риска» при safe >= AllowThreshold, иначе «есть риск». Недостаточная понятность действия остаётся отдельным бинарным риском. Код LakeCode применяет приоритет запрет → ручное решение → авторазрешение. Начальные пороги: risk >= 0.95 по описанию или передаче данных запрещает вызов; авторазрешение требует safe >= 0.8 по всем шести критериям; остальные случаи требуют подтверждения. Это вероятности категорий, не поле confidence. Порог авторазрешения 0.8 выбран пользователем для уменьшения числа подтверждений. Это повышает риск ошибочного разрешения относительно 0.995; пороги не проверены на локальном наборе вызовов и не гарантируют отсутствие ошибок. Соблюдение задачи не оценивается Jev; существующие ограничения инструментов и доступа сохраняются.

Неизвестные скрипты и состояние eval не подгружаются: требуется ручное решение при недостатке сведений. Таймаут, HTTP-ошибка, неправильная версия модели или некорректный ответ → видимая причина и ручное подтверждение. Повторов запросов и скрытого обрезания нет. Для ограничения расходов и размера контекста вызовы длиннее 48 000 символов отправляются на ручное решение, ответы ограничены 64 KiB. Классификация не является sandbox и не доказывает отсутствие побочных эффектов.

Кнопки «Отклонить» / «Разрешить один раз» встроены в соответствующую карточку bash/eval по CallId; отдельной карточки с повтором команды нет. Начальный фокус — «Отклонить». До решения вывод скрыт, таймер Bash показывается только после появления данных исполнения. Секция «Оценки Jev» раскрывает бинарные показатели, реальные вероятности safe/risk, пороги и модель; отметки рассчитываются по порогу, а не только по наиболее вероятной категории. При запрете политикой кнопок разрешения нет. Отмена и завершение снимают возможность решения. Новая политика — jev-plan-v2-binary; старые оценки отображаются как исторические и не переинтерпретируются. История хранит оценки, версию модели/политики, пороги и ручное решение, без ключа и дополнительной копии запроса. Старые карточки без оценки поддерживаются. Stop не позволяет запустить действие после завершения отменённой оценки.

Ручная проверка Jev (не использовать реальные платежи и секреты):

  1. Добавить TypeSafe-подключение через /providers и добавить фиксированный ID, назначить и включить в /models → «Прочее» → «Классификатор». Новый Plan-запрос с понятным чтением должен пройти автоматически при достижении порогов. При выключенном классификаторе и в Build — прежнее поведение.
  2. Честно описанная запись, внешнее изменение или платный запрос — подтверждение; отклонить, не исполняя. Описание, скрывающее запись, или передача непубличных данных — запрет при пороге.
  3. Непросмотренный скрипт / неизвестная переменная eval — ручное решение.
  4. Неверный ключ, таймаут, Stop во время оценки и вложенный bash — проверить причину, отмену и независимость разрешений; ожидание вложенного решения не расходует timeout eval.
  5. Изменить настройки при очереди и во время хода: уже принятые сообщения сохраняют снимок.
  6. Открыть сохранённую историю: результаты классификации и ручные решения остаются в карточках. Раскрыть «Оценки Jev»: только два показателя вероятности, без выдуманных значений. Проверить кнопки внутри bash/eval, фокус на отказе, Tab/Enter, отказ и Stop без запуска; до решения нет таймера выполнения и сообщения о непоступившем выводе.
  7. В /providers проверить discovery, ручной ID и отсутствие чат-полей у System One. В /models чат-роли не разрешают назначить Jev без чат-характеристик (сырой ID может быть виден в discovery шлюза), классификатор не предлагает чат-модели или алиасы.
  8. Удалить назначенный провайдер: выбор остаётся недоступным, модель не подменяется. Отключить классификатор и сохранить: выбранная пара и пороги сохраняются, ручные подтверждения работают.

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

Приложение и тесты агентом не запускаются. При выключенном классификаторе после сборки проверить вручную:

  1. В Build вызвать bash/eval: описание видно, дополнительного разрешения нет. Пустое description отклоняется до исполнения. Старые карточки без description продолжают открываться.
  2. Выбрать Plan и попросить исправление: агент исследует и планирует; прямые файловые записи недоступны.
  3. Запросить разрешённую проектом диагностику. До подтверждения процесс/код не запускается; отказ возвращает «не выполнено». Повторный вызов снова спрашивает. Описание и точные аргументы доступны.
  4. В Plan вызвать bash через eval: нужны два разрешения, ожидание второго дольше timeout не уничтожает worker. Вызов write_file/edit_file через bridge блокируется. Stop во время ожидания исключает запуск.
  5. Подготовить план через submit_plan: виден Markdown, после завершения/сохранения доступна реализация. Нажать её дважды: принимается один Build-запрос, инструменты возвращаются, план передан целиком.
  6. Переключать режим во время выполнения и при очереди: принятые запросы сохраняют свой режим; фоновые продолжения Plan тоже требуют подтверждений. Вход в Plan сбрасывает прежние eval-переменные.
  7. Открыть новую и старую сессию, восстановить очередь, сделать rewind до/внутри планирующего хода: режимы сохраняются, устаревшее разрешение не используется, частичный план нельзя запустить кнопкой.

Провайдеры

Providers/ProviderRegistry.cs владеет неизменяемыми профилями, ProviderStorage сохраняет их в ~/.lakecode/providers.json независимо от рабочего каталога. Запись атомарная; на Linux каталог создаётся с правами 0700, файл — 0600. Ключи хранятся без шифрования. Повреждённые настройки не сбрасываются автоматически; пути через символические ссылки отклоняются. При отсутствии файла список подключений пуст. Встроенных подключений и чтения ключей из переменных окружения нет: готовые подключения добавляются через /providers, кастомные — только правкой providers.json. Исторический ProviderId версий сессий 1/2 сохранён только для чтения старых файлов и не создаёт подключение.

/providers добавляет одно подключение для каждого зарегистрированного провайдера и позволяет изменить название, учётные данные и модели; /models назначает модели ролям. ProviderProfile хранит ID определения, ID и revision подключения, имя, данные авторизации и пользовательские правила моделей. Протокол, адреса и базовые модели принадлежат определению Provider; дополнительные провайдеры и их особенности добавляются разработчиком в коде. Сохранение публикует новую revision после атомарной записи; принятые запросы продолжают использовать свой снимок.

На вкладке «Модели» кнопка «Обновить список» запускает discovery выбранного провайдера. OpenAI-совместимый транспорт использует Models API, Anthropic — пагинированный список, TypeSafe — список System One. OpenRouter дополнительно загружает отдельный список моделей решений и сохраняет их канонические ID. Запросы выполняются вне UI-потока, без HTTP-редиректов и пробной генерации; ошибка оставляет предыдущий список и черновик.

ModelResolver разрешает характеристики чат-модели из каталога выбранного провайдера, затем применяет пользовательское полное описание или overrides. Явный CatalogModelId позволяет сопоставить модель с другой записью каталога; неизвестные discovery-ID получают общие параметры и могут быть переопределены. Доступность ID в каталоге или discovery не гарантирует доступность на сервере. Модели решений хранятся отдельно от чатовых и назначаются классификатору через общий контракт DecisionModel.

ProviderCatalog собирает определения из сгенерированного каталога models.dev и ручные реализации в Providers/Custom. Все определения наследуют Provider; по умолчанию используются API-ключ и общий транспорт. Ручная реализация может изменить авторизацию, discovery, поддерживаемые виды моделей и настройки запроса. OpenCode добавляет x-opencode-session; TypeSafe и OpenRouter предоставляют модели решений. Сгенерированные определения с неподдерживаемыми транспортами и шаблонными адресами не включаются.

У TypeSafe встроены фиксированные версии Jev 1.13 и 1.14; дополнительные ID можно добавить вручную. OpenRouter получает decision-модели из своего отдельного каталога и использует их канонические ID. Дополнительный ID модели решений можно добавить в редакторе провайдера. Классификатор использует общий DecisionModel, а System One и OpenRouter применяют общий клиент типизированных решений.

NeuralDeep и WikiVibe подключаются по API-ключу через OpenAI-совместимый Chat Completions API. Их модели загружаются кнопкой «Обновить список» с /v1/models соответствующего сервиса; встроенный список для них не задан. Если сервер не выдаёт характеристики модели, их можно уточнить в настройках модели.

OpenAI поддерживает API key и импорт авторизации Codex CLI из $CODEX_HOME/auth.json (по умолчанию ~/.codex/auth.json). Токены сохраняются только в настройках LakeCode; импорт из системного keyring пока недоступен. После импорта используется отдельный backend Codex и запрашивается его каталог с заданной версией совместимости клиента. Доступны только модели из этого списка; неизвестные характеристики нужно описать в редакторе. Модели и кэш Codex CLI не читаются. При обновлении access token LakeCode сохраняет обновлённые данные атомарно; переключение входа не меняет уже принятые запросы. Codex CLI и LakeCode сначала разделяют импортированный refresh token: обновление одним приложением может потребовать повторного входа в другом.

Ручная проверка импорта: выбрать OpenAI → Codex CLI, импортировать и сохранить подключение. Проверить адрес backend и полученный список в /models, отправить сообщение и убедиться, что после перезапуска подключение восстанавливается. Затем переключить вход на API key и обратно, проверить разные каталоги, ошибку при отсутствии auth.json и повторный импорт после истечения или отзыва токена. Сохранение без доступного каталога оставляет подключение настроенным и показывает причину; список можно обновить позже в /providers.

В /providers кнопка «Добавить» открывает каталог ещё не подключённых провайдеров. Редактор профиля содержит вкладки подключения и моделей; пользователь может добавить ID модели и переопределить её характеристики. Отмена сохраняет действующие настройки, удаление требует подтверждения.

Каталог чат-моделей генерируется на сборке: LakeCode.Catalog.Generator скачивает https://models.dev/api.json условным GET с ETag, пишет obj/.../ModelsDevCatalog.g.cs и добавляет его в Compile. Сгенерированный файл не коммитится. -p:ModelsDevCatalogRefresh=false использует кэш без сети; design-time build сеть не трогает. Фильтр — tool-capable модели с текстовым выводом; ID генерации изображений остаются явными алиасами.

providers.json использует SchemaVersion 3. Версии 1 и 2 читаются с преобразованием в памяти, если старый адрес и протокол точно соответствуют зарегистрированному провайдеру; неподдерживаемое кастомное подключение сообщает об ошибке и исходный файл не изменяется. Настройки записываются в новой схеме только при сохранении. Discovery хранится отдельно в ~/.lakecode/model-cache/{ProviderId}/{Revision}.json и привязан к определению и данным подключения хешем; прежний кэш после миграции может быть загружен заново.

Лимиты и возможности каталога/полного описания должны соответствовать документации конкретной модели и шлюза. OpenAI-совместимый протокол допускает reasoning none,minimal,low,medium,high,xhigh,max; конкретная модель ограничивает этот набор через ReasoningEfforts. Anthropic: none или явный budget:N, где 1024 ≤ N < max output. Для Chat Completions none означает отсутствие запроса reasoning effort. Генерация заголовка использует то же подключение и изолированную сессию без инструментов.

Кэширование промпта включается по-разному для каждого протокола. OpenAI-совместимый транспорт передаёт prompt_cache_key с ID сессии в Responses API. Anthropic-транспорт ставит top-level cache_control: {"type":"ephemeral"} — автоматический кэш на 5 минут; поле отправляется только для первопартийного anthropic, поскольку сторонние Anthropic-совместимые эндпоинты могут отвергнуть неизвестное поле. Запросы заголовка сессии кэш не включают: они одноразовые и не переиспользуют префикс. Явные block-level breakpoints и TTL 1h не используются.

Композиция агентской сессии сохраняет общий MAF pipeline, локальную историю, инструменты и разрешения чтения при смене транспорта между запросами. AgentRunPreparer настраивает подключение, а AgentHistory нормализует архив. При смене подключения, revision, API mode или модели история нормализуется: текст, оригинальные изображения, вызовы и результаты инструментов сохраняются, публичный reasoning превращается в обычный текст с пометкой, закрытые provider-specific блоки не переносятся. Число сообщений не меняется, чтобы сохранить границы rewind. Внутреннее состояние reasoning и cache hit между провайдерами не гарантируются. Если история содержит изображения, переход на модель без image input блокируется.

Принятая очередь держит снимок подключения только в памяти. В pending.json и session.json ключи не записываются; после перезапуска подключение разрешается по ProviderId из текущих настроек. Восстановленная очередь не запускается автоматически; отсутствующий профиль или модель оставляют её на паузе с ошибкой. Историю можно открыть даже после удаления подключения и продолжить через другую пару провайдер/модель.

Примеры overrides

Фрагменты внутри профиля в providers.json (API ID — ключ словаря):

"ModelOverrides": {
  "company/gpt-6.1-sol": {
    "CatalogModelId": "gpt-6.1-sol",
    "Overrides": {
      "MaxOutputTokens": 16000,
      "ReasoningEfforts": ["low", "medium", "high"]
    }
  },
  "internal/research-model": {
    "Manual": true,
    "Definition": {
      "Name": "Internal Research",
      "ContextWindowTokens": 128000,
      "MaxInputTokens": 112000,
      "MaxOutputTokens": 16000,
      "InputModalities": ["Text"],
      "OutputModalities": ["Text"],
      "ReasoningEfforts": ["none"]
    }
  }
}

Характеристики второй записи — пример конфигурации, не утверждение о реальной модели. У первой записи характеристики берутся из models.dev; discovery не обязателен для выбора, но сервер всё равно должен обслуживать API ID. Manual разрешает ID вне последнего списка провайдера.

Обновление списка и ручной выбор в /models

ChatModelPicker используется четырьмя чат-ролями. Верхняя строка — фильтр «Все подключения» и поиск, ниже список «подключение · модель», ниже строка reasoning с одним выбором. Строка списка помечена глифом: ● можно назначить, ? ID вне последнего списка, ✕ нет чат-характеристик. Пояснение показывается одной строкой только для выделенной строки, поэтому служебный текст не повторяется у каждой модели. Обновление не выбирает первую строку вместо текущей модели; исчезнувшее назначение остаётся видимым. ModelStore.Refresh больше не меняет подключение или модель автоматически, если прежний выбор недоступен.

F5 (и кнопка «Обновить» в диалоге ручного ID) вызывает ProviderRegistry.RefreshDiscoveryAsync: сериализует операцию с записями профилей, получает ID, атомарно сохраняет только кэш текущей revision и затем публикует новый неизменяемый профиль. Ключ, URL, overrides, revision и назначения не меняются. Ошибка сети или записи оставляет прежний список. Редактор /providers по-прежнему загружает discovery в черновик, который сохраняется только вместе с подключением. Автозагрузки при старте и пробных генераций нет.

«Использовать ID вручную» — отдельное явное сохранение Manual: true в overrides выбранного подключения. Кнопка доступна только для чат-модели с разрешёнными характеристиками; для неизвестной нужно сначала настроить сопоставление или полное описание в /providers. Остальные overrides сохраняются. Операция меняет revision, но не назначения; уже принятые сообщения сохраняют старый снимок. Новое назначение роли применяется только сохранением окна («Сохранить» или Enter в списке).

Загрузка списка и разрешение ручного ID сохраняются сразу: последующая «Отмена» окна отменяет черновики назначений, но не эти завершённые действия. Это указано рядом с кнопками. Неизменённое недоступное назначение не препятствует редактированию другой чат-роли; при попытке использовать его сообщается конкретная причина, а другая модель не подставляется.

Ручная проверка после сборки:

  1. Подключение без кэша: открыть /models, выбрать его, загрузить список. Убедиться, что IDs и дата появились, а ключ, overrides, revision и выбранные роли не изменились. Перезапустить и проверить список.
  2. На вкладке изображений найти gpt-image в поиске image ID; выбрать объявленный ID либо ввести свой. Сбросить поиск, обновить список: введённый image ID и carrier не меняются, даже если их нет в новом ответе.
  3. Известный чат-ID без discovery: ввести его, нажать «Использовать ID вручную», применить назначение. Неизвестный ID не должен получать выдуманные характеристики. Ключ не должен появляться в сообщениях.
  4. HTTP-ошибка, timeout и недоступная запись кэша: прежний список, дата и введённые ID остаются. Повторная загрузка выполняется только по кнопке. Проверить отсутствие фонового запроса при открытии окна.
  5. Обновить список, из которого исчезла выбранная модель: она не заменяется первой доступной. Проверить явное ручное разрешение и сохранение остальных ролей с неизменённым недоступным назначением.
  6. Изменить список/ручной допуск при принятой очереди и активном ходе: их снимки не меняются. Проверить Tab, стрелки, поиск, длинные ID, пустой ответ, Esc при открытом dropdown и сохранность черновиков после ошибки. «Отмена» не откатывает отдельно завершённую загрузку или ручное разрешение.

Ручная проверка провайдеров и discovery

  1. Добавить подключение из /providers, проверить отмену, сохранение и запрет второго подключения того же провайдера.

  2. Обновить список моделей OpenAI и Anthropic, добавить ручную модель, изменить характеристики и проверить сохранение после перезапуска.

  3. Проверить OpenCode: запросы одной сессии используют стабильный x-opencode-session.

  4. Добавить TypeSafe, выбрать фиксированную версию Jev; для OpenRouter обновить каталог и выбрать модель решений в /models.

  5. Проверить ошибки сети, некорректный ответ и отмену: черновик и предыдущий список сохраняются; секреты не показываются.

  6. Проверить старые настройки: совместимые подключения читаются; неподдерживаемый адрес сообщает об ошибке без изменения файла. Принятые запросы сохраняют свой снимок.

  7. Без providers.json открыть /providers: список пуст, встроенное подключение/ключ из окружения не появляются. Нажать «Добавить»: виден поиск по готовым пресетам; выбрать провайдера, ввести ключ, отменить — записи не появляется. Кастомный профиль появляется только после правки providers.json.

  8. Для OpenAI получить список через /models, для Anthropic — список с несколькими страницами. Точное совпадение с каталогом становится доступным в выборе модели, неизвестный ID виден как требующий настройки.

  9. Сопоставить API ID шлюза с каталогом, оставить часть полей пустыми, изменить max output и reasoning. После сохранения и перезапуска проверить наследование и фактический API ID в запросе.

  10. Полностью описать неизвестную модель; удалить обязательное поле — сохранение показывает ошибку на её вкладке/строке, введённые значения остаются. Исправить и сохранить.

  11. Ввести неполную/некорректную настройку, перейти на другую модель и обратно — черновик не теряется. «Сбросить override» возвращает наследование.

  12. Проверить 401/404, редирект, таймаут и неправильный JSON Models API: список и overrides остаются прежними, ошибки не содержат ключ/тело ответа. Добавить ручной API ID для шлюза без discovery.

  13. Обновить список, где ранее доступный ID исчез: override остаётся, но ID больше не выбирается для работы без явного ручного режима. Принятое ранее сообщение сохраняет свой снимок.

  14. Изменить ключ в UI; URL/протокол изменить только в providers.json и открыть заново. Старый discovery не переносится; после обновления получаются модели нового подключения. При ручной правке конфига проверить инвалидирование кэша.

  15. Открыть старый providers.json версии 1, проверить модели и возможности, сохранить — версия 2 с полными ручными определениями. Недоступный файл настроек/кэша при записи оставляет действующий профиль прежним.

  16. Удалить провайдера из списка: подтверждение, возвращение в список, история остаётся. Восстановленная очередь без соответствующего подключения остаётся на паузе.

Модель планирования

На вкладке «План» можно отдельно выбрать модель и reasoning. Назначение сохраняется для всех проектов. «Как у основного агента» снимает отдельный выбор: следующие Plan-запросы используют текущую основную модель и её reasoning, включая последующие изменения основного выбора. Build и реализация готового плана через /implement или кнопку используют основную модель, а не PlanModel. Shift+Tab и нижняя строка по-прежнему относятся к основному выбору; reasoning отдельного Plan-назначения меняется через /models.

CLI разрешает PlanModel до подготовки файлов и передаёт фактические модель, провайдер и reasoning через существующие поля SubmitMessage. Лимит файлов и поддержка изображений проверяются по фактической модели. Недоступное явное назначение вызывает ошибку с сохранением черновика, без скрытого перехода на основную модель. Очередь сохраняет фактические ID и reasoning запроса; изменение PlanModel не переназначает принятые сообщения. Назначение сабагентов независимо: без него они наследуют фактическую модель родительского запроса, в том числе Plan. Фоновые продолжения используют конфигурацию последнего запроса.

Ручная проверка планирования:

  1. Открыть старый settings.json без PlanModel: вкладка наследует основной выбор, назначение сабагентов сохранено.
  2. Выбрать отдельные модель, провайдер и reasoning, применить и перезапустить: назначение сохранено, Plan использует его, Build — основную модель.
  3. Вернуть «Как у основного агента», применить и изменить основную модель/reasoning: следующие Plan-запросы наследуют новый выбор, поле reasoning на вкладке планирования отключено.
  4. Изменить три вкладки и переключать их: черновики сохраняются; Отмена/Esc ничего не применяет. При открытом dropdown первый Esc закрывает только список. При ошибке сохранения настройки не меняются, черновики остаются в окне.
  5. Удалить назначенное подключение/модель или сделать reasoning неподдерживаемым: отправка Plan показывает ошибку, черновик сохранён. Выбор наследования восстанавливает отправку.
  6. Отправить Plan с файлами и изображениями: ограничения проверяются по Plan-модели; несовместимые вложения не принимаются, черновик остаётся.
  7. Поставить Plan в очередь и изменить назначение/режим, в том числе во время подготовки файлов: запрос сохраняет захваченные модель, reasoning и режим. После восстановления очереди используются сохранённые ID, а не новое назначение.
  8. Получить готовый план и запустить /implement или кнопку: создаётся Build-запрос на основной модели.
  9. Делегировать из Plan без отдельного назначения сабагентов и с ним: сначала наследуется Plan-модель, затем используется явное назначение сабагентов. Фоновые продолжения сохраняют конфигурацию родительского запроса.

Назначение моделей планирования и сабагентов

/models содержит вкладки «Основной», «План», «Сабагенты», «Сжатие» и «Прочее» (стандартный TabControl). Вкладки переключаются мышью или ← → при фокусе на заголовках; Tab переводит фокус между контролами. Стрелки ↑ ↓ выбирают модель без дополнительного подтверждения, reasoning выбирается строкой радиокнопок. Для планирования, сабагентов и сжатия доступны все пары провайдер/модель и вариант «Как у основного агента» с отключённой строкой reasoning. У каждой вкладки свой черновик, сохраняющийся при переключении. Enter в списке моделей и общая кнопка «Сохранить» выполняют одну и ту же запись назначений и закрывают окно; «Отмена» или Esc откатывают черновики (при открытом dropdown первый Esc закрывает список, при раскрытой секции — сворачивает её). Отдельно завершённые загрузки discovery и разрешения ручного ID не откатываются. При ошибке сохранения окно остаётся открытым с черновиками, действующие настройки не меняются.

Settings/AgentSettingsStorage.cs хранит глобальные настройки агента в ~/.lakecode/settings.json, отдельно от providers.json. Сейчас AgentSettings содержит версию схемы и необязательные PlanModel и SubagentModel (ProviderId, ModelId, ReasoningEffort); null означает наследование. Старые файлы без PlanModel читаются без изменения версии схемы и наследуют основной выбор для Plan. Файл читается до создания UI, сохраняется вне UI-потока атомарной заменой с проверкой отмены до commit, на Linux новый файл имеет права 0600, существующие права сохраняются. Символические ссылки запрещены, повреждение и неподдерживаемая версия не сбрасываются автоматически. Отсутствующий файл означает наследование; фактическое назначение сохраняется при явном сохранении из окна. Выбор основной модели между запусками по-прежнему не сохраняется.

SubmitMessage передаёт назначение в Core; ConversationService разрешает и фиксирует отдельный снимок подключения, модели и reasoning при принятии сообщения. Смена настройки не меняет принятую очередь, активный запрос и созданные дочерние задачи. Фоновые продолжения сохраняют назначение последнего основного запроса. В очередь записываются только идентификаторы назначения и reasoning, снимок подключения с ключом исключён из JSON. После перезапуска очередь разрешает назначение через текущий registry; отсутствующее подключение/модель или неподдерживаемый reasoning оставляют её на паузе с ошибкой, без скрытого перехода на основную модель. Старые записи без назначения наследуют основной запрос. Снимок дочерней задачи содержит безопасные имена/ID провайдера и модели и reasoning; окно «Сабагенты» показывает фактическую конфигурацию задачи, включая исторические карточки новых сессий.

Ручная проверка назначения:

  1. В /models открыть вкладку «Сабагенты», выбрать другую пару провайдер/модель стрелками, выбрать reasoning радиокнопками и нажать «Сохранить» (или Enter в списке); после перезапуска назначение сабагентов должно сохраниться. До сохранения переключить вкладки и проверить сохранность черновиков. Изменить все три вкладки и нажать «Отмена»/Esc — действующие настройки не должны измениться.
  2. Отправить запрос с делегированием: основная модель не меняется, окно «Сабагенты» показывает назначенную модель и reasoning. Проверить также другой провайдер.
  3. Поставить сообщение в очередь, сменить назначение, затем выполнить очередь: сообщение использует прежние настройки. Уже запущенный ребёнок и его продолжение после вопроса родителю тоже сохраняют исходную конфигурацию.
  4. Вернуть «Как у основного агента» и сохранить; следующие отправки наследуют основную модель и reasoning. Изменение черновика назначения с последующим Esc не должно сохраняться.
  5. Удалить назначенную модель или подключение: новая отправка должна показать ошибку с сохранением черновика, а восстановленная очередь — остаться на паузе. Явно переназначить модель через /models для новых отправок.
  6. Проверить ошибку доступа при сохранении settings.json: окно сохраняет черновик настройки, действующее назначение не меняется.

Фоновые read-only сабагенты

Agent/Subagents/Delegation/DelegationCoordinator.cs владеет исследовательскими задачами на время основной агентской сессии; его ресурсы принадлежат AgentComponents, а операциями управляет ConversationService. Новых зависимостей и универсального event bus нет: переиспользуются MAF-сессии, обычные .NET Tasks, WorkspaceFiles и контракты событий ядра. Дочерние потоки читаются независимо; основной экземпляр AgentLoop по-прежнему вызывается последовательно.

delegate(task, context) теперь всегда запускает задачу в фоне и сразу возвращает JSON Running с taskId. Промпт основного агента рекомендует самостоятельно делегировать подходящие независимые исследования, продолжая свою непересекающуюся работу; запускать сабагента ради чтения известного файла не нужно. Сабагенты продолжают работу после финального ответа, между пользовательскими сообщениями. Каждый получает отдельную сессию и UUID cache affinity, тот же рабочий корень и инструкции, зафиксированные при создании основной сессии. История разговора не копируется. Подключение, модель и reasoning фиксируются при запуске задачи и сохраняются при продолжениях.

У ребёнка есть только read_file, list_directory, search_text, find_files, outline_file, trace_code и ask_parent: без записи, bash, eval, веба, skills, обращения к пользователю и вложенного делегирования. Общий WorkspaceFiles сохраняет единые разрешения и запреты внешнего чтения. Запросы разрешения и ask_tool сериализуются, чтобы параллельные задачи не открывали конкурирующие интерактивные панели.

Ожидание и доставка

Основному агенту доступен отдельный прямой инструмент:

wait_subagents(taskIds = null, waitAll = false, timeoutSeconds = 300)
  • Без taskIds выбираются работающие задачи и ещё не доставленные результаты/вопросы. Явные IDs позволяют ждать конкретные задачи.
  • По умолчанию ожидание возвращается при первом готовом результате или вопросе, вместе с текущими состояниями остальных выбранных задач.
  • waitAll: true ждёт все выбранные задачи, но вопрос NeedsParentInput всегда завершает ожидание раньше: иначе родитель и исполнитель могли бы ждать друг друга.
  • Таймаут — 1..600 секунд. Он возвращает состояния и не отменяет исследования. Ожидание реализовано через сигнал изменения состояния, без sleep и polling.
  • Результаты и вопросы, полученные через wait, помечаются доставленными и не создают повторного автоматического уведомления. Повторный явный запрос сохранённого ID может вернуть его состояние снова.

Недоставленные завершения и вопросы автоматически запускают отдельный служебный ход основного агента, когда текущий ход и его сохранение закончились. Пользовательская очередь имеет приоритет. Сервис не входит повторно в занятый AgentLoop; уведомления воспринимаются как данные инструмента, а не пользовательские инструкции. Служебный ход использует настройки последнего основного запроса и сохраняется с флагом IsBackgroundContinuation. UI показывает отметку «Уведомление фоновых исследований», а не фиктивное сообщение пользователя с полным JSON. Если очередь остановлена или сохранение/контекст заблокированы, автоматическая доставка ожидает возобновления работы. Автоматические продолжения выполняют дополнительные запросы к модели.

Уточнения, жизненный цикл и UI

ask_parent(question, reason) завершает ход ребёнка через штатный FunctionInvocationContext.Terminate, с полным чтением потока и записью результата инструмента. Задача становится WaitingForParent. Вопрос приходит через wait_subagents или автоматическое уведомление с taskId и questionId. Родитель отвечает из контекста либо через существующий ask_tool; delegate(taskId, questionId, answer) возобновляет ту же дочернюю сессию в фоне. Повторные/устаревшие ответы отклоняются. Итог — Completed, ошибка — Failed, отмена — Cancelled.

Ограничения: до четырёх незавершённых задач (включая ожидающие родителя), до 32 записей задач в памяти; доставленные завершённые записи освобождаются при новых запусках. На задачу — три уточнения и 32 обращения к модели; каждое исполнение между уточнениями ограничено пятью минутами. Поручение, контекст и ответ родителя — до 16 000 символов каждый; результат — до 32 000. Превышение лимита результата не выдаётся за успешный усечённый итог.

Остановка и прерывание основного запроса отменяют фоновые задачи. Stop работает и между сообщениями. /new, переключение сессии, rewind и выход отменяют и дожидаются дочерних операций перед освобождением ресурсов. Завершение обычного ответа, переход к уточнению на границе инструментов и ошибка основного запроса сами по себе не уничтожают фоновые задачи; при ошибке основная очередь остаётся на паузе. Восстановления живых дочерних сессий после перезапуска нет. При открытии истории незавершённые карточки помечаются отменёнными; старые IDs не возобновляются автоматически.

BackgroundToolCallChanged связывает снимки с задачей и вызовом инструмента; номер запроса отличает возобновления после уточнений. Запрос, вопрос, ответ родителя и итог выводятся отдельными свёрнутыми блоками в хронологическом порядке, а не переписывают исходную карточку. Снимки сохраняются в последнем ходе на момент события; актуальные задачи доступны через ConversationState.Delegations. Разрешение на внешнее чтение показывается в блоке, связанном с исходным вызовом. Завершение основного ответа не помечает работающие делегирования прерванными.

Кнопка «Сабагенты» у правого края хедера показывает число работающих задач, ожидания и ошибки; открывает список задач со статусами. Выбор задачи открывает её ленту в том же немодальном окне: используются компоненты основного чата, Markdown, сворачиваемый reasoning и блоки инструментов, без поля ввода и токенового стриминга. DelegationActivityChanged передаёт публичные сообщения после завершения ответа провайдера и структурированные изменения инструментов; границы сообщений сохраняются отдельно. Основной чат продолжает стримиться. Эти события наблюдения не входят в историю основного агента и не сохраняются.

Кнопка «Вернуться в чат» меняет только фокус: окно наблюдателя остаётся поверх основного окна. Для обязательного вопроса или разрешения оно временно уступает передний план, не закрываясь. Закрытие наблюдателя не отменяет работу. Каждая задача хранит собственный прокручиваемый набор компонентов, включая раскрытие блоков; смена задачи не переводит чтение в конец. Автопрокрутка использует то же поведение, что основной чат. Бюджет ленты — 64 000 символов и 128 блоков: удаляются целые старые блоки с явной отметкой, при этом единственный большой блок и незавершённые блоки не обрезаются. Для открытой сохранённой сессии доступны задача, состояние и итог, но не внутренняя лента. Индикатор активности учитывает работающих сабагентов, но их usage не прибавляется к показателям основного запроса.

Сценарии ручной проверки

Приложение и тесты агентом не запускаются. После сборки вручную проверить:

  1. Запустить два независимых исследования: оба delegate сразу возвращают IDs, основной агент может продолжить другую работу.
  2. Завершить ответ основного агента, пока дети работают, и отправить следующее сообщение: карточки продолжают обновляться; недоставленный итог вызывает служебное продолжение без повторного входа в занятую сессию.
  3. Вызвать wait_subagents для одного ID и нескольких IDs с waitAll; уже полученный результат не должен приходить повторно автоматическим уведомлением.
  4. Ребёнок задаёт вопрос при waitAll: ожидание возвращается, родитель отвечает и продолжает ту же задачу; другой ребёнок остаётся работающим.
  5. Вопрос приходит после финального ответа: родитель автоматически получает уведомление, при необходимости показывает ask_tool и возобновляет ребёнка с прежними IDs.
  6. Короткий таймаут wait возвращает Timeout, а исследование продолжает работу. Пустой список работы и неизвестный ID дают понятную ошибку.
  7. Разрешение на внешнюю папку появляется в исходной карточке, в том числе между сообщениями. Одновременно активна только одна панель подтверждения/вопроса; сохранённый запрет не обходится.
  8. Остановить работу во время основного запроса и между сообщениями; затем проверить /new, переключение сессии, rewind и выход: дети отменяются, поздние события не попадают в новую сессию.
  9. Проверить ошибку провайдера и повторный ответ со старым questionId; перезапуск не должен автоматически восстанавливать или повторять исследования.