Przejdź do głównej zawartości

Inteligencja kodu

Control Center daje agentowi cztery sposoby rozumienia kodu poza jego czytaniem. Odpowiadają one na naprawdę różne pytania, a powód, dla którego są cztery, a nie jeden, jest taki, że żaden pojedynczy mechanizm nie odpowiada na wszystkie.

Mechanizm Odpowiada na Koszty
Graf kodu Co tu jest i co od tego zależy Przebieg indeksowania; tanie zapytania
Serwer językowy Czy to się w ogóle kompiluje i czym jest ten typ Proces serwera na projekt
Wyszukiwanie strukturalne Gdzie występuje ten KSZTAŁT Parsowanie na plik
Debugger Co było prawdą w tym momencie, w tej ramce Zatrzymany proces

Graf kodu jest budowany na podstawie tree-sittera: symbole, krawędzie wywołań, promień wpływu. Jest szybki, jest tani i nie wie nic o typach. Nie powie, czy wywołanie, które właśnie napisano, się kompiluje, czy dane pole istnieje w obiekcie ani czy zmiana nazwy pominęła jakiś reeksport.

Bez serwera językowego agent dowiaduje się o tym na etapie testów — albo nigdy. Pisze coś wiarygodnie wyglądającego, idzie dalej, a błąd wypływa trzy tury później, przypisany do innej zmiany.

Zysk nie polega więc tak naprawdę na narzędziu lsp. Polega na tym, że write, edit i apply_patch są opakowane: po każdym zapisie plik przechodzi przez serwer językowy, a nowo wprowadzone komunikaty diagnostyczne są dołączane do wyniku narzędzia. Agent dowiaduje się, że zepsuł kompilację, w momencie, w którym ją zepsuł — niezależnie od tego, czy wpadł na to, żeby o to zapytać.

Powtarzanie tego, co agent już usłyszał, jest gorsze niż milczenie: wypełnia wynik szumem, który agent uczy się pomijać wzrokiem, a razem z nim ucieka ten jedyny nowy błąd. Rejestr prowadzony dla każdego uruchomienia śledzi, co zostało zgłoszone, z kluczem opartym na wadze, kodzie i komunikacie — nie na pozycji, więc komunikat, który jedynie przesunął się o cztery linie w dół, nie zostaje ponownie zgłoszony jako nowy.

Żaden serwer nie startuje przy uruchomieniu. Serwer startuje przy pierwszym żądaniu, które go potrzebuje — czyli prawie zawsze przy pierwszej edycji pliku, który obsługuje — a bezczynne są zamykane. Host, który nigdy nie dotyka Darta, nigdy nie uruchamia analizatora.

Wykrywanie to część wspólna: znacznik korzenia musi istnieć w kopii roboczej i plik binarny musi dać się odnaleźć. Sam znacznik mówi „to projekt w Pythonie” i nic o tym, czy serwer jest zainstalowany; sam odnajdywalny plik binarny mówi, że host ma zestaw narzędzi, i nic o tej kopii roboczej. Dopiero oba razem oznaczają, że zapytanie może się udać, a narzędzie oferowane i zawsze zawodzące jest gorsze od nieobecnego.

grep dopasowuje znaki. To właściwe narzędzie do nazwy i niewłaściwe do kształtu.

Wzorzec strukturalny jest parsowany przez tę samą gramatykę co plik, więc dispose($X) znajduje wywołanie niezależnie od tego, jak jest rozstawione, zawinięte czy łamane, i nigdy nie dopasowuje tych samych znaków wewnątrz literału tekstowego lub komentarza.

Cechą, która czyni go czymś więcej niż wyrażeniem regularnym z dodatkowymi krokami, jest to, że metazmienna powtórzona w jednym wzorcu musi przechwycić ten sam tekst:

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

pasuje do warunku ochronnego, który zwalnia to, co sprawdził, a nie pasuje do takiego, który zwalnia coś innego. To klasa błędów, której nie da się w ogóle wyrazić tekstowo.

Korzysta z tree-sittera, który już jest podłączony

Dział zatytułowany „Korzysta z tree-sittera, który już jest podłączony”

Nie ma drugiego silnika. cc_natives już ładuje tree-sittera i jego gramatyki na potrzeby grafu kodu, z pamięcią podręczną skompilowanych zapytań i tabelą języków. Dopasowywacz to czysty Dart operujący na drzewach, które i tak były parsowane. Drugi silnik strukturalny oznaczałby drugi parser, drugi zestaw gramatyk do zbudowania i wdrożenia na każdej platformie oraz dwie odpowiedzi na pytanie „czym jest wyrażenie wywołania”.

