[bug][ai-chat] Tool-call валидация отдаёт модели сырое zod-сообщение вместо понятного (роняется pageId в параллельной партии) #190
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Симптом
При работе встроенного AI-чата (in-app, с инструментами правки страницы) при пакетном/параллельном вызове
createCommentпериодически прилетает:Паттерн из реального лога одной сессии:
createComment— успех.pageId undefined.Причина (разобрано)
Это не state-баг сервера. В записанных аргументах упавших вызовов
pageIdфизически отсутствует:а все успешные содержат
"pageId": "019efe44-…".То есть модель сама роняет обязательный
pageIdв части параллельных tool-call'ов (типичное поведение LLM: «очевидный» повторяющийся аргумент опускается в пакете). zod v4 корректно отклоняетundefined. Реплика агента «хотя pageId был передан» — галлюцинация, в аргументах его нет.Проверено, что HTTP-тело / серверный путь ни при чём:
req.body(Fastify) изолирован per-request,parsedBodyпрокидывается без потерь, аргументы переживают разбор JSON-RPC. Источник пустогоpageId— уровень tool-call модели.Деталь про слои: формат ошибки — zod v4 (
Invalid input: expected string, received undefined) → это in-app AI-чат (apps/server, zod 4.x). На MCP-пути пакета (packages/mcp, zod 3.x) то же самое дало быRequired. Подтверждается camelCase-именами инструментов в фидбэке (getPage,getOutline,getPageJson,createComment— этоinAppKey).Чего НЕ делаем
Поведение модели не «чиним» и страницу не угадываем (не подставляем «текущую» страницу молча — это привело бы к комментированию не той страницы, ср. #159).
Что делаем
Сообщение валидации должно быть человекочитаемым для модели, чтобы она осознанно повторила вызов с
pageId. Сейчас понятного сообщения нет нигде: все инструменты используют голыйz.string()/z.object(), и при пропуске любого параметра модель получает сырой zod-текст.Нужен централизованный враппер входных схем для всех in-app инструментов (а не правка поля за полем):
modelFriendlyInput(shape)=jsonSchema(z.toJSONSchema(z.object(shape), { target: 'draft-7' }), { validate }), гдеvalidateгоняетz.object(shape).safeParse(value)и при ошибке возвращает понятное сообщение с именами проблемных параметров (напр.parameter "pageId": missing/invalid; include every REQUIRED parameter, don't drop ids like pageId when issuing parallel tool calls);sharedTool(...)и во всех инлайновыхtool({ inputSchema: ... });public-share-chat-tools.service.ts;required/optionalостаются как есть (JSON-схема изz.toJSONSchemaсохраняетrequired,descriptionи ограничения полей).Проверенные факты для реализации:
aiэкспортируетjsonSchema; для нашегоSchema(изjsonSchema()) AI SDK вызывает нашvalidateнапрямую и оборачивает ошибку вInvalidToolInputError→ текст уходит модели как tool-error;z.toJSONSchema(schema, { target: 'draft-7' })(zod 4.3.6) даёт чистую draft-07 схему сrequired/description/ ограничениями.Файлы
apps/server/src/core/ai-chat/tools/ai-chat-tools.service.ts(основное)apps/server/src/core/ai-chat/tools/public-share-chat-tools.service.ts(тот же паттерн)Acceptance
required/optionalинструментов не изменился; значения не угадываются.ai-chat-tools.service.spec.ts/public-share-chat-tools.service.spec.tsзелёные.Связано с #188 (заметки об инструментах). Первоисточник — фидбэк «createComment — intermittent pageId undefined при пакетном вызове».