fix(agent): el bot no contesta — schema strict rechazado por el email de rg_register_by_phone - #24
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_phonedeclaraemail: z.string().email(). zod lo convierte en unpatterncon lookahead ((?!\.)(?!.*\.\.)…). El modo strict de la Responses API ya no admite ese patrón y, en lugar de devolver un error, respondestatus: "incomplete"(max_output_tokens) sin output y con 0 tokens para todo el conjunto de tools.@openai/agentsinterpreta la respuesta vacía como "sin salida final", repite el turno y agotamaxTurns.Reproducido en srv07 con el mismo
dist, la clave y el modelo por defecto (gpt-5.4-mini):incomplete, output[], 0 tokens (también congpt-4.1-mini);strict:false→completed;rg_register_by_phone;patterndel 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.emailpasa az.string(). El formato ya lo valida la API (RegisterByPhoneDtocon@IsEmail).patterncon lookahead. Se comprobó que falla antes del cambio.typecheck ✅ · build ✅ · tests 98/98 ✅
Al mergear,
deploy.ymldespliega a srv07 y recarga PM2.Cambios tras revisión
.email(), un email roto llegaba a la API, que responde 400.TrustedAuthClientsolo mapeaba el 409, así que el agente recibía el error genérico del SDK.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 devuelveundefined.rg_register_by_phonevalida conEmailantes 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.TrustedAuthClient.registerByPhonetraduce el 400 aInvalidRegistrationDataError(exportado).rg_register_by_phonelo convierte en un mensaje equivalente. Igual queapi-client, el body de la API se registra conconsole.errory no se incluye en el error.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 dez.string().email(), encuentra patterns anidados enanyOf/itemse ignora descripciones y grupos con nombre.emailField(description)(z.string().describe(...)) se usa enauthorSchema.email,rg_preregister_donation.donorEmailyrg_register_by_phone.email. En los campos opcionales el SDK eliminabaformat/patternsin avisar, así que no validaban nada. Ahora:rg_preregister_donation: si llega undonorEmailno 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 desderg_register_resource,rg_submit_offeryrg_create_need:withNormalizedAuthorEmailaplica la misma validación y normalización en los tresexecute, después del control de autenticación.rg_register_by_phonese 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 objectEmail.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 deexecutepara email inválido en el registro (sin llamar a la API), 400 de la API,donorEmailinválido y válido con espacios (se envía recortado), yauthor.emailinválido y válido con espacios (se envía recortado).typecheck ✅ · build ✅ · tests 111/111 ✅