Перейти до вмісту

Інтелект коду

Control Center дає агенту чотири способи зрозуміти код, окрім простого читання. Вони відповідають на справді різні питання, і чотири з них, а не один, існують тому, що жоден окремий механізм не відповідає на всі.

Механізм Відповідає на Витрати
Граф коду Що тут є і що від цього залежить Прохід індексування; дешево запитувати
Мовний сервер Чи це справді компілюється і що це за тип Процес сервера на кожен проєкт
Структурний пошук Де з’являється ця ФОРМА Розбір на кожен файл
Налагоджувач Що було істинним у цю мить, у цьому кадрі Зупинений процес

Граф коду побудований на tree-sitter: символи, ребра викликів, радіус впливу. Він швидкий, він дешевий і не знає нічого про типи. Він не скаже, чи компілюється виклик, який ви щойно написали, чи існує те поле в об’єкті й чи не пропустило ваше перейменування якийсь реекспорт.

Без мовного сервера агент дізнається про це під час тестів — або ніколи. Він пише щось правдоподібне, іде далі, а збій виринає три ходи потому, причеплений до іншої зміни.

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

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

Жоден сервер не запускається під час старту. Він запускається на перший запит, який його потребує, а це майже завжди перше редагування файлу, який він обслуговує, а простоювальні сервери прибираються. Хост, який ніколи не торкається Dart, ніколи не запускає аналізатор.

Виявлення — це перетин: у чекауті має існувати кореневий маркер і має резолвитися бінарник. Сам маркер каже «цей проєкт на Python» і нічого не каже про те, чи встановлено сервер; сам резолвний бінарник каже, що на хості є тулчейн, і нічого — про цей чекаут. Лише обидва разом означають, що запит може вдатися, а інструмент, який пропонують і який завжди падає, гірший за відсутній.

grep зіставляє символи. Це правильний інструмент для імені й неправильний для форми.

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

Властивість, яка робить його більшим, ніж regex із зайвими кроками, — це те, що метазмінна, повторена в одному шаблоні, має захопити той самий текст:

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

зіставляє захисну умову, яка звільняє саме те, що перевірила, і не зіставляє ту, яка звільняє щось інше. Це клас помилок, який текстом не виразити взагалі.

Другого рушія немає. cc_natives уже завантажує tree-sitter і його граматики для графа коду, разом із кешем скомпільованих запитів і таблицею мов. Механізм зіставлення — це чистий Dart над деревами, які й так розбиралися. Другий структурний рушій означав би другий парсер, другий набір граматик, який треба збирати й готувати на кожній платформі, і дві відповіді на питання «що таке вираз виклику».

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

Тож шаблон пробують на драбині каркасів — інструкція, вираз, член класу, верхній рівень — і перемагає перший, який його приймає. Фрагмент, який не приймає жоден контекст, позначають як непридатний до розбору, а не шукають і тихо не знаходять.

Чому структурний перезапис спершу готується, а не записується

Розділ із назвою «Чому структурний перезапис спершу готується, а не записується»

Структурний перезапис — найширша зміна, яку може зробити агент: один шаблон, сорок файлів, і ніхто не читає результат рядок за рядком — ні модель, ні людина, яка його попросила. Записати його й доповісти «готово» означає, що перший чесний погляд на зміну — це git diff після факту.

Тож ast_edit повідомляє, що він зробив би — кількість, файли, знайдені місця — а зміна лягає на диск лише тоді, коли resolve її зафіксує. Це також те, що дозволяє показати диф і кнопки Прийняти/Відкинути людині, перш ніж щось дійде до диска.

Фіксація відбувається за принципом «усе або нічого», і кожен файл звіряють із вмістом, зафіксованим у момент підготовки. Між підготовкою й фіксацією агент міг відредагувати один із цих файлів вручну, прохід діагностики міг його переписати, а форматувальник — торкнутися його; якби фіксація все одно відбулася, цю роботу було б відкинуто, а диф виглядав би навмисним. Будь-яка розбіжність відхиляє всю зміну, бо частково застосований структурний перезапис лишає дерево, яке не компілюється в жодній з форм.

Що налагоджувач відповідає такого, чого не може оператор друку

Розділ із назвою «Що налагоджувач відповідає такого, чого не може оператор друку»

Цикл з оператором друку такий: додати друк, знову запустити все, прочитати вивід, прибрати друк. Він коштує повного прогону тестів на кожне питання й відповідає лише на те питання, яке ви здогадалися поставити до запуску.

Зупинений кадр відповідає одразу на всі питання про цю мить — кожну локальну змінну, кожен виклик і вираз, обчислений у власній області видимості кадру. Для завислого процесу чи збою, який відтворюється раз на десять запусків, це різниця між цілим днем і хвилиною.

Налагоджувальний адаптер володіє зупиненим процесом, який тримає все, що тримає цей процес — порт, блокування, з’єднання з базою даних. Сесія, яку ніхто не веде, — це витік, який переживає хід, що її запустив. Тож є жорсткий TTL, рівно одна сесія на розмову, а другий launch відхиляється, а не тихо замінює перший: агент, який запускає другий, не завершивши перший, майже напевно загубив перший із виду.

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

Якби кадрування писали двічі, було б дві відповіді на питання «повідомлення розтягнуте на два блоки», «в одному блоці три повідомлення» і «сервер написав банер у stdout перед своїм першим кадром», і лише одна з них отримала б виправлення.