Skip to content

fix(agent): el bot no contesta — schema strict rechazado por el email de rg_register_by_phone - #24

Merged
vgpastor merged 4 commits into
mainfrom
fix/strict-schema-email-register-by-phone
Sep 11, 2026
Merged

vgpastor merged 4 commits into
mainfrom
fix/strict-schema-email-register-by-phone

Conversation

@vgpastor

@vgpastor vgpastor commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Problema

El bot no responde a ningún usuario (Telegram y WhatsApp): cada mensaje termina en Max turns (10) exceeded.

Causa raíz

rg_register_by_phone declara email: z.string().email(). zod lo convierte en un pattern con lookahead ((?!\.)(?!.*\.\.)…). El modo strict de la Responses API ya no admite ese patrón y, en lugar de devolver un error, responde status: "incomplete" (max_output_tokens) sin output y con 0 tokens para todo el conjunto de tools. @openai/agents interpreta la respuesta vacía como "sin salida final", repite el turno y agota maxTurns.

Reproducido en srv07 con el mismo dist, la clave y el modelo por defecto (gpt-5.4-mini):

  • con las 28 tools → incomplete, output [], 0 tokens (también con gpt-4.1-mini);
  • sin tools, o con strict:falsecompleted;
  • probando tool por tool, solo falla rg_register_by_phone;
  • quitando únicamente el pattern del email, las 28 tools en strict → completed.

No se ha gastado dinero en OpenAI: las respuestas incompletas consumen 0 tokens.

Cambio

  • rg_register_by_phone.email pasa a z.string(). El formato ya lo valida la API (RegisterByPhoneDto con @IsEmail).
  • Test nuevo: falla si cualquier tool vuelve a tener un pattern con lookahead. Se comprobó que falla antes del cambio.

typecheck ✅ · build ✅ · tests 98/98 ✅

Al mergear, deploy.yml despliega a srv07 y recarga PM2.

Cambios tras revisión

  1. Un email mal formado ya no se trata como fallo técnico. Tras quitar .email(), un email roto llegaba a la API, que responde 400. TrustedAuthClient solo mapeaba el 409, así que el agente recibía el error genérico del SDK.
    • Nuevo value object de dominio Email (src/domain/email.ts): Email.tryCreate(raw) recorta espacios y valida con una regex simple sin lookahead (^[^\s@]+@[^\s@]+\.[^\s@]{2,}$). Si no es válido devuelve undefined.
    • rg_register_by_phone valida con Email antes de llamar a la API. Si no es válido, devuelve al agente un mensaje para que pida al usuario revisar el email y escribirlo de nuevo, sin lanzar excepción. Si es válido, envía el valor normalizado.
    • Defensa en profundidad: TrustedAuthClient.registerByPhone traduce el 400 a InvalidRegistrationDataError (exportado). rg_register_by_phone lo convierte en un mensaje equivalente. Igual que api-client, el body de la API se registra con console.error y no se incluye en el error.
  2. Test de lookahead más preciso. Ahora recorre el JSON schema y comprueba solo los valores de las claves pattern, buscando lookahead y lookbehind ((?=, (?!, (?<=, (?<!). Ya no revisa las descripciones, y los grupos con nombre (?<name> no cuentan como fallo. El detector tiene su propio test: detecta el pattern real de z.string().email(), encuentra patterns anidados en anyOf/items e ignora descripciones y grupos con nombre.
  3. Una sola forma de modelar los campos email. El helper emailField(description) (z.string().describe(...)) se usa en authorSchema.email, rg_preregister_donation.donorEmail y rg_register_by_phone.email. En los campos opcionales el SDK eliminaba format/pattern sin avisar, así que no validaban nada. Ahora:
    • rg_preregister_donation: si llega un donorEmail no válido, devuelve un mensaje pidiendo revisarlo u omitirlo y no llama a la API. Si es válido, lo envía recortado (Email.value).
    • author.email, que se envía a la API desde rg_register_resource, rg_submit_offer y rg_create_need: withNormalizedAuthorEmail aplica la misma validación y normalización en los tres execute, después del control de autenticación.
  4. Comentario. El comentario sobre el email de rg_register_by_phone se sustituye por uno normal que explica el porqué: el modo strict de OpenAI rechaza los patterns con lookahead, y por eso el formato se valida con el value object Email.

Tests nuevos:

  • src/domain/email.test.ts: casos válidos, inválidos y recorte de espacios.
  • trusted-auth-client.test.ts: la respuesta 400, comprobando que el error no lleva el body.
  • tools.test.ts: tests de execute para email inválido en el registro (sin llamar a la API), 400 de la API, donorEmail inválido y válido con espacios (se envía recortado), y author.email inválido y válido con espacios (se envía recortado).

typecheck ✅ · build ✅ · tests 111/111 ✅

…il en rg_register_by_phone

zod emite z.string().email() como un pattern con lookahead ((?!\.)(?!.*\.\.)).
El modo strict de la Responses API ya no lo acepta y, en vez de dar error,
devuelve status=incomplete (max_output_tokens) sin output ni tokens para todo
el set de tools. El agente agota maxTurns (10) en cada mensaje y el bot no
responde a nadie, en Telegram ni en WhatsApp.

- rg_register_by_phone: email como z.string(); la API ya valida con @isemail.
- Test que falla si cualquier tool vuelve a tener un pattern con lookahead.
…ropio

- Nuevo value object de dominio Email (src/domain/email.ts): Email.tryCreate
  recorta y valida con una regex simple sin lookahead; devuelve undefined si
  el formato no es válido.
- TrustedAuthClient.registerByPhone traduce el 400 de validación de la API a
  InvalidRegistrationDataError (exportado) en vez de lanzar un Error genérico.
Al quitar .email() del schema, un email mal formado llegaba a la API (400) y
el agente recibía el error genérico del SDK, que trataba como fallo técnico en
vez de pedir otro email.

- emailField(description): fuente única para los campos email (author.email,
  donorEmail y el email de rg_register_by_phone). En los opcionales el SDK
  eliminaba format/pattern en silencio, así que no validaban nada.
- rg_register_by_phone valida con Email antes de llamar a la API, envía el
  valor normalizado y convierte InvalidRegistrationDataError en un mensaje
  para que el agente pida revisar los datos.
- rg_preregister_donation (donorEmail) y author.email en rg_register_resource,
  rg_submit_offer y rg_create_need devuelven un mensaje pidiendo revisar u
  omitir el email si no es válido, sin llamar a la API.
- El test de lookahead recorre el schema y comprueba solo los valores de
  `pattern` (lookahead y lookbehind; los grupos con nombre no cuentan).
- Comentario del email de rg_register_by_phone reescrito explicando el porqué.
…el body del 400

- normalizeOptionalEmail valida con Email y devuelve el valor recortado o el
  mensaje para el agente. rg_preregister_donation envía donorEmail normalizado.
- withNormalizedAuthorEmail sustituye el bloque repetido en rg_register_resource,
  rg_submit_offer y rg_create_need: devuelve el mensaje o el input con
  author.email recortado.
- TrustedAuthClient: el body del 400 de register-by-phone se registra con
  console.error y InvalidRegistrationDataError lleva un mensaje genérico, igual
  que api-client (el agente podría parafrasear el error).
- Tests: donorEmail y author.email con espacios se envían recortados; el error
  del 400 no contiene el body.
@vgpastor
vgpastor merged commit 69755ed into main Sep 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant