Cambiar lo que puede hacer tu agente no debería ser un despliegue

Imaginá una firma de inversiones con un agente que toma pedidos de retiro. Un día, alguien cambia la cuenta de destino de un cliente y pide un retiro a los pocos minutos, y resulta ser un fraude. Compliance fija una regla nueva: después de un cambio de destino, no pasa ningún retiro hasta que back office lo verifique con el cliente. Durante cuánto tiempo aplica depende de la política de cada cuenta.

No alcanza con actualizar un valor. La firma nunca guardó cuándo cambia un destino ni cuándo se verifica, y no tiene políticas para consultar. Si las reglas del agente están en su código, hay que escribir esa lógica en cada lugar desde donde pueda pedir un retiro y sacar una versión nueva. Si están en el prompt, le pediste al modelo que la cumpla, y nada lo obliga.

Antes de que la regla rija, alguien debería poder contestar tres preguntas:

En el resto de la nota hago ese cambio en Zarel, con un contrato de demo en un stack local, el 3 de noviembre de 2026. Los comandos y las salidas son de esa corrida, recortados. Es el mismo agente de demo que en nuestro sitio no da consejos de inversión, ampliado para registrar pedidos de retiro. No mueve plata.

Paso 1: agregar los datos

En Zarel, las reglas de un agente y los datos que leen se declaran en un contrato, fuera de su código. La primera publicación agrega una política y las dos fechas de cada cuenta:

- name: withdrawal_policies
  fields:
    - name: name
      type: string
      required: true
    - name: destination_hold_days
      type: number
      required: true
- name: accounts
  fields:
    # los campos que ya tenía, y:
    - name: policy
      type: reference
      references: withdrawal_policies
    - name: destination_changed_at
      type: datetime
    - name: destination_verified_at
      type: datetime
    - name: destination_hold_days
      type: number
      compute:
        expression: policy.destination_hold_days
        strategy: read

No hay migración ni despliegue, y ningún pedido se trata distinto. Back office carga las políticas y las cuentas mientras siguen llegando pedidos. En esta corrida se aceptaron cinco, de 30.000 cada uno.

Los datos van primero por un motivo: el paso siguiente juzga pedidos pasados, y de un pedido anterior a estos campos no se puede saber qué tenían.

Paso 2: probar la regla contra lo que ya pasó

La regla es un chequeo sobre cada pedido de retiro:

- name: recent_destination_change
  on: [ create ]
  effect: deny
  expression: >-
    account.destination_changed_at > now() - duration(account.destination_hold_days, 0, 0, 0)
    and (account.destination_verified_at == null
         or account.destination_verified_at < account.destination_changed_at)
  message: recent_destination_change

Una prueba en seco juzga con ella los cinco pedidos pasados:

$ zarel contract spec dry-run rule.yaml --from ... --to ... --watch

- Sessions evaluated: 5
- Re-evaluated with the same outcome: 3 (60.0%)
- Outcomes changed: 2 (40.0%)

| from     | to      | count | cause                                                                        |
| executed | refused | 2     | Diverged stages: checks; check:withdrawal_requests.recent_destination_change |

Cada pedido, con la regla:

Si la prueba abarca un período más largo, también alcanza pedidos de antes de que existieran los campos nuevos. A esos no los informa como iguales: dice que no pudo juzgarlos (field_not_declared_then). Tampoco vuelve a correr el modelo, así que no puede decir qué habría propuesto el agente con la regla nueva.

Paso 3: la aprueba otra persona

En esta corrida, el contrato dice quién aprueba un cambio en sus entidades. Por eso, publicar crea una propuesta, y todavía no rige nada:

$ zarel contract spec apply rule.yaml

"publication": {
  "status": "proposed",
  "requirements": { "entities": "four_eyes" }
}

Mientras esperaba, un pedido nuevo de la primera de esas cuentas se siguió aceptando, y a un asesor que intentó aprobar la propuesta se lo rechazó.

La propuesta también le avisó a quien aprueba algo que necesita saber: una cuenta todavía no tiene política, así que la regla no se le puede aplicar hasta que alguien se la asigne. Un chequeo sobre un valor vacío no se dispara, y conviene enterarse antes de que rija.

El usuario de back office la aprobó por la API. La publicación registró quién la propuso, quién la aprobó y con qué rol. Este paso es opcional: un contrato que no nombra aprobadores rige cuando se publica.

Paso 4: rige desde la acción siguiente

No hubo ningún despliegue. El siguiente pedido de esa primera cuenta:

HTTP 422 validation_violation
a validation rule refused the write (entity withdrawal_requests; rule recent_destination_change)

Las tres cuentas aceptadas se volvieron a aceptar. Los rechazos de antes y de después de un cambio citan versiones distintas del contrato por su hash, en la cadena de decisiones.

Otras dos cosas cambiaron sin una versión nueva del contrato, porque son datos. Back office acortó la política a 1 día y el mismo pedido se aceptó. Con la espera otra vez en 7 días, un asesor intentó registrar la verificación y se lo rechazó. Back office la registró, y el pedido pasó.

Lo que esto no hace