jfastframework
Documentación English PyPI GitHub

Trabajar con agentes de IA

La premisa de este framework es que un agente escribiendo código en tu repositorio ya es normal, y que lo que lo hace sobrevivible no es un prompt mejor — son reglas que el agente no puede romper en silencio.

Tres superficies, en orden creciente de cuánto ayudan:

Qué le da a un agente
jfast ai context --json todo sobre este proyecto, en una sola llamada
jfast next qué queda sin terminar, en el orden en que se puede hacer
contracts.toml qué puede y qué no, verificado por CI
AGENTS.md + .jfast/skills/ cómo espera este proyecto que se trabaje

La superficie de agente generada

jfast new service billing --agent-docs
jfast init                              # asks

Escribe:

AGENTS.md                                   the rules, at the root where agents look
.jfast/skills/respect-contracts/SKILL.md    read the contract before writing
.jfast/skills/design-system/SKILL.md        only when there is a frontend

Apagado por defecto. Un proyecto al que nadie le apunta un agente no debe archivos de agente, y cada archivo que se envía es un archivo que puede desviarse del código que describe.

Por qué .jfast/skills/ y no un archivo grande

Porque el valor está en no cargarlo todo. Una skill declara para qué sirve y cuándo saltarla:

---
name: respect-contracts
description: Read this project's contract before writing code in it, and verify
  the code against it before calling the work done.
when_to_use: Always, before any code change in billing.
when_not_to_use: Answering a question about the project without changing it.
---

Un agente lee el front matter de cada una, elige la que corresponde a la tarea, y carga solo esa. Un archivo monolítico gasta el presupuesto de contexto en reglas que no aplican al cambio en curso — y duplica AGENTS.md, que entonces se desincroniza.

Para qué es cada archivo

AGENTS.md — las reglas que aplican a todo cambio: la forma a nivel de servicio, las cinco reglas verificadas, qué no hacer, y los comandos. Corto a propósito.

No dice nada sobre los archivos que hay dentro de un módulo, y es a propósito. Se escribe al generar el servicio, antes de que exista un módulo, y un mismo servicio puede tener módulos en los cuatro layouts. Nombrar router.py ahí era una promesa que se cumplía en uno de los cuatro. En su lugar nombra las dos cosas que se escriben junto al código y por eso siempre son ciertas: [modules.<name>] en jfast.toml para el layout, y modules/<name>/README.md para el mapa de archivos de ese layout.

Una skill — el procedimiento para un tipo de tarea: precondiciones, pasos con los comandos exactos, cómo verificar, y los errores que la gente comete de verdad.


Las reglas de las que un agente no se puede desviar

No son consejos. jfast contracts check rompe el build:

