Ir al contenido

Inteligencia de código

Control Center le da a un agente cuatro formas de entender el código más allá de leerlo. Responden a preguntas genuinamente distintas, y la razón de que haya cuatro en lugar de una es que ningún mecanismo por sí solo responde a todas.

Mecanismo Responde a Costos
Grafo de código Qué hay aquí y de qué depende Una pasada de indexación; consultas baratas
Servidor de lenguaje Si esto compila de verdad y qué es este tipo Un proceso de servidor por proyecto
Búsqueda estructural Dónde aparece esta FORMA Un análisis por archivo
Depurador Qué era cierto en este momento, en este frame Un proceso detenido

El grafo de código se construye a partir de tree-sitter: símbolos, aristas de llamada, un radio de impacto. Es rápido, es barato y no sabe nada de tipos. No puede decirte si la llamada que acabas de escribir compila, si ese campo existe en el objeto o si tu renombrado se saltó una reexportación.

Sin un servidor de lenguaje, un agente se entera hasta que ejecuta las pruebas, o nunca. Escribe algo plausible, sigue adelante y el fallo aparece tres turnos después, asociado a un cambio distinto.

Así que el beneficio real no es la herramienta lsp. Es que write, edit y apply_patch están envueltas: después de cada escritura, el archivo pasa por el servidor de lenguaje y los diagnósticos recién introducidos se incorporan al resultado de la herramienta. El agente se entera de que rompió la compilación en el mismo momento en que la rompe, se le haya ocurrido preguntar o no.

Repetir lo que ya se le dijo al agente es peor que no decir nada: llena el resultado de ruido que el agente aprende a pasar por alto, y el único error nuevo se va con él. Un registro por ejecución lleva la cuenta de lo que ya se reportó, con la clave puesta en la severidad, el código y el mensaje —no en la posición, así que un diagnóstico que solo bajó cuatro líneas no se vuelve a reportar como nuevo.

Ningún servidor arranca al iniciar. Uno arranca en la primera solicitud que lo necesita, que casi siempre es la primera edición de un archivo que reclama como suyo, y los que están inactivos se limpian. Un host que nunca toca Dart nunca arranca un analizador.

La detección es una intersección: debe existir un marcador de raíz en el checkout y el binario debe poder resolverse. Un marcador por sí solo dice «este proyecto es Python» y nada sobre si el servidor está instalado; un binario que se resuelve por sí solo dice que el host tiene una cadena de herramientas y nada sobre este checkout. Solo los dos juntos significan que una consulta puede tener éxito, y una herramienta que se ofrece y siempre falla es peor que una que no está.

Por qué existe la búsqueda estructural si ya está grep

Sección titulada «Por qué existe la búsqueda estructural si ya está grep»

grep compara caracteres. Es la herramienta correcta para un nombre y la equivocada para una forma.

Un patrón estructural se analiza con la misma gramática que el archivo, así que dispose($X) encuentra la llamada sin importar cómo esté espaciada, envuelta o partida en varias líneas, y nunca coincide con los mismos caracteres dentro de un literal de cadena o un comentario.

La propiedad que lo hace algo más que una regex con pasos extra es que una metavariable repetida en un patrón debe capturar el mismo texto:

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

coincide con una guarda que libera lo que comprobó y no coincide con una que libera otra cosa. Es una clase de error que no puedes expresar de forma textual en absoluto.

Se ejecuta sobre el tree-sitter que ya está enlazado

Sección titulada «Se ejecuta sobre el tree-sitter que ya está enlazado»

No hay un segundo motor. cc_natives ya carga tree-sitter y sus gramáticas para el grafo de código, con una caché de consultas compiladas y una tabla de lenguajes. El buscador es Dart puro sobre árboles que de todos modos ya se estaban analizando. Un segundo motor estructural significaría un segundo analizador, un segundo conjunto de gramáticas que compilar y preparar en cada plataforma, y dos respuestas a «qué es una expresión de llamada».

Una gramática solo define qué es un archivo completo. Analizado tal cual en el nivel superior de Dart, dispose(x) es una firma de función perfectamente válida —una declaración llamada dispose que recibe un parámetro x— sin ningún error en ninguna parte que lo delate. Un buscador construido sobre ese análisis busca declaraciones, no encuentra llamadas y reporta «ninguna coincidencia» para un patrón que nunca se estaba leyendo como parece.

Así que un patrón se prueba en una escalera de andamiajes —sentencia, expresión, miembro de clase, nivel superior— y gana el primero que lo acepta. Un fragmento que ningún contexto acepta se reporta como no analizable en lugar de buscarse y no encontrarse en silencio.

Por qué una reescritura se prepara en lugar de escribirse directamente

Sección titulada «Por qué una reescritura se prepara en lugar de escribirse directamente»

Una reescritura estructural es la edición más amplia que puede hacer un agente: un patrón, cuarenta archivos y nadie lee el resultado línea por línea, ni el modelo ni la persona que lo pidió. Escribirla y reportar que está «hecha» significa que la primera mirada honesta al cambio es un git diff a posteriori.

Así que ast_edit reporta lo que haría —el conteo, los archivos, los sitios coincidentes— y el cambio solo se aplica cuando resolve lo confirma. Eso es también lo que permite que un diff y un Aceptar/Descartar se pongan delante de una persona antes de que algo llegue al disco.

La confirmación es todo o nada y cada archivo se comprueba contra el contenido capturado en el momento de la preparación. Entre la preparación y la confirmación, puede que el agente haya editado a mano uno de esos archivos, que una pasada de diagnósticos lo haya reescrito o que un formateador lo haya tocado; confirmar de todos modos descartaría ese trabajo y el diff parecería intencional. Cualquier discrepancia rechaza todo el cambio, porque una reescritura estructural aplicada a medias deja un árbol que no compila con ninguna de las dos formas.

Qué responde un depurador que no puede responder una instrucción print

Sección titulada «Qué responde un depurador que no puede responder una instrucción print»

El bucle de la instrucción print es: agregar un print, volver a ejecutarlo todo, leer la salida y borrar el print. Cuesta una ejecución completa de pruebas por cada pregunta y solo responde a la pregunta que se te ocurrió hacer antes de ejecutar.

Un frame detenido responde de una vez a todas las preguntas sobre ese momento: cada variable local, cada invocador y una expresión evaluada en el propio ámbito del frame. Para un proceso atorado o un fallo que se reproduce una vez de cada diez ejecuciones, esa es la diferencia entre una tarde entera y un minuto.

Acotada como un recinto, y por el mismo motivo

Sección titulada «Acotada como un recinto, y por el mismo motivo»

Un adaptador de depuración posee un proceso detenido que retiene todo lo que retiene ese proceso: un puerto, un bloqueo, una conexión a una base de datos. Una sesión que nadie dirige es una fuga que sobrevive al turno que la inició. Así que hay un TTL estricto, exactamente una sesión por conversación, y un segundo launch se rechaza en lugar de reemplazar en silencio al primero: un agente que inicia un segundo sin terminar el primero casi con seguridad perdió la pista del primero.

El Debug Adapter Protocol comparte el framing de Content-Length del Language Server Protocol y nada por encima de él: claves de correlación distintas, una forma de fallo distinta y un tercer tipo de mensaje (eventos) sin equivalente en LSP. El framing vive en un solo lugar; los protocolos se apoyan en él por separado.

Escribir el framing dos veces significaría dos respuestas a «un mensaje abarca dos fragmentos», «un fragmento contiene tres mensajes» y «el servidor escribió un banner en stdout antes de su primer frame», y solo una de ellas recibiría la corrección.