chore: detectar respuestas incompletas del modelo, fijar dependencias y modelo explícito - #25
Merged
Merged
Conversation
…fallar rápido Si la Responses API devuelve status "incomplete" con output vacío, el SDK lo tomaba como "sin salida final", repetía turnos y acababa en MaxTurnsExceededError sin pista de la causa (incidente 2026-09-11). - ModelIncompleteResponseError (aplicación) con el motivo de la API. - Guard en infraestructura (src/infrastructure/openai): decorador de Model y ModelProvider sobre el OpenAIProvider por defecto; lanza el error en el primer turno. Si la respuesta incompleta trae output, solo se registra. - index.ts inyecta en ConversationService un Runner con el guard. - ConversationService lo registra como "model-incomplete (<motivo>)", no reintenta y envía el aviso genérico. - Tests con modelo mockeado, incluido un Runner real del SDK que reproduce el MaxTurnsExceededError sin guard.
… latest Con "latest", cualquier npm install saltaría a @openai/agents 0.18 (exige openai >=7.2 y cambia el modelo por defecto). Cada spec pasa al rango caret de la versión ya resuelta en package-lock.json. El lock solo cambia en los metadatos raíz (specs, name, license, engines); ninguna versión resuelta cambia.
Con OPENAI_MODEL vacío el modelo lo elegía el SDK, y ese valor ya cambió entre versiones. env.ts usa gpt-5.4-mini por defecto y agent.ts pasa siempre model. Los modelSettings efectivos no cambian (reasoning none + verbosity low, igual que con el default del SDK). Documentado en .env.example y README.
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.
Contexto: incidente 2026-09-11
El bot dejó de contestar a todos (Telegram y WhatsApp). Un schema de tool en modo strict hacía que la Responses API devolviera
status: "incomplete"(incomplete_details.reason: "max_output_tokens") conoutput: []y 0 tokens.@openai/agentslo tomó como "sin salida final", repitió turnos y cada mensaje acabó enMaxTurnsExceededError: Max turns (10) exceeded, sin ninguna pista de la causa.La causa raíz se arregla en #24. Esta PR endurece el sistema para que un fallo así no vuelva a pasar desapercibido. Complementa a #24 y no depende de ella: se pueden mergear en cualquier orden (#24 solo toca
src/agent/tools.tsy su test).Qué hace
R3 · Detectar respuestas incompletas y fallar rápido
ModelIncompleteResponseError(src/application/): error con nombre yreason(mensajemodel-incomplete (max_output_tokens)).src/infrastructure/openai/incomplete-response-guard.ts): decorador deModel+ModelProvidersobre elOpenAIProviderque registra el SDK por defecto. TrasgetResponsemiraproviderData(la respuesta cruda de/v1/responses):status: "incomplete"youtputvacío → lanza el error en el primer turno (sin repetir 10 turnos).status: "incomplete"con output (p. ej. truncado) → no corta el flujo, solo lo registra ({"t":"model","kind":"incomplete-partial",...}).index.tscrea elRunnercon el guard (createOpenAIRunner()) y lo inyecta enConversationService. La aplicación solo conoce el error.ConversationService: registrakind: "error"conmodel-incomplete (<motivo>), no reintenta (no es transitorio) y envía el aviso genérico que ya existía.Por qué se engancha así en
@openai/agents0.12:new OpenAI({ fetch }): el clienteopenaitrata un error lanzado dentro defetchcomo fallo de conexión, lo reintenta y lo relanza comoAPIConnectionError, así que se perdería el tipo.ModelaAgent.model: el SDK solo aplica losmodelSettingspor defecto del modelo (reasoning.effort: none,verbosity: low) cuandomodeles un string.ModelProvider: el runner resuelve el nombre del modelo con él, y conretry.maxRetriespor defecto en 0 el error llega arun()sin envolver.R4 · Fijar dependencias
Cada
"latest"pasa al rango caret de la versión ya resuelta en el lock:@openai/agents ^0.12.0,openai ^6.45.0,telegraf ^4.16.3,dotenv ^17.4.2,@types/node ^26.1.0,tsx ^4.22.5,typescript ^6.0.3. Lock regenerado connpm install --package-lock-only: ninguna versión resuelta cambia. Solo cambian los metadatos raíz: los specs y, de paso,name,licenseyengines, que en el lock estaban desfasados respecto apackage.json.R5 · Modelo explícito
OPENAI_MODELvacío →gpt-5.4-mini(DEFAULT_OPENAI_MODELensrc/config/env.ts), yagent.tspasa siempremodel. LosmodelSettingsefectivos son idénticos a los que aplicaba el default del SDK (comprobado connew Agent(...)). Documentado en.env.example,README.mdyREADME.es.md.Cómo se ha probado
incomplete-response-guard.test.ts(modelo mockeado):reason/responseId;unknown;Runnerreal del SDK: sin guard reproduce elMaxTurnsExceededErrordel incidente; con guard falla en la primera llamada conModelIncompleteResponseError.conversation-service.test.ts: registramodel-incomplete (max_output_tokens), 1 sola llamada, aviso genérico;isTransientErrorno lo captura.env.test.ts: valor por defecto y override deOPENAI_MODEL.tracingDisableddel Runner no evita la exportación de trazas.Gate (igual que el CI):