modules/payment/<file>:41: cross-module: module 'payment' imports modules.invoice.service; import modules.invoice.public instead
  (only public.py is another module's API -- call a function in modules/invoice/public.py that returns DTOs (add one if it is missing), and add "invoice" to depends_on under [modules.payment] in contracts.toml)

archivo:línea, la regla, qué pasó, y qué hacer al respecto. Esa segunda línea importa más de lo que parece: un agente al que le das una violación sin remedio tiende a satisfacer al checker en vez de arreglar el diseño — borrando el import, copiando el código, o apagando la regla. Nombrar el destino elimina la ambigüedad.

La lista completa está en Contratos. Las que más seguido atrapan código generado:

Regla Por qué la pisa un agente
La capa HTTP no puede importar sqlalchemy Consultar desde el handler es el camino más corto a un endpoint que anda
Un módulo importa a otro solo a través de su public.py, declarado en depends_on Reusar el modelo del vecino es más fácil que pedírselo a su fachada
Nada de SQL crudo contra tablas de otro módulo Un text("SELECT ...") parece esquivar la regla de imports
shared/ no puede importar un módulo Arreglar lo anterior importando al revés
Nada bloqueante en async def time.sleep y requests es lo que usan casi todos los ejemplos

Genera el módulo que querías, y construye encima

Un agente al que se le daba jfast new module invoice gastaba sus primeras ediciones borrando el ejemplo -- name, description, is_active y sus tests --, y el diff que leía el revisor era sobre todo eso. Dale los campos al generador y el primer borrador no tiene nada que borrar:

jfast new module invoice --fields "customer_id:int, number:str(20), total:money, paid:bool=false" \
  --unique "number"

Lo que escribe ya pasa ruff check ., ruff format --check ., mypy . (estricto) y pytest, en todos los layouts, así que un control en rojo después de la edición de un agente es la edición del agente. Las rutas quedan protegidas según esté configurado el servicio (current_tenant con tenancy, require_auth con auth), y el módulo trae un tasks.py para su trabajo en segundo plano. La gramática está en Módulos; --bare es la estructura sin ningún campo.


Exime una línea, no borres la regla

from modules.invoice.enums import Status  # contracts: allow migrating to shared

La exención va en una línea, con una razón, y jfast contracts waivers las lista todas. Apagar la regla en contracts.toml la quita para todo el mundo, en silencio, y la próxima violación no se reporta — que es cómo un contrato deja de significar algo.


Una sola llamada antes de la primera edición

jfast ai context --json

Todo sobre este proyecto en una sola respuesta: cada módulo y su forma, qué importa realmente main.py, el grafo de imports entre módulos, el contrato, qué dicen ahora mismo analyze y contracts check, qué queda sin terminar, y los comandos que devuelven lo que se dejó afuera.

Bajo environment lista los ajustes de jfast.toml que son del entorno, cuáles sobrescribe el entorno actual (enmascarados), y la regla que un agente debe seguir: esos valores del archivo son solo defaults, y un desacuerdo se le reporta a una persona, nunca se "arregla" de ningún lado (Quién gana).

Es una composición, no una reimplementación — inspect, analyze, graph, contracts y next en un solo payload, así que no puede contradecir al comando que te dice que ejecutes. Nunca importa el proyecto, así que sigue respondiendo en un servicio al que le faltan dependencias o cuyo código no parsea.

Cuánto pesa

No hay un número único, y publicar uno solo lo volvía equivocado para todo proyecto que no fuera aquel donde se midió. El payload son hechos sobre tu servicio, así que escala con tu servicio. Medido sobre servicios generados — jfast new service shop --with database y después jfast new module N veces (0.1.0a12):

módulos --json --json --brief
1 10,295 3,750
3 12,353 5,340
5 14,429 6,944

Un servicio recién generado es el piso, porque todavía no hay nada mal en él. El mismo servicio de cinco módulos después de trabajarlo un rato — dos módulos que main.py nunca levantó, un archivo fuera de todo módulo, dos violaciones de contrato — mide unos 3.3 KB más que el piso de arriba completo, y 0.8 KB más con --brief (medido en 0.1.0a9, cuando los pisos eran 11.5 KB y 5.4 KB). Aproximadamente +1,000 bytes por módulo (0.1.0a11 agregó la fachada, los eventos y las tareas de cada módulo); el resto es next y checks creciendo con lo que realmente está pendiente.

Dónde se va el payload completo en ese servicio de cinco módulos, en bytes:

next      3,052   contract  2,300   commands 1,576   checks  1,562
modules   1,499   omitted     921   project    135   plugins   130

0.1.0a12 agregó la sección environment: unos 600 bytes en un servicio generado, más por cada ajuste del entorno que escribe jfast.toml.

Las cifras de tokens son bytes ÷ 4 — la estimación gruesa habitual, no un tokenizer.

jfast ai context --size es la respuesta para tu proyecto, y es el comando que ejecutas antes de decidir si vas a llamar al otro dentro de un bucle. La tabla de arriba es una escala, no un presupuesto: no planifiques una ventana de contexto contra ella.

Qué deja afuera a propósito

La implementación obvia de "todo lo que un modelo necesita" concatena docs/. Está mal por dos motivos distintos:

Así que el payload lleva hechos sobre el proyecto, y nombra cada hueco bajo omitted junto con el comando que lo cierra:

Qué se deja afuera Cómo obtenerlo
La documentación https://jfabrizzio5.github.io/JFastFramework/latest/
El contenido de los archivos abre los que nombra — nunca cita código fuente
El listado de archivos por módulo jfast ai context --module <name>
Settings resueltos, plugin graph vivo, tabla de rutas jfast describe --json (importa la app)
Qué le hace cada revisión sin aplicar a una base con filas jfast migration check --json, jfast migration plan
Findings o violaciones más allá de los primeros 20 jfast analyze --json, jfast contracts check --json
Historia git log

Nombrar los huecos importa más de lo que parece. Un agente al que le pasas una respuesta parcial sin costura la trata como completa, y después escribe código contra un archivo que nunca vio.

Acotar

jfast ai context --json --module invoice   # un módulo, con su lista de archivos
jfast ai context --json --brief            # la forma, sin el detalle
jfast ai context --size                    # cuánto cuestan los dos de arriba

--module acota los módulos, los findings y las violaciones. No acota next, a propósito: preguntaste por un módulo, y el paso que te está bloqueando puede estar en otro.

--brief descarta las reglas del contrato (conserva su alcance, los nombres de layers y los invariants), las listas de findings y violaciones (conserva los conteos), shared/, y la prosa de commands y omitted.

Los comandos a los que apunta

Cada entrada de commands es un punto de entrada legible por máquina, con qué devuelve y qué te ahorra leer:

jfast inspect --json           # modules, layouts, routes, wiring, tables
jfast analyze --json           # structural findings, each with a remedy
jfast graph --format json      # module-to-module import edges
jfast contracts show --json    # layers, forbidden calls, interfaces, invariants
jfast contracts check --json   # violations, with file, line and rule
jfast next --json              # what is unfinished, in order
jfast describe --json          # resolved settings and plugin graph

Todos ellos menos describe leen el sistema de archivos sin importar el proyecto.


jfast next — qué queda sin terminar

El mismo motor que jfast analyze, dado vuelta. analyze dice qué está mal; next dice qué hacer al respecto, en el orden en que se puede hacer:

  next  shop

   1. module 'ghost' declares routes but main.py never imports it
      └─ edit main.py between the [jfast:imports] and [jfast:routers] markers
   2. 'helpers.py' belongs to no module                      move it into a module, or into shared/
   3. ghosts has no revision (this project has none)         alembic revision --autogenerate
   4. invoices has no revision (this project has none)       alembic revision --autogenerate
   5. contracts check fails (2 violations)                   jfast contracts check
   6. modules/order has no tests                             add modules/order/tests/
   7. contracts.toml still has its generated placeholders    edit contracts.toml
   8. modules/payment has no README                          add modules/payment/README.md

  wire -> shape -> persist -> verify -> cover -> document   (dependency order, not severity)

Esta es la respuesta a "el agente se saltó un paso".

El orden es el punto

Los pasos se ordenan por etapa, y una etapa es precondición de las que vienen después — nunca por qué tan grave es el finding:

Etapa Nada posterior vale la pena hasta que
boot el servicio arranque — un plugin habilitado que nadie provee lo impide
scaffold exista un módulo que cablear, migrar o testear
wire main.py lo importe: los tests de un módulo sin cablear pasan mientras sus rutas dan 404
shape el código haya dejado de moverse entre archivos
persist existan las tablas que quedaron tras acomodar el código
verify el contrato se haya verificado contra dónde terminaron los archivos
cover el código bajo el test esté cableado, ubicado y respaldado por una tabla
document — al final: describe lo que las etapas de arriba dejaron resuelto

La severidad daría un orden distinto y peor. En el listado de arriba, 'helpers.py' belongs to no module es low y ghosts has no revision es medium, y aun así el archivo suelto va primero: moverlo cambia qué tablas declara el proyecto, así que una revisión generada antes de moverlo es una que regeneras después. Testear un módulo sin cablear es el mismo error en versión más ruidosa — el test pasa, y el endpoint da 404.

jfast next --json lleva el stage y su rank numérico en cada paso, más un bloque stages que explicita de qué es precondición cada uno.

Un hecho, un paso

Un contrato escrito para otro layout lo reportan dos comandos a la vez: analyze emite un contract-governs-nothing por capa vacía, y contracts check emite un layer-unmatched por esas mismas capas. Cuatro capas vacías llegaban entonces como cuatro pasos más un resumen contracts check fails (4 violations) de esos mismos cuatro — cinco líneas sobre un archivo, archivadas bajo shape como si algo tuviera que moverse, y cada una con jfast analyze # contract-governs-nothing, que reimprime lo que ya estás mirando.

Ahora es un solo paso, en verify, y nombra el comando que lo resuelve:

   2. contracts.toml governs nothing: 4 layers match no file here
      └─ jfast contracts init --layout hexagonal --force

El layout sale de lo que jfast.toml registra para los módulos. Si no están todos en el mismo layout, el hueco queda como <layout>: --force pisa el contrato y una adivinanza no vale ese riesgo. Y una violación que no sea layer-unmatched sigue teniendo su propio paso contracts check fails (n) — sacar el duplicado no puede sacar el resto.

Cuando no queda nada

  ✓ shop: nothing outstanding

  3 modules, every one registered, tested and documented.
  2 revisions cover every declared table.
  contracts.toml passes, and its placeholders are filled in.

Enunciado como la lista de lo que se verificó y no como felicitación, porque una herramienta que siempre encuentra algo que hacer entrena a la gente a ignorarla, y una que dice "todo bien" sin decir qué miró no es mejor.

jfast next siempre sale con 0. Es una pregunta, no una compuerta — la compuerta es jfast analyze --fail-on high.

Sobre un servicio recién generado

No se queda callado, y todo lo que dice es cierto. jfast new service escribe tres modelos con __tablename__ y ninguna revisión, y un contracts.toml cuyos owns y does_not_own siguen en TODO:

   1. invoices has no revision (this project has none)   alembic revision --autogenerate
   2. orders has no revision (this project has none)     alembic revision --autogenerate
   3. payments has no revision (this project has none)   alembic revision --autogenerate
   4. contracts.toml still has its generated placeholders: owns, does_not_own, invariants
      └─ edit contracts.toml

No afirma que los módulos estén sin cablear o sin tests — el generador los cableó y los testeó, y decir lo contrario es la falla que hace que la gente deje de leer la salida.

Este es además el único punto donde next reporta algo que analyze no. analyze se queda callado sobre migraciones cuando un proyecto no tiene ninguna, para no recibir a un servicio nuevo con un finding. next existe para nombrar el paso siguiente al que acabas de dar, y en ese proyecto el paso es la primera revisión.


Lo que esto no resuelve

Un agente que sigue todas las reglas igual puede construir lo equivocado. Los contratos restringen estructura, no intención: nada aquí se da cuenta de que la funcionalidad no era la que pediste, de que el test afirma el bug, o de que una regla que escribiste en enero está mal en junio.

Lo que sí compras es más angosto y vale igual: el código no se degrada mientras no estás mirando, y un review puede ser sobre si la funcionalidad está bien en vez de sobre dónde quedó el archivo.


La superficie de agente del propio framework

Este repositorio lo practica: AGENTS.md en la raíz y siete skills bajo .jfast/skills/, cubriendo creación de módulos, autoría de plugins, contratos, el frontend y el sistema de diseño. Skills para agentes cubre cómo escribir una.