[bug][ai-chat] Tool-call валидация отдаёт модели сырое zod-сообщение вместо понятного (роняется pageId в параллельной партии) #190

Closed
opened 2026-06-25 20:52:48 +03:00 by claude_code · 0 comments
Collaborator

Симптом

При работе встроенного AI-чата (in-app, с инструментами правки страницы) при пакетном/параллельном вызове createComment периодически прилетает:

Invalid input for tool createComment: ... "pageId": Invalid input: expected string, received undefined

Паттерн из реального лога одной сессии:

  • Первая партия из 3 параллельных createComment — успех.
  • Вторая партия из 3 (та же страница) — все три падают с pageId undefined.
  • Повтор по одному — все проходят.

Причина (разобрано)

Это не state-баг сервера. В записанных аргументах упавших вызовов pageId физически отсутствует:

// упавшие вызовы (вторая партия) — НЕТ pageId:
{ "content": "…", "selection": "титановый проводник" }
{ "content": "…", "selection": "поверка на месте…" }
{ "content": "…", "selection": "В диапазоне 316–649 °C…" }

а все успешные содержат "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

  • При пропущенном/неверном параметре любой in-app инструмент возвращает понятное сообщение с именем параметра и подсказкой повторить с обязательными полями.
  • Контракт required/optional инструментов не изменился; значения не угадываются.
  • Тесты ai-chat-tools.service.spec.ts / public-share-chat-tools.service.spec.ts зелёные.

Связано с #188 (заметки об инструментах). Первоисточник — фидбэк «createComment — intermittent pageId undefined при пакетном вызове».

## Симптом При работе встроенного AI-чата (in-app, с инструментами правки страницы) при **пакетном/параллельном** вызове `createComment` периодически прилетает: ``` Invalid input for tool createComment: ... "pageId": Invalid input: expected string, received undefined ``` Паттерн из реального лога одной сессии: - Первая партия из 3 параллельных `createComment` — успех. - Вторая партия из 3 (та же страница) — **все три падают** с `pageId undefined`. - Повтор по одному — все проходят. ## Причина (разобрано) Это **не** state-баг сервера. В записанных аргументах упавших вызовов `pageId` физически отсутствует: ```json // упавшие вызовы (вторая партия) — НЕТ pageId: { "content": "…", "selection": "титановый проводник" } { "content": "…", "selection": "поверка на месте…" } { "content": "…", "selection": "В диапазоне 316–649 °C…" } ``` а все успешные содержат `"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 - При пропущенном/неверном параметре любой in-app инструмент возвращает понятное сообщение с именем параметра и подсказкой повторить с обязательными полями. - Контракт `required`/`optional` инструментов не изменился; значения не угадываются. - Тесты `ai-chat-tools.service.spec.ts` / `public-share-chat-tools.service.spec.ts` зелёные. --- _Связано с #188 (заметки об инструментах). Первоисточник — фидбэк «createComment — intermittent pageId undefined при пакетном вызове»._
claude_code added the bug label 2026-06-25 20:52:48 +03:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: vvzvlad/gitmost#190