Перейти к содержимому

Интеллект кода

Control Center даёт агенту четыре способа понимать код помимо его чтения. Они отвечают на по-настоящему разные вопросы, и четыре их не один только потому, что ни один механизм в одиночку не отвечает на все.

Механизм На что отвечает Цена
Граф кода Что здесь есть и что от этого зависит Проход индексации; дёшево запрашивать
Языковой сервер Это вообще компилируется и что это за тип Процесс сервера на каждый проект
Структурный поиск Где встречается эта ФОРМА Разбор на каждый файл
Отладчик Что было истинно в этот момент, в этом кадре Остановленный процесс

Разрыв, который закрывает языковой сервер

Заголовок раздела «Разрыв, который закрывает языковой сервер»

Граф кода строится из tree-sitter: символы, рёбра вызовов, радиус влияния. Он быстрый, он дешёвый и ничего не знает о типах. Он не скажет, компилируется ли только что написанный вами вызов, существует ли это поле у объекта и не пропустило ли ваше переименование реэкспорт.

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

Так что выигрыш не столько в инструменте lsp. Он в том, что write, edit и apply_patch обёрнуты: после каждой записи файл проходит через языковой сервер, и впервые возникшие диагностики подмешиваются в результат инструмента. Агент узнаёт, что сломал сборку, в тот самый момент, когда сломал её, — независимо от того, догадался ли он спросить.

Повторять то, что агенту уже сказали, хуже, чем молчать: это наполняет результат шумом, который агент учится пропускать взглядом, и вместе с ним уходит единственная новая ошибка. Журнал на каждый запуск отслеживает, что уже сообщалось, с ключом по серьёзности, коду и сообщению — не по позиции, поэтому диагностика, просто сдвинувшаяся на четыре строки вниз, не считается новой и не сообщается повторно.

Ни один сервер не запускается при старте. Сервер запускается при первом запросе, которому он нужен, — а это почти всегда первая правка файла, на который он претендует, — и простаивающие серверы вычищаются. Хост, который никогда не касается Dart, никогда не запускает анализатор.

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

grep сопоставляет символы. Это правильный инструмент для имени и неправильный для формы.

Структурный шаблон разбирается той же грамматикой, что и файл, поэтому dispose($X) находит вызов независимо от пробелов, переносов и разбиения по строкам и никогда не сопоставляет те же символы внутри строкового литерала или комментария.

Свойство, которое делает его не просто регулярным выражением с лишними шагами, — повторяющаяся в одном шаблоне метапеременная должна захватить один и тот же текст:

if ($X != null) $X.dispose()

находит проверку, которая освобождает то же, что проверяла, и не находит ту, что освобождает что-то другое. Это целый класс ошибок, который текстом не выразить вообще.

Второго движка нет. cc_natives уже загружает tree-sitter и его грамматики для графа кода, вместе с кэшем скомпилированных запросов и таблицей языков. Сопоставитель — чистый Dart над деревьями, которые и так разбирались. Второй структурный движок означал бы второй парсер, второй набор грамматик, который нужно собирать и ставить на каждой платформе, и два ответа на вопрос «что такое выражение вызова».

Грамматика определяет, что такое целый файл, и только. Разобранный в голом виде на верхнем уровне Dart, dispose(x) — совершенно допустимая сигнатура функции: объявление с именем dispose, принимающее параметр x, — и нигде нет ошибки, которая выдала бы подвох. Сопоставитель, построенный на таком разборе, ищет объявления, не находит вызовов и сообщает «нет совпадений» для шаблона, который никогда не читался так, как выглядит.

Поэтому шаблон пробуется в лестнице каркасов — инструкция, выражение, член класса, верхний уровень — и побеждает первый, который его принимает. Фрагмент, который не принимает ни один контекст, сообщается как неразбираемый, а не ищется и тихо не находится.

Почему перезапись сначала готовит изменение, а не записывает его

Заголовок раздела «Почему перезапись сначала готовит изменение, а не записывает его»

Структурная перезапись — самое широкое изменение, которое может сделать агент: один шаблон, сорок файлов, и никто не читает результат строка за строкой — ни модель, ни человек, который об этом попросил. Записать её и отчитаться «готово» означало бы, что первым честным взглядом на изменение станет git diff после факта.

Поэтому ast_edit сообщает, что он сделал бы, — количество, файлы, найденные места, — и изменение попадает на диск только когда resolve его фиксирует. Именно это также позволяет показать человеку diff и кнопки «Принять/Отклонить», прежде чем что-либо дойдёт до диска.

Фиксация идёт по принципу «всё или ничего», и каждый файл сверяется с содержимым, зафиксированным на момент подготовки. Между подготовкой и фиксацией агент мог править один из этих файлов вручную, проход диагностики мог его перезаписать, а форматтер — его тронуть; фиксация в любом случае отбросила бы эту работу, а diff выглядел бы намеренным. Любое расхождение отклоняет всё изменение целиком, потому что частично применённая структурная перезапись оставляет дерево, которое не компилируется ни в одной из форм.

На что отладчик отвечает так, как не может оператор print

Заголовок раздела «На что отладчик отвечает так, как не может оператор print»

Цикл с оператором print таков: добавить print, запустить всё заново, прочитать вывод, удалить print. Он стоит полного прогона тестов на каждый вопрос и отвечает только на тот вопрос, который вы догадались задать до запуска.

Остановленный кадр отвечает сразу на все вопросы об этом моменте: каждая локальная переменная, каждый вызывающий, выражение, вычисленное в собственной области видимости кадра. Для зависшего процесса или ошибки, воспроизводящейся один раз из десяти запусков, это разница между половиной дня и минутой.

Ограничен, как огороженная площадка, и по той же причине

Заголовок раздела «Ограничен, как огороженная площадка, и по той же причине»

Отладочный адаптер владеет остановленным процессом, который держит всё, что держит этот процесс: порт, блокировку, соединение с базой данных. Сессия, которой никто не управляет, — это утечка, живущая дольше хода, который её запустил. Поэтому здесь жёсткий TTL, ровно одна сессия на разговор, а второй launch отклоняется, вместо того чтобы молча заменить первый: агент, запускающий вторую, не завершив первую, почти наверняка потерял первую из виду.

Debug Adapter Protocol разделяет с Language Server Protocol кадрирование по Content-Length и ничего выше него: другие ключи корреляции, другая форма ошибок и третий тип сообщений (события), у которого нет аналога в LSP. Кадрирование живёт в одном месте; протоколы лежат на нём отдельно.

Написать кадрирование дважды означало бы два ответа на вопросы «сообщение занимает два фрагмента», «во фрагменте три сообщения» и «сервер написал баннер в stdout до своего первого кадра», и исправление получил бы только один из них.