Интеллект кода
Control Center даёт агенту четыре способа понимать код помимо его чтения. Они отвечают на по-настоящему разные вопросы, и четыре их не один только потому, что ни один механизм в одиночку не отвечает на все.
| Механизм | На что отвечает | Цена |
|---|---|---|
| Граф кода | Что здесь есть и что от этого зависит | Проход индексации; дёшево запрашивать |
| Языковой сервер | Это вообще компилируется и что это за тип | Процесс сервера на каждый проект |
| Структурный поиск | Где встречается эта ФОРМА | Разбор на каждый файл |
| Отладчик | Что было истинно в этот момент, в этом кадре | Остановленный процесс |
Разрыв, который закрывает языковой сервер
Заголовок раздела «Разрыв, который закрывает языковой сервер»Граф кода строится из tree-sitter: символы, рёбра вызовов, радиус влияния. Он быстрый, он дешёвый и ничего не знает о типах. Он не скажет, компилируется ли только что написанный вами вызов, существует ли это поле у объекта и не пропустило ли ваше переименование реэкспорт.
Без языкового сервера агент узнаёт об этом во время тестов — или не узнаёт никогда. Он пишет что-то правдоподобное, идёт дальше, и ошибка всплывает три хода спустя, привязанная к другому изменению.
Так что выигрыш не столько в инструменте lsp. Он в том, что write, edit и apply_patch обёрнуты: после каждой записи файл проходит через языковой сервер, и впервые возникшие диагностики подмешиваются в результат инструмента. Агент узнаёт, что сломал сборку, в тот самый момент, когда сломал её, — независимо от того, догадался ли он спросить.
Только новые диагностики
Заголовок раздела «Только новые диагностики»Повторять то, что агенту уже сказали, хуже, чем молчать: это наполняет результат шумом, который агент учится пропускать взглядом, и вместе с ним уходит единственная новая ошибка. Журнал на каждый запуск отслеживает, что уже сообщалось, с ключом по серьёзности, коду и сообщению — не по позиции, поэтому диагностика, просто сдвинувшаяся на четыре строки вниз, не считается новой и не сообщается повторно.
Ленивый — и честно говорит, что его нет
Заголовок раздела «Ленивый — и честно говорит, что его нет»Ни один сервер не запускается при старте. Сервер запускается при первом запросе, которому он нужен, — а это почти всегда первая правка файла, на который он претендует, — и простаивающие серверы вычищаются. Хост, который никогда не касается Dart, никогда не запускает анализатор.
Обнаружение — это пересечение: в рабочей копии должен существовать корневой маркер и должен резолвиться бинарник. Один маркер говорит «этот проект на Python» и ничего — об установленном сервере; один резолвящийся бинарник говорит, что на хосте есть тулчейн, и ничего — об этой рабочей копии. Запрос может завершиться успешно, только когда есть и то и другое, а инструмент, который предлагается и всегда падает, хуже отсутствующего.
Зачем структурный поиск, если есть grep
Заголовок раздела «Зачем структурный поиск, если есть grep»grep сопоставляет символы. Это правильный инструмент для имени и неправильный для формы.
Структурный шаблон разбирается той же грамматикой, что и файл, поэтому dispose($X) находит вызов независимо от пробелов, переносов и разбиения по строкам и никогда не сопоставляет те же символы внутри строкового литерала или комментария.
Свойство, которое делает его не просто регулярным выражением с лишними шагами, — повторяющаяся в одном шаблоне метапеременная должна захватить один и тот же текст:
if ($X != null) $X.dispose()находит проверку, которая освобождает то же, что проверяла, и не находит ту, что освобождает что-то другое. Это целый класс ошибок, который текстом не выразить вообще.
Он работает на уже подключённом tree-sitter
Заголовок раздела «Он работает на уже подключённом tree-sitter»Второго движка нет. 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 до своего первого кадра», и исправление получил бы только один из них.
См. также
Заголовок раздела «См. также»- Искать код через граф кода — о символах и влиянии
- Отладить падающий тест — отладчик от начала до конца
- Отрефакторить код структурно —
ast_grepиast_edit - Встроенные инструменты агента — каждая операция и аргумент