Gramatyka definiuje tylko to, czym jest cały plik. Sparsowany samodzielnie na najwyższym poziomie Darta, dispose(x) jest całkowicie poprawną sygnaturą funkcji — deklaracją o nazwie dispose przyjmującą parametr x — i żaden błąd nigdzie tego nie zdradza. Dopasowywacz zbudowany na takim parsowaniu szuka deklaracji, nie znajduje wywołań i zgłasza „brak dopasowań” dla wzorca, który nigdy nie był czytany tak, jak wygląda.

Dlatego wzorzec jest próbowany w drabinie rusztowań — instrukcja, wyrażenie, składowa klasy, najwyższy poziom — i wygrywa pierwsze, które go przyjmie. Fragment, którego nie przyjmuje żaden kontekst, jest zgłaszany jako nieparsowalny, a nie przeszukiwany i po cichu nieznaleziony.

Dlaczego przepisanie strukturalne najpierw trafia do poczekalni, a nie jest od razu zapisywane

Dział zatytułowany „Dlaczego przepisanie strukturalne najpierw trafia do poczekalni, a nie jest od razu zapisywane”

Przepisanie strukturalne to najszersza edycja, jakiej może dokonać agent: jeden wzorzec, czterdzieści plików, a nikt nie czyta wyniku linia po linii — ani model, ani osoba, która o to poprosiła. Zapisanie go i zaraportowanie „gotowe” oznacza, że pierwszym uczciwym spojrzeniem na zmianę jest późniejszy git diff.

Dlatego ast_edit raportuje, co zrobiłoby — liczbę, pliki, dopasowane miejsca — a zmiana trafia na miejsce dopiero wtedy, gdy zatwierdzi ją resolve. To także pozwala postawić przed człowiekiem diff i wybór Zaakceptuj/Odrzuć, zanim cokolwiek dotrze na dysk.

Zatwierdzenie jest typu wszystko albo nic, a każdy plik jest porównywany z treścią zapisaną w momencie przygotowania. Między przygotowaniem a zatwierdzeniem agent mógł ręcznie edytować jeden z tych plików, przebieg diagnostyki mógł go przepisać albo formater mógł go dotknąć — zatwierdzenie mimo to odrzuciłoby tę pracę, a diff wyglądałby na zamierzony. Każda niezgodność odrzuca całą zmianę, bo częściowo zastosowane przepisanie strukturalne zostawia drzewo, które nie kompiluje się w żadnym z tych dwóch kształtów.

Na co odpowiada debugger, czego nie potrafi instrukcja print

Dział zatytułowany „Na co odpowiada debugger, czego nie potrafi instrukcja print”

Pętla z instrukcją print wygląda tak: dodaj print, uruchom wszystko jeszcze raz, przeczytaj wynik, usuń print. Kosztuje pełne uruchomienie testów na każde pytanie i odpowiada tylko na pytanie, które przyszło do głowy przed uruchomieniem.

Zatrzymana ramka odpowiada naraz na każde pytanie o ten moment — o każdą zmienną lokalną, każdego wywołującego i o wyrażenie obliczone w zasięgu samej ramki. W przypadku zawieszonego procesu albo błędu, który powtarza się raz na dziesięć uruchomień, to różnica między całym popołudniem a minutą.

Adapter debuggera posiada zatrzymany proces, który trzyma wszystko, co trzyma ten proces — port, blokadę, połączenie z bazą danych. Sesja, której nikt nie prowadzi, to wyciek, który przeżywa turę, w której ją rozpoczęto. Dlatego obowiązuje twardy TTL, dokładnie jedna sesja na rozmowę, a drugie launch jest odrzucane, zamiast po cichu zastępować pierwsze: agent, który rozpoczyna drugie bez zakończenia pierwszego, prawie na pewno zgubił pierwsze z oczu.

Debug Adapter Protocol dzieli z Language Server Protocol ramkowanie Content-Length i nic ponadto — inne klucze korelacji, inny kształt błędów i trzeci typ komunikatu (zdarzenia), który nie ma odpowiednika w LSP. Ramkowanie mieszka w jednym miejscu, a protokoły leżą na nim osobno.

Napisanie ramkowania dwa razy oznaczałoby dwie odpowiedzi na pytania „komunikat rozciąga się na dwa fragmenty”, „fragment zawiera trzy komunikaty” i „serwer wypisał baner na stdout przed pierwszą ramką”, a poprawkę dostałaby tylko jedna z nich.