feat(ai-chat): персист tool-error частей — упавшие тулы видны в истории и сохраняют текст ошибки #426
Reference in New Issue
Block a user
Delete Branch "feat/407-persist-tool-errors"
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?
Summary
Когда агентский тул бросает исключение (штатный режим для retryable-ошибок — throw в
ai-chat-tools.service.ts:442), ошибка показывалась модели вживую, но никогда не персистилась: в ai@6 упавший тул — этоtool-errorчасть вstep.content({type,toolCallId,toolName,input,error}), а не элементtoolResults. Итог:serializeStepsписал в колонкуtool_callsтолько вызовы+успешные результаты (ошибка терялась, оставался orphan tool-call без результата), аassistantPartsэмитил заглушку'Tool call did not complete.'— реальный текст ошибки терялся для мультиходового реплея, и модель на следующем ходу не знала, почему упало, и повторяла ту же ошибку. По анализу БД (см.docs/reading-ai-logs.md) наивный скан ошибок показывал 0 ошибок там, где UI показывал повторяющиеся красные фейлы.Исправление:
StepLikeрасширен полемcontent; новый хелперnormalizeToolError(Error/string/object → строка, обрезка через существующийcompactValue/лимиты — новый лимит не вводится).serializeSteps: на каждыйtool-errorпушит парный{toolName, error}тем же паттерном, что успешный{toolName, output}→ колонкаtool_callsфиксирует сбой и его причину.assistantParts: при наличии совпавшегоtool-errorэмититoutput-errorс реальным текстом ошибки; заглушка остаётся только для по-настоящему непарных вызовов (прерванных mid-step).docs/reading-ai-logs.mdпереписан под новую персистируемую форму + cutover-оговорка (строки до фикса сохраняют старую двухчастную форму).Обратно совместимо: старые строки читаются как раньше,
error-элемент аддитивен. Живой throw-путь (ai-chat-tools.service.ts:442) не тронут.closes #407
How verified
pnpm --filter ./apps/server exec jest src/core/ai-chat/ai-chat.service.spec.ts --maxWorkers=2→ 88 passed, 88 total (включая +4 новых: реплей несёт реальный errorText; прерванный вызов → заглушка; serializeSteps фиксирует{toolName,error}; 5000-символьная ошибка обрезается).tool-error/output-error/errorText) сверена внутренним ревью сnode_modules/ai/dist/index.d.ts(ai@6) — совпадает точно.prom-client-резолв в metrics.registry.ts — артефакт симлинка node_modules, к этому изменению не относится.)Внутренний цикл: 1 проход coder → внутреннее ревью (сверка с SDK-типами + прогон тестов) → замечаний нет.
Checklist
В ai@6 упавший тул — это tool-error часть в step.content ({type,toolCallId, toolName,input,error}), а не элемент toolResults. Раньше serializeSteps писал только toolCalls+toolResults (ошибка терялась, orphan tool-call без результата), а assistantParts эмитил заглушку 'Tool call did not complete.' (реальный текст ошибки терялся для мультиходового реплея — модель не знала, почему упало, и повторяла ошибку). - StepLike расширен полем content; новый хелпер normalizeToolError (Error/string/ object -> строка, обрезка через существующий compactValue/лимиты). - serializeSteps: на каждый tool-error пушит парный {toolName, error} тем же паттерном, что успешный {toolName, output} -> колонка tool_calls фиксирует сбой. - assistantParts: при наличии tool-error эмитит output-error с РЕАЛЬНЫМ текстом; заглушка остаётся только для по-настоящему непарных вызовов (прерванных). - docs/reading-ai-logs.md обновлён под новую форму + cutover-оговорка. Обратно совместимо: старые строки читаются как раньше, error-элемент аддитивен. closes #407 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Ревью — #426 · feat(ai-chat): персист tool-error частей (closes #407)
Вердикт: ПРАВКИ (CHANGES) — одна правка, и та в доке. Код отличный:
serializeSteps/assistantPartsчитают ai@6tool-errorизstep.content, фиксируют ошибку вtool_callsи реплеят реальный текст черезoutput-error;normalizeToolErrorустойчив; форма частей SDK подтверждена поai@6.0.134d.ts. Но переписанный док сам себе противоречит.Сделать — потом повторное ревью
reading-ai-logs.mdпротиворечит новомуerror-полю в секции «READ THIS» —docs/reading-ai-logs.md:66-67Строка 66 (заглавная секция «How tool calls are stored — READ THIS») утверждает: «The only keys that ever appear on an element are
toolName,input,output. There is nostate, noerrorText, notype». Но этот же PR добавляет ключerrorи ниже (стр. 98-219) подробно его описывает («New rows: query theerrorfield directly», «is_result … counts anerrorelement as a paired result»). Т.е. док-апдейт (заявленный в PR как выполненный) оставил самый заметный раздел фактически неверным для новых строк. Проверено против кода:serializeStepsреально пушит{toolName, error}(ai-chat.service.ts:2089-2093). Fix: на стр. 66-67 добавитьerrorкак возможный ключ для строк после #407 (напр. «…toolName,input,output, и (для брошенных ошибок, строки после #407)error»).Как проверено (гейт, clean-room
/home/claude/gm-ci)apps/servertsc (tsconfig.build) — PASS (0 ошибок).ai-chat.service.spec— 88/88 PASS (вкл. +4: реплей несёт реальный errorText; прерванный → заглушка; serializeSteps фиксирует{toolName,error}; 5000-симв. ошибка обрезается).apps/server/node_modules/ai/dist/index.d.ts(ai@6.0.134):tool-errorcontent-part несёт{type, toolCallId, toolName, input, error}(StaticToolError/DynamicToolError,toolCallId: string— required);state:'output-error'валиден только сerrorText: string. Читаемая PR-ом форма точна.else if (errorsById.has…)→ падает тест реального errorText; снять tool-error-цикл в serializeSteps → падает;normalizeToolError→ "" → падает 3 теста. Ядро (персист+реплей реальной ошибки) покрыто.result > error > stubкорректна (result/error взаимоисключающи по SDK); заглушка теперь только для реально прерванных (aborted mid-step); живой throw-путь (ai-chat-tools.service.ts:442) не тронут; error-текст ограничен существующимcompactValue(600 симв. / 200KB), новый лимит не вводится.⛔ DROP — кодеру НЕ делать (лог калибровки)
[speculative]medium [coherence] асимметрия фильтра tool-error:assistantPartsгардит&& part.toolCallId,serializeSteps— нет —ai-chat.service.ts:1809,2088— недостижимо: SDK-типы гарантируютtoolCallId: string(required, сверено в d.ts); и даже при мисматче serializeSteps аудитит, а assistantParts падает на stub — это корректно по назначению (id-less ошибку нельзя спарить для реплея), не потеря.[below-threshold]high [test-coverage]normalizeToolErrorветки object-with-message /String(non-string)без теста —ai-chat.service.ts:1760-1771— чистый хелпер, ядро мутационно-гардировано; ветки тривиальны (вернуть.message/String()), рисковый путь покрыт.[below-threshold]medium [test-coverage] нет теста round-tripoutput-errorчерезconvertToModelMessages— формаoutput-errorунаследована от уже проверенного в проде stub-пути (идентичный shape), не новый риск этого PR.[below-threshold]low [test-coverage] точная семантика обрезки (длина превью/маркер/граница 600) не заассерчена — переиспользуется предсуществующий протестированныйcompactValue; наличие обрезки покрыто.9 аспектов (security · stability · conventions · documentation · regressions · test-coverage · simplification · architecture · coherence). Security: персист error-текста — same-boundary (ошибка уже показывалась модели вживую и в UI); normalizeToolError без инъекций,
compactValue→stripNulChars→JSONB. Stability:normalizeToolErrorкрэш-безопасен даже при бросающем.message-геттере (внешний try-catch → graceful failed-insert). Escalate нет.Поправил единственную запрошенную правку (документация).
[documentation]
reading-ai-logs.mdпротиворечил новомуerror-полю — исправлено.Заглавный абзац секции «How tool calls are stored — READ THIS» (стр. 66-67) больше не утверждает, что ключей только три: теперь
errorявно указан как возможный ключ для брошенных ошибок на строках после #407 (со ссылкой на секцию «Hard failures»), и оговорено, что на pre-#407 строках брошенная ошибка вообще не имеет парного результата (тихий orphan). Заодно поправил два следствия: подсчёт инвокаций = элементы сoutputилиerror; пайринг — успех несётoutput, брошенная ошибка (post-#407) несётerror. Теперь заглавная секция согласована с детальным описанием ниже (стр. 98-219).Только markdown, кода не касалось. Готово к повторному ревью.
Ревью — #426 (fix-round) · doc-противоречие reading-ai-logs
Вердикт: OK (PASS). Правка внесена полно и согласованно; проверено — новых противоречий не внёс.
Что проверено (head
96faa282, дельта654ba9f2..96faa282)docs/reading-ai-logs.md; код (ai-chat.service.ts) не тронут (остаётся версией, что я уже подтвердил зелёной: tsc PASS, jest 88/88, форма SDK сверена).errorтеперь перечислен (для брошенных сбоев, строки после #407, со ссылкой на «Hard failures»); отдельно отмечены pre-#407 orphan'ы.outputилиerror» — теперь совпадает с предикатомis_resultниже (стр. 219, где error-элемент уже считался paired result); (2) pairing: success несётoutput, брошенный сбой (post-#407) несётerror.tool_calls(это UIMessage-поля, в БД не пишутся;error— пишется).DO прошлого раунда закрыт полностью, побочек нет. Апрув. Лейбл →
review/approved.