@ykurilov/agent-chat-core 0.63.2 → 0.64.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.64.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 99cded9: Подробности сбоя провайдера для админа. Ответ, упавший на провайдере, хранит `error.details`: провайдер, модель, код, причина одной строкой (до 300 знаков; ключи окружения, `Bearer`, значения `key=`/`token`/`api-key`/`password`, маски ключей со `*`, параметры адресов, `org-…`/`proj_…` и e-mail вычищены; модерация может повторять текст человека) и `resetsAt` — когда провайдер снимет предел (OpenRouter «resets 2026-11-01»; только будущее до двух лет, без пояса — UTC). В браузер подробности уходят только по новому праву `chatCapabilities` → `errorDetails: true` (по умолчанию `false`, при `modelDetails: false` тоже `false`) — в истории, потоке и при переподключении; остальным — только `error.kind`. `afterTurn` получает их всегда. Экран: подпись-функция `labels.errorText(error, plain)`, без неё или если она бросила исключение — прежний текст `labels.errors[kind]`; типы `ChatError`, `ChatErrorDetails`, помощник `customErrorText`. Хранилища: тип колонки `error` — `ChatError`, миграций нет.
8
+
3
9
  ## 0.63.2
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -82,8 +82,8 @@ import type { ChatStorage } from "@ykurilov/agent-chat-core";
82
82
  - Данные чата без записи: `service.getChatData(userId, chatId)` — копия, проверки владения как у `updateChatData`.
83
83
  - Проверка ответа виджета на сервере: `checkAnswer(answer, ctx)` у своего виджета и общая `agent.widgets.checkAnswer(call, answer, ctx)` (для всех виджетов без своей). Зовётся после схемы под замком чата. Исход: `undefined` — принять, `{ answer }` — принять с заменой, `{ refuse: "текст" }` — отказать (`WidgetAnswerRefusedError`, сервер — 409 `widget_answer_refused`, виджет остаётся открытым). `ctx` — `ownerId`, `chatId`, `messageId`, `toolName`, `toolCallId`, `input`, `subject?`, `chatData?`, `state` (записи ложатся в ответ вместе с результатом виджета, хук `onStateChanged` — с причиной `widgetAnswer`), `emitAppEvent` (событие приложения вместе с ответом виджета), `signal` (отмена по сроку или «Стопу»: приложение слушает его само). Срок — `agent.widgets.checkAnswerTimeoutMs` (5 000 мс, заметно меньше `requestTimeoutMs` страницы). Исключение — 500, ничего не записано, в журнале только имя ошибки; срок вышел — `WidgetAnswerCheckTimeoutError`, сервер — 504 `widget_check_timeout`. Чат закрыли во время проверки — `ChatClosedError`, ответ не пишется.
84
84
  - Набор виджетов по чату: `agent.widgets.enabledFor({ chat: { id, ownerId, data?, subject? } })` → имена включённых. Выключенных модель не видит (нет в инструментах запроса), их прошлые вызовы идут модели текстом. Синхронная, те же данные — тот же набор; смена набора сбрасывает кэш модели. Неизвестное имя — строка в журнал, исключение — ошибка хода. Подробно — [docs/widgets.md](../../docs/widgets.md).
85
- - Права человека в чате: `chatCapabilities({ userId, chat })` возвращает запреты (`edit`, `regenerate`, `retry`, `branches`, `modelChoice`, `answerEditing`, `chatList`, `compact`, `attachments`, `voice`, `reasoning`, `toolDetails`, `steer`, `modelDetails`; по умолчанию всё `true`; `modelDetails: false` выключает и `modelChoice`). Неизвестное право или не булево значение — `TypeError`. Запрещённое действие — `ActionForbiddenError(action)` до любой записи и до `beforeTurn`. За один вызов службы права спрашиваются не больше раза. История несёт `capabilities`. Вручную — `service.assertCapability(userId, chatId, capability)`.
86
- - Скрытие частей ответа: `reasoning: false` — мысли модели, `toolDetails: false` — служебные инструменты не уходят в историю, поток и повтор кусков после переподключения; хранилище, модель и `afterTurn` получают всё. Виджеты, браузерные инструменты, вызовы с запросом подтверждения и инструменты с `metadata: { showOnScreen: true }` в описании видны всегда. `modelDetails: false` убирает у чата истории `ownerId`, `modelId`, `providerId`, `reasoning`, у сообщений — `metadata.modelId`, `providerId`, `usage`, у частей и кусков — `providerMetadata`/`callProviderMetadata`/`resultProviderMetadata`, у `data-step-reset` — провайдера; отметки сжатия идут без текста сводки (`summary: ""`, `summaryTokens: 0`) в истории и `getCompactions`. Меню моделей — опция `modelMenu({ userId })`: `false` — `getModelMenu(userId)` бросает `ActionForbiddenError("modelDetails")`. История несёт `modelAcceptsImages` (видит ли модель чата картинки). Права — на того, кто открыл поток.
85
+ - Права человека в чате: `chatCapabilities({ userId, chat })` возвращает запреты (`edit`, `regenerate`, `retry`, `branches`, `modelChoice`, `answerEditing`, `chatList`, `compact`, `attachments`, `voice`, `reasoning`, `toolDetails`, `steer`, `modelDetails`, `errorDetails`; по умолчанию всё `true`, кроме `errorDetails`; `modelDetails: false` выключает и `modelChoice`, и `errorDetails`). Неизвестное право или не булево значение — `TypeError`. Запрещённое действие — `ActionForbiddenError(action)` до любой записи и до `beforeTurn`. За один вызов службы права спрашиваются не больше раза. История несёт `capabilities`. Вручную — `service.assertCapability(userId, chatId, capability)`.
86
+ - Скрытие частей ответа: `reasoning: false` — мысли модели, `toolDetails: false` — служебные инструменты не уходят в историю, поток и повтор кусков после переподключения; хранилище, модель и `afterTurn` получают всё. Виджеты, браузерные инструменты, вызовы с запросом подтверждения и инструменты с `metadata: { showOnScreen: true }` в описании видны всегда. `modelDetails: false` убирает у чата истории `ownerId`, `modelId`, `providerId`, `reasoning`, у сообщений — `metadata.modelId`, `providerId`, `usage`, у частей и кусков — `providerMetadata`/`callProviderMetadata`/`resultProviderMetadata`, у `data-step-reset` — провайдера; отметки сжатия идут без текста сводки (`summary: ""`, `summaryTokens: 0`) в истории и `getCompactions`. Подробности сбоя провайдера (`metadata.error.details`: `providerId`, `modelId`, `statusCode`, `reason` — одна строка до 300 знаков без секретов, параметров адресов, `org-…`/`proj_…` и e-mail, `resetsAt` — ISO, только будущее не дальше двух лет) хранятся всегда, а в историю и поток идут только при `errorDetails: true` (например, админу); без права — только `error.kind`. Ответ модерации в `reason` может повторять текст человека — отдавайте право только тому, кому этот текст можно видеть. Меню моделей — опция `modelMenu({ userId })`: `false` — `getModelMenu(userId)` бросает `ActionForbiddenError("modelDetails")`. История несёт `modelAcceptsImages` (видит ли модель чата картинки). Права — на того, кто открыл поток.
87
87
  - Закрытый чат: `service.closeChat({ ownerId, chatId, code, message })` и `reopenChat({ ownerId, chatId })` (события `chat-closed`, `chat-reopened`), из инструмента — `model.closeChat({ code, message })`. Закрытие — запись `$closed` на ветке: из инструмента — в ответе сразу при вызове, из кода — служебной записью в конце ветки (идёт ход — после него). Правка, «Переделать», выбор ветки и правка ответа до точки закрытия открывают чат; закрыть навсегда — правами (`chatCapabilities` видит `chat.closed`). Закрытие побеждает паузу: ждущие подтверждения и виджеты закрываются отказом, как при «Стопе». В закрытом чате отправка, «Повторить», подтверждения и виджеты — `ChatClosedError`; служебные сообщения пишутся без хода. `listChats` отдаёт `closed` — копию закрытия текущей ветки (`setChatClosed`); права и история берут закрытие с ветки, не записавшуюся копию дописывает следующее открытие истории. Закрытие чатов 0.27 переносится в запись лениво, при первой правке или смене ветки.
88
88
  - `beforeCreateChat({ userId, origin? })` — можно ли человеку завести новый чат (`createChat`): `{ code, message }` → `ChatCreationRefusedError`. Чаты объектов и импорт хук не спрашивают. У чата `closed?: { code, message }`, хранилище — необязательный `setChatClosed(chatId, closed | null)`.
89
89
  - Чат объекта: `service.openSubjectChat({ ownerId, key, data?, title?, greeting? })` → `{ chat, created }`, один чат на ключ у человека. Инструменты получают `model.subject`, итог хода — `turn.subject`. `listChats(userId, { subjects })` — по умолчанию без чатов объектов.
package/dist/access.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  accessRefusedResponse,
3
3
  allowedUser
4
- } from "./chunk-FBULM4V6.js";
5
- import "./chunk-FOAIG4DY.js";
4
+ } from "./chunk-5UUWSY6V.js";
5
+ import "./chunk-CVRDB3NM.js";
6
6
  export {
7
7
  accessRefusedResponse,
8
8
  allowedUser
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  normalizeTurnRefusal
3
- } from "./chunk-FOAIG4DY.js";
3
+ } from "./chunk-CVRDB3NM.js";
4
4
 
5
5
  // src/access/resolved-user.ts
6
6
  function allowedUser(resolved) {
@@ -17,4 +17,4 @@ export {
17
17
  allowedUser,
18
18
  accessRefusedResponse
19
19
  };
20
- //# sourceMappingURL=chunk-FBULM4V6.js.map
20
+ //# sourceMappingURL=chunk-5UUWSY6V.js.map
@@ -53,4 +53,4 @@ export {
53
53
  TurnRefusedError,
54
54
  validateAppEvent
55
55
  };
56
- //# sourceMappingURL=chunk-FOAIG4DY.js.map
56
+ //# sourceMappingURL=chunk-CVRDB3NM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/turn-hooks/turn-hooks.ts"],"sourcesContent":["import type { AgentChannel, Chat, ChatError, ChatSubject, FinalAnswerStatus, StoredMessage, TokenUsage } from \"../conversation/types\";\n\n/** Что человек делает с чатом, когда приложение решает, пускать ли ход. */\nexport type TurnAction = \"send\" | \"edit\" | \"retry\" | \"regenerate\" | \"approval\" | \"widget\" | \"notice\";\n\n/** Отказ приложения в ходе: код для программы, текст для человека. */\nexport interface TurnRefusal {\n /** Латиница в нижнем регистре, цифры и `.`, `_`, `-`; от 1 до 64 знаков. */\n code: string;\n /** До 300 знаков; длиннее — обрезается. */\n message: string;\n}\n\n/**\n * Сотрудник, который действует в чужом чате (копия чата человека в песочнице). Пока он есть у хода (`model.staff`),\n * инструменты приложения данные `ownerId` только читают: любая запись в них запрещена — это обязанность инструментов.\n */\nexport interface StaffActor {\n id: string;\n}\n\n/** Событие приложения в ответе агента: `type` — 1–100 знаков, `data` — JSON до 16 КБ. */\nexport interface AppEvent {\n type: string;\n data?: unknown;\n}\n\n/** Записать сообщение человека без хода: агент промолчит (рамки дня, пауза сценария). */\nexport interface TurnHold {\n hold: true;\n}\n\n/** Ход с готовым текстом, без модели: путь `reply` из `deliverNotices`. */\nexport interface TurnReply {\n reply: { text: string };\n}\n\n/** Что `beforeTurn` может решить, кроме «пускать»: отказ, «записать без хода» или «ответить готовым текстом». */\nexport type BeforeTurnDecision = TurnRefusal | TurnHold | TurnReply;\n\n/**\n * Проверка приложения перед ходом: вернуть отказ — ход не начнётся и ничего не запишется. `{ hold: true }` и\n * `{ reply }` — с 0.49 (сценарии): сообщение записывается, хода модели нет.\n */\nexport type BeforeTurnHandler = (input: {\n userId: string;\n chat: Chat;\n action: TurnAction;\n /** Текст сообщения человека (`send`, `edit`). */\n text?: string;\n /** Id сообщения человека (`send`, `edit`): по нему приложение свяжет свой резерв с ходом. */\n messageId?: string;\n /**\n * Ход на пачку сообщений человека (`sendBurstMessage`): id всех её сообщений по порядку, последнее — `messageId`, а\n * `text` — их тексты через пустую строку. Одно решение на всю пачку; счёт сообщений — по длине списка.\n */\n burstMessageIds?: string[];\n /**\n * Ход на пачку начал сам сервер после перезапуска или за упавшую копию (`answerAfterRestart`): вызова, который ждёт\n * ответ или отказ, нет. Отказ дойдёт только до журнала — сообщить человеку может само приложение.\n */\n unattended?: true;\n} & AbandonedAnswers) => Promise<BeforeTurnDecision | void> | BeforeTurnDecision | void;\n\n/**\n * Ответы агента, которые уйдут с текущей ветки, если ход начнётся (`regenerate`, `edit`, `retry`): по ним приложение откатывает\n * побочные дела брошенных ответов (правки формы и т. п.). Ход не начался (`turnNotStarted`) — ветка прежняя, ничего не\n * ушло: тот хук получает те же поля. Посчитаны по ветке на миг проверки; сообщения нет на ветке — полей нет.\n */\nexport interface AbandonedAnswers {\n /** Ответы агента, которые уходят с ветки, сверху вниз; у `edit` без ответов ниже — пустой список. */\n abandonedAnswerIds?: string[];\n /**\n * Ответ, который заменит новый: переделываемый (`regenerate`), ответ на правленый вопрос (`edit`), если был, или\n * оборванный ответ, который «Повторить» (`retry`) заменяет соседом. У «Повторить» вопроса без ответа полей нет.\n */\n previousAnswerId?: string;\n}\n\n/** Почему ход не начался после того, как `beforeTurn` его пропустил. */\nexport type TurnNotStartedReason =\n /** В чате уже идёт ответ, или служба останавливается (`flush`). */\n | \"busy\"\n /** У человека уже идёт предел одновременных ходов. */\n | \"too_many_turns\"\n /** Лимит расходов исчерпан. */\n | \"spending_limit\"\n /** Сообщение с таким id уже есть (повтор запроса). */\n | \"duplicate_message\"\n /** Запрос неверен: сообщение, файл, решение, ответ на виджет. */\n | \"invalid\"\n /** Чат закрыли, пока ход запускался (`closeChat` между проверкой и началом хода). */\n | \"chat_closed\"\n /** Хода и не будет: проверка без хода (`assertTurnAllowed`), ответ ждёт ещё решений, служебное сообщение без ответа. */\n | \"no_turn\"\n /** Сбой (база, сеть): ход не начат. */\n | \"error\";\n\n/**\n * Ход не начался, хотя `beforeTurn` его пропустил: приложение снимает свой резерв. Каждый пропуск `beforeTurn`\n * закрывается ровно одним: ходом (`afterTurn`) или этим хуком. Ошибка хода наружу прежняя; бросок хука — в журнал.\n */\nexport type TurnNotStartedHandler = (input: {\n userId: string;\n chat: Chat;\n action: TurnAction;\n messageId?: string;\n text?: string;\n /** Пачка сообщений, как в `beforeTurn`. */\n burstMessageIds?: string[];\n /** Ход без вызова, как в `beforeTurn`: об отказе (например, `spending_limit`) человеку может сказать только приложение. */\n unattended?: true;\n reason: TurnNotStartedReason;\n} & AbandonedAnswers) => Promise<void> | void;\n\n/** Кто остановил ход: человек («Стоп»), общий срок хода (`agent.turnTimeoutMs`) или остановка службы (`flush`). */\nexport type TurnStopReason = \"user\" | \"timeout\" | \"shutdown\";\n\n/** Итог законченного хода для приложения. */\nexport interface TurnResult {\n ownerId: string;\n /** Кто сделал ход: владелец чата, а в копии у сотрудника — сотрудник. */\n actorId: string;\n chatId: string;\n messageId: string;\n status: FinalAnswerStatus;\n /** Кто остановил ход; только у `status: \"stopped\"` живого хода (у доделанного после перезапуска нет). */\n stopReason?: TurnStopReason;\n /** Ответ упёрся в предел токенов (`finishReason: \"length\"`, см. `agent.maxOutputTokens`) и оборван. */\n lengthCut?: true;\n /**\n * Ход дошёл до предела шагов (`agent.maxSteps`): последний шаг был итоговым, без инструментов. Это «дошли до потолка»,\n * а не обязательно «недоделали»: модель могла закончить дело как раз на последнем шаге. Статус обычный (`complete`).\n * В журнале SDK — строка `[предел шагов]`.\n */\n stepLimit?: true;\n /** Текстовые части ответа подряд. */\n text: string;\n /** Части ответа, как они записаны, кроме служебной части с блоками контекста (`data-frozen-context`). */\n parts: StoredMessage[\"parts\"];\n usage: TokenUsage | null;\n /** Цена хода в долларах; `null` — у модели нет цены. */\n costUsd: number | null;\n /** Вид сбоя и подробности провайдера (`details`): хук на сервере получает их всегда. */\n error?: ChatError;\n /**\n * Каким действием начат ход (см. `TurnAction`). `null` — итог доделан после перезапуска (`recovered`), а ядро не\n * помнит действие; ход по служебному сообщению и тогда — `notice`.\n */\n trigger: TurnAction | null;\n channel: AgentChannel;\n /**\n * Ход сделал сотрудник в копии чата человека (песочница): `ownerId` и `subject` — человека и его исходного чата.\n * При `staff` в данные человека не писать.\n */\n staff?: StaffActor;\n /** Объект чата (урок, заказ…): ключ и данные чата объекта. Нет — обычный чат. */\n subject?: ChatSubject;\n /** Данные чата (`Chat.data`) на момент итога; нет данных — поля нет. */\n chatData?: Record<string, unknown>;\n /** Итог доделан после перезапуска сервера, а не сразу после хода. */\n recovered: boolean;\n}\n\n/**\n * Хук итога хода: зовётся после записи итога и расхода, до `finish` клиенту; пока он идёт, чат занят. Получает любой\n * итог: законченный, остановленный, с ошибкой, прерванный (в том числе восстановлением после перезапуска) и ждущий\n * человека. Ждущий ответ (`awaiting_approval`) после решения человека продолжается тем же сообщением — хук позовут\n * второй раз, с новым статусом. Хук может быть вызван повторно (повтор после сбоя, доделка после перезапуска):\n * ключ повтора — `messageId` + `status`. Возвращённые `events` ложатся в конец ответа частями `data-app-event`.\n */\nexport type AfterTurnHandler = (\n result: TurnResult,\n) => Promise<{ events?: AppEvent[] } | void> | { events?: AppEvent[] } | void;\n\nexport const TURN_REFUSAL_MESSAGE_MAX_LENGTH = 300;\nexport const APP_EVENT_TYPE_MAX_LENGTH = 100;\nexport const APP_EVENT_DATA_MAX_BYTES = 16 * 1024;\n\nconst TURN_REFUSAL_CODE = /^[a-z0-9_.-]{1,64}$/;\n\nexport function isTurnRefusalCode(code: unknown): code is string {\n return typeof code === \"string\" && TURN_REFUSAL_CODE.test(code);\n}\n\n/** Отказ по правилам: код проверен (иначе `TypeError`), текст обрезан до 300 знаков. */\nexport function normalizeTurnRefusal(refusal: TurnRefusal): TurnRefusal {\n if (!isTurnRefusalCode(refusal?.code)) throw new TypeError(\"Код отказа — [a-z0-9_.-], от 1 до 64 знаков\");\n if (typeof refusal.message !== \"string\") throw new TypeError(\"Текст отказа — строка\");\n // По знакам, а не по половинкам: эмодзи на границе не рвётся.\n const message = Array.from(refusal.message).slice(0, TURN_REFUSAL_MESSAGE_MAX_LENGTH).join(\"\");\n return { code: refusal.code, message };\n}\n\n/** Приложение отказало в ходе: ход не начат, ничего не записано. */\nexport class TurnRefusedError extends Error {\n readonly code: string;\n\n constructor(refusal: TurnRefusal) {\n const normalized = normalizeTurnRefusal(refusal);\n super(normalized.message);\n this.name = \"TurnRefusedError\";\n this.code = normalized.code;\n }\n\n get refusal(): TurnRefusal {\n return { code: this.code, message: this.message };\n }\n}\n\n/** Событие по правилам или `TypeError`. Отдаёт копию: данные прошли через JSON. */\nexport function validateAppEvent(event: unknown): AppEvent {\n if (event === null || typeof event !== \"object\") throw new TypeError(\"Событие приложения — объект { type, data? }\");\n const { type, data } = event as { type?: unknown; data?: unknown };\n if (typeof type !== \"string\" || type.length < 1 || type.length > APP_EVENT_TYPE_MAX_LENGTH) {\n throw new TypeError(`Тип события приложения — строка от 1 до ${APP_EVENT_TYPE_MAX_LENGTH} знаков`);\n }\n if (data === undefined) return { type };\n let json: string | undefined;\n try {\n json = JSON.stringify(data);\n } catch {\n json = undefined;\n }\n if (json === undefined) throw new TypeError(\"Данные события приложения — значение JSON\");\n if (new TextEncoder().encode(json).length > APP_EVENT_DATA_MAX_BYTES) {\n throw new TypeError(`Данные события приложения больше ${APP_EVENT_DATA_MAX_BYTES} байт`);\n }\n return { type, data: JSON.parse(json) };\n}\n"],"mappings":";AA+KO,IAAM,kCAAkC;AACxC,IAAM,4BAA4B;AAClC,IAAM,2BAA2B,KAAK;AAE7C,IAAM,oBAAoB;AAEnB,SAAS,kBAAkB,MAA+B;AAC/D,SAAO,OAAO,SAAS,YAAY,kBAAkB,KAAK,IAAI;AAChE;AAGO,SAAS,qBAAqB,SAAmC;AACtE,MAAI,CAAC,kBAAkB,SAAS,IAAI,EAAG,OAAM,IAAI,UAAU,iJAA6C;AACxG,MAAI,OAAO,QAAQ,YAAY,SAAU,OAAM,IAAI,UAAU,iHAAuB;AAEpF,QAAM,UAAU,MAAM,KAAK,QAAQ,OAAO,EAAE,MAAM,GAAG,+BAA+B,EAAE,KAAK,EAAE;AAC7F,SAAO,EAAE,MAAM,QAAQ,MAAM,QAAQ;AACvC;AAGO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EACjC;AAAA,EAET,YAAY,SAAsB;AAChC,UAAM,aAAa,qBAAqB,OAAO;AAC/C,UAAM,WAAW,OAAO;AACxB,SAAK,OAAO;AACZ,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EAEA,IAAI,UAAuB;AACzB,WAAO,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,QAAQ;AAAA,EAClD;AACF;AAGO,SAAS,iBAAiB,OAA0B;AACzD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,OAAM,IAAI,UAAU,qKAA6C;AAClH,QAAM,EAAE,MAAM,KAAK,IAAI;AACvB,MAAI,OAAO,SAAS,YAAY,KAAK,SAAS,KAAK,KAAK,SAAS,2BAA2B;AAC1F,UAAM,IAAI,UAAU,sMAA2C,yBAAyB,uCAAS;AAAA,EACnG;AACA,MAAI,SAAS,OAAW,QAAO,EAAE,KAAK;AACtC,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,SAAS,OAAW,OAAM,IAAI,UAAU,2MAA2C;AACvF,MAAI,IAAI,YAAY,EAAE,OAAO,IAAI,EAAE,SAAS,0BAA0B;AACpE,UAAM,IAAI,UAAU,qLAAoC,wBAAwB,2BAAO;AAAA,EACzF;AACA,SAAO,EAAE,MAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AACxC;","names":[]}
@@ -3,7 +3,7 @@ import {
3
3
  TurnRefusedError,
4
4
  normalizeTurnRefusal,
5
5
  validateAppEvent
6
- } from "./chunk-FOAIG4DY.js";
6
+ } from "./chunk-CVRDB3NM.js";
7
7
 
8
8
  // src/conversation/types.ts
9
9
  var CURRENT_FORMAT_VERSION = 1;
@@ -525,7 +525,10 @@ var MIN_SECRET_LENGTH = 8;
525
525
  var HIDDEN = "[\u0441\u043A\u0440\u044B\u0442\u043E]";
526
526
  var SECRET_FIELD = String.raw`[\w-]*(?:password|passwd|secret|token|api[_-]?key|(?:secret|private|access)[_-]?key)(?![\w-])`;
527
527
  var FIELD_VALUE = String.raw`(?!\d+(?![^&\s"',;]))(?:"[^"]*"|'[^']*'|[^&\s"',;]+)`;
528
+ var SECRET_LIKE = String.raw`(?=[^\s"',;]*\d)(?=[^\s"',;]*[A-Za-z])[^\s"',;]{8,}`;
528
529
  var KNOWN_KEY_PATTERNS = [
530
+ // Маска ключа целиком: провайдер показывает начало и последние знаки ключа.
531
+ /[\w-]*\*{4,}[\w-]*/g,
529
532
  /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
530
533
  /\bsk-[A-Za-z0-9_-]{8,}/g,
531
534
  /\b[spr]k_(?:live|test)_[A-Za-z0-9]{8,}/g,
@@ -547,7 +550,7 @@ function scrubSecrets(text, knownSecrets = []) {
547
550
  if (secret && secret.length >= MIN_SECRET_LENGTH) scrubbed = scrubbed.split(secret).join(HIDDEN);
548
551
  }
549
552
  for (const pattern of KNOWN_KEY_PATTERNS) scrubbed = scrubbed.replace(pattern, HIDDEN);
550
- return scrubbed.replace(URL_PASSWORD, `$1:${HIDDEN}@`).replace(/\b((?:Proxy-)?Authorization\s*[:=]\s*)(?:(Basic|Bearer|Token|Digest)\s+)?[^\s"',;]+/gi, (_, head, scheme) => `${head}${scheme ? `${scheme} ` : ""}${HIDDEN}`).replace(/\bBearer\s+(?!\[скрыто\])[^\s"',;]+/g, `Bearer ${HIDDEN}`).replace(/([?&;]key=)[^&\s"]+/g, `$1${HIDDEN}`).replace(new RegExp(String.raw`("${SECRET_FIELD}"\s*:\s*)"[^"]*"`, "gi"), `$1"${HIDDEN}"`).replace(
553
+ return scrubbed.replace(URL_PASSWORD, `$1:${HIDDEN}@`).replace(/\b((?:Proxy-)?Authorization\s*[:=]\s*)(?:(Basic|Bearer|Token|Digest)\s+)?[^\s"',;]+/gi, (_, head, scheme) => `${head}${scheme ? `${scheme} ` : ""}${HIDDEN}`).replace(/\b(Bearer\s+)(?!\[скрыто\])[^\s"',;]+/gi, `$1${HIDDEN}`).replace(/([?&;]key=)[^&\s"]+/g, `$1${HIDDEN}`).replace(new RegExp(String.raw`(?<![\w-])(key\s*[:=]\s*)${SECRET_LIKE}`, "gi"), `$1${HIDDEN}`).replace(new RegExp(String.raw`(?<![\w-])((?:token|api[_-]?key|secret|password|passwd|key)\s+)${SECRET_LIKE}`, "gi"), `$1${HIDDEN}`).replace(new RegExp(String.raw`("${SECRET_FIELD}"\s*:\s*)"[^"]*"`, "gi"), `$1"${HIDDEN}"`).replace(
551
554
  new RegExp(String.raw`(?<![\w-])(${SECRET_FIELD}\s*[:=]\s*)(?!"\[скрыто\]")(${FIELD_VALUE})`, "gi"),
552
555
  (_, head, value) => /^["']/.test(value) ? `${head}${value[0]}${HIDDEN}${value[0]}` : `${head}${HIDDEN}`
553
556
  );
@@ -5326,6 +5329,100 @@ ${summary}
5326
5329
  </answer_progress>`;
5327
5330
  }
5328
5331
 
5332
+ // src/provider-errors/provider-error-details.ts
5333
+ import { APICallError as APICallError4 } from "ai";
5334
+ var MAX_REASON_LENGTH = 300;
5335
+ var MAX_RESET_AHEAD_MS = 2 * 366 * 24 * 60 * 60 * 1e3;
5336
+ function providerErrorDetails(error, context) {
5337
+ const cause = underlyingError(error);
5338
+ const statusCode = errorStatusCode(cause);
5339
+ const { message, metadata } = providerMessageOf(cause);
5340
+ const reason = message === void 0 ? void 0 : shownReason(message, environmentSecrets(context.env, context.catalog));
5341
+ const nowMs = (context.now ?? Date.now)();
5342
+ const found = resetFromMetadata(metadata, nowMs) ?? (message === void 0 ? void 0 : resetFromText(message));
5343
+ const resetsAt = found !== void 0 && found > nowMs && found - nowMs <= MAX_RESET_AHEAD_MS ? new Date(found).toISOString() : void 0;
5344
+ return {
5345
+ providerId: context.providerId,
5346
+ modelId: context.modelId,
5347
+ ...statusCode !== void 0 && { statusCode },
5348
+ ...reason !== void 0 && reason !== "" && { reason },
5349
+ ...resetsAt !== void 0 && { resetsAt }
5350
+ };
5351
+ }
5352
+ function providerMessageOf(error) {
5353
+ if (APICallError4.isInstance(error)) {
5354
+ const fromBody = typeof error.responseBody === "string" ? fromJson(error.responseBody) : void 0;
5355
+ if (fromBody?.message !== void 0) return fromBody;
5356
+ const raw = [error.responseBody, error.message].find(isText);
5357
+ return { ...raw !== void 0 && { message: raw }, metadata: fromBody?.metadata };
5358
+ }
5359
+ if (typeof error === "string") return isText(error) ? { message: error } : {};
5360
+ return fromObject(error) ?? {};
5361
+ }
5362
+ function fromJson(body) {
5363
+ try {
5364
+ return fromObject(JSON.parse(body));
5365
+ } catch {
5366
+ return void 0;
5367
+ }
5368
+ }
5369
+ function fromObject(value) {
5370
+ if (typeof value !== "object" || value === null) return void 0;
5371
+ const fields = value;
5372
+ if (typeof fields.error === "object" && fields.error !== null) {
5373
+ const nested = fields.error;
5374
+ if (isText(nested.message)) return { message: nested.message, metadata: nested.metadata };
5375
+ }
5376
+ if (isText(fields.error)) return { message: fields.error, metadata: fields.metadata };
5377
+ if (isText(fields.message)) return { message: fields.message, metadata: fields.metadata };
5378
+ return void 0;
5379
+ }
5380
+ var isText = (value) => typeof value === "string" && value.trim() !== "";
5381
+ var URL_WITH_QUERY = /(https?:\/\/[^\s?#"'<>]+)[?#][^\s"'<>]*/g;
5382
+ var ACCOUNT_IDS = [/\borg-[A-Za-z0-9]{6,}/g, /\bproj_[A-Za-z0-9]{6,}/g, /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g];
5383
+ function shownReason(message, knownSecrets) {
5384
+ const oneLine2 = message.replace(/\s*[\r\n]+\s*/g, " ").trim();
5385
+ let shown = scrubSecrets(oneLine2.replace(URL_WITH_QUERY, "$1"), knownSecrets);
5386
+ for (const pattern of ACCOUNT_IDS) shown = shown.replace(pattern, "[\u0441\u043A\u0440\u044B\u0442\u043E]");
5387
+ return cutAtCharBoundary(shown, MAX_REASON_LENGTH);
5388
+ }
5389
+ var RESET_FIELD = /reset/i;
5390
+ var RELATIVE_FIELD = /sec|after|in$|_in_|ms$|_ms|Ms$/i;
5391
+ var MS_FIELD = /ms$|_ms|Ms$/;
5392
+ var MIN_UNIX_SECONDS = 1e9;
5393
+ function resetFromMetadata(metadata, nowMs) {
5394
+ if (typeof metadata !== "object" || metadata === null) return void 0;
5395
+ for (const [key, value] of Object.entries(metadata)) {
5396
+ if (!RESET_FIELD.test(key)) continue;
5397
+ const parsed = typeof value === "number" ? fromNumber(key, value, nowMs) : typeof value === "string" ? fromDateText(value) : void 0;
5398
+ if (parsed !== void 0) return parsed;
5399
+ }
5400
+ return void 0;
5401
+ }
5402
+ function fromNumber(key, value, nowMs) {
5403
+ if (!Number.isFinite(value) || value <= 0) return void 0;
5404
+ if (RELATIVE_FIELD.test(key) || value < MIN_UNIX_SECONDS) return nowMs + (MS_FIELD.test(key) ? value : value * 1e3);
5405
+ return value < 1e12 ? value * 1e3 : value;
5406
+ }
5407
+ var LIMIT_MESSAGE_START = /^[^'"«“]{0,80}?\b(?:limit|budget|quota)\b/i;
5408
+ var QUOTED = /'[^']*'|"[^"]*"|«[^»]*»|“[^”]*”/g;
5409
+ var RESET_IN_TEXT = /\bresets?\s+(?:at\s+|on\s+)?(\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?)/i;
5410
+ function resetFromText(message) {
5411
+ if (!LIMIT_MESSAGE_START.test(message)) return void 0;
5412
+ const found = RESET_IN_TEXT.exec(message.replace(QUOTED, " "))?.[1];
5413
+ return found === void 0 ? void 0 : fromDateText(found);
5414
+ }
5415
+ function fromDateText(text) {
5416
+ const trimmed = text.trim();
5417
+ const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)(Z|[+-]\d{2}:?\d{2})?)?$/i.exec(trimmed);
5418
+ if (!match) return void 0;
5419
+ const [, year, month, day, time, zone] = match;
5420
+ const calendarDay = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
5421
+ if (calendarDay.getUTCMonth() !== Number(month) - 1 || calendarDay.getUTCDate() !== Number(day)) return void 0;
5422
+ const parsed = Date.parse(`${year}-${month}-${day}${time === void 0 ? "" : `T${time}${zone ?? "Z"}`}`);
5423
+ return Number.isNaN(parsed) ? void 0 : parsed;
5424
+ }
5425
+
5329
5426
  // src/agent-run/answer-saver.ts
5330
5427
  import { readUIMessageStream } from "ai";
5331
5428
 
@@ -6608,6 +6705,14 @@ async function runTurn(turnInput, emitRaw) {
6608
6705
  let errors = [];
6609
6706
  let terminal = [];
6610
6707
  let errorKind;
6708
+ let errorDetails;
6709
+ const detailsOf2 = (error2, failedProviderId) => providerErrorDetails(error2, {
6710
+ providerId: failedProviderId,
6711
+ modelId,
6712
+ env: input.env,
6713
+ catalog: input.catalog,
6714
+ now: () => (input.now ?? (() => /* @__PURE__ */ new Date()))().getTime()
6715
+ });
6611
6716
  let providerId = input.providerId;
6612
6717
  let answeredProviderId = input.providerId;
6613
6718
  let viewer;
@@ -6650,6 +6755,7 @@ async function runTurn(turnInput, emitRaw) {
6650
6755
  });
6651
6756
  const fallbackProviderIds = allFallbackIds.filter((candidate) => outages.pausedKind(candidate) === void 0);
6652
6757
  let firstOutage;
6758
+ let firstOutageDetails;
6653
6759
  const primaryPaused = outages.admit(input.providerId);
6654
6760
  if (primaryPaused !== void 0 && fallbackProviderIds[0] !== void 0) {
6655
6761
  providerId = fallbackProviderIds[0];
@@ -6746,7 +6852,9 @@ async function runTurn(turnInput, emitRaw) {
6746
6852
  } catch (error2) {
6747
6853
  if (error2 instanceof MissingProviderKeyError) logError(error2.message);
6748
6854
  if (!input.abortSignal.aborted) {
6749
- errorKind = errorKindForPerson(classify(error2), firstOutage);
6855
+ const kind = classify(error2);
6856
+ errorKind = errorKindForPerson(kind, firstOutage);
6857
+ errorDetails = errorKind === firstOutage ? firstOutageDetails : detailsOf2(error2, providerId);
6750
6858
  errors = [{ type: "error", errorText: errorKind }];
6751
6859
  }
6752
6860
  break;
@@ -6790,7 +6898,10 @@ async function runTurn(turnInput, emitRaw) {
6790
6898
  let next = stepsDone < maxSteps ? decideNextAttempt(result, nextState()) : { action: "fail" };
6791
6899
  const outage = isProviderOutage(result.kind) ? result.kind : void 0;
6792
6900
  if (outage) {
6793
- if (providerId === input.providerId) firstOutage ??= outage;
6901
+ if (providerId === input.providerId && firstOutage === void 0) {
6902
+ firstOutage = outage;
6903
+ firstOutageDetails = detailsOf2(result.error, providerId);
6904
+ }
6794
6905
  outages.report({
6795
6906
  providerId,
6796
6907
  kind: outage,
@@ -6879,6 +6990,7 @@ async function runTurn(turnInput, emitRaw) {
6879
6990
  continue;
6880
6991
  }
6881
6992
  errorKind = errorKindForPerson(result.kind, firstOutage);
6993
+ errorDetails = errorKind === firstOutage ? firstOutageDetails : detailsOf2(result.error, providerId);
6882
6994
  errors = errorKind === result.kind ? attempt.errors : [{ type: "error", errorText: errorKind }];
6883
6995
  terminal = attempt.terminal;
6884
6996
  logError(
@@ -6916,6 +7028,7 @@ async function runTurn(turnInput, emitRaw) {
6916
7028
  if (!input.abortSignal.aborted) {
6917
7029
  if (error2 instanceof AgentViewerError) logError(`[agent.viewer] \u0447\u0430\u0442=${input.chatId} \u0445\u043E\u0434 \u043D\u0435 \u043D\u0430\u0447\u0430\u0442: ${error2.message}`);
6918
7030
  errorKind = error2 instanceof AgentViewerError ? "other" : classifyProviderError(error2, void 0);
7031
+ errorDetails = void 0;
6919
7032
  errors = [{ type: "error", errorText: errorKind }];
6920
7033
  }
6921
7034
  }
@@ -6986,7 +7099,7 @@ async function runTurn(turnInput, emitRaw) {
6986
7099
  for (const chunk of heldText?.release() ?? []) emitShown(chunk);
6987
7100
  turnUsage.closeAttempt();
6988
7101
  const usage = messageUsage();
6989
- const error = status === "error" ? { kind: errorKind ?? "other" } : null;
7102
+ const error = status === "error" ? { kind: errorKind ?? "other", ...errorDetails && { details: errorDetails } } : null;
6990
7103
  const answeredByBackup = answeredProviderId !== input.providerId && { providerId: answeredProviderId };
6991
7104
  let finished = false;
6992
7105
  if (!takeover.signal.aborted) {
@@ -9419,10 +9532,12 @@ var CHAT_CAPABILITIES = [
9419
9532
  "reasoning",
9420
9533
  "toolDetails",
9421
9534
  "steer",
9422
- "modelDetails"
9535
+ "modelDetails",
9536
+ "errorDetails"
9423
9537
  ];
9538
+ var OFF_BY_DEFAULT = /* @__PURE__ */ new Set(["errorDetails"]);
9424
9539
  function resolveChatCapabilities(given) {
9425
- const resolved = Object.fromEntries(CHAT_CAPABILITIES.map((capability) => [capability, true]));
9540
+ const resolved = Object.fromEntries(CHAT_CAPABILITIES.map((capability) => [capability, !OFF_BY_DEFAULT.has(capability)]));
9426
9541
  if (given === void 0 || given === null) return resolved;
9427
9542
  if (typeof given !== "object") throw new TypeError("\u041F\u0440\u0430\u0432\u0430 \u0447\u0430\u0442\u0430 \u2014 \u043E\u0431\u044A\u0435\u043A\u0442 { \u043F\u0440\u0430\u0432\u043E: boolean }");
9428
9543
  const unknown = Object.keys(given).find((key) => !CHAT_CAPABILITIES.includes(key));
@@ -9433,7 +9548,10 @@ function resolveChatCapabilities(given) {
9433
9548
  if (typeof value !== "boolean") throw new TypeError(`\u041F\u0440\u0430\u0432\u043E \xAB${capability}\xBB \u2014 true \u0438\u043B\u0438 false`);
9434
9549
  resolved[capability] = value;
9435
9550
  }
9436
- if (!resolved.modelDetails) resolved.modelChoice = false;
9551
+ if (!resolved.modelDetails) {
9552
+ resolved.modelChoice = false;
9553
+ resolved.errorDetails = false;
9554
+ }
9437
9555
  return resolved;
9438
9556
  }
9439
9557
  var ActionForbiddenError = class extends Error {
@@ -11465,6 +11583,12 @@ var SHOWS_EVERYTHING = {
11465
11583
  };
11466
11584
  var MODEL_METADATA_KEYS = ["modelId", "providerId", "usage"];
11467
11585
  var MODEL_CHAT_KEYS = ["ownerId", "modelId", "providerId", "reasoning"];
11586
+ var withoutErrorDetails = (metadata) => {
11587
+ const error = metadata.error;
11588
+ if (typeof error !== "object" || error === null || !("details" in error)) return metadata;
11589
+ const { details: _details, ...rest } = error;
11590
+ return { ...metadata, error: rest };
11591
+ };
11468
11592
  var without = (carrier, keys) => {
11469
11593
  if (!keys.some((key) => key in carrier)) return carrier;
11470
11594
  const rest = { ...carrier };
@@ -11502,10 +11626,15 @@ function clientVisibilityOf(capabilities, sdkScreenTool) {
11502
11626
  const hidesReasoning = !capabilities.reasoning;
11503
11627
  const hidesTools = !capabilities.toolDetails;
11504
11628
  const hidesModel = capabilities.modelDetails === false;
11505
- if (!hidesReasoning && !hidesTools && !hidesModel) return SHOWS_EVERYTHING;
11629
+ const hidesErrorDetails = hidesModel || capabilities.errorDetails !== true;
11630
+ if (!hidesReasoning && !hidesTools && !hidesModel && !hidesErrorDetails) return SHOWS_EVERYTHING;
11506
11631
  const hidesCall = (toolName, call) => hidesTools && !sdkScreenTool(toolName) && !markedForScreen(call);
11507
11632
  const hiddenMetadataKeys = [...hidesReasoning ? ["reasoningDurationsMs"] : [], ...hidesModel ? MODEL_METADATA_KEYS : []];
11508
- const visibleMetadata = (metadata) => typeof metadata === "object" && metadata !== null ? without(metadata, hiddenMetadataKeys) : metadata;
11633
+ const visibleMetadata = (metadata) => {
11634
+ if (typeof metadata !== "object" || metadata === null) return metadata;
11635
+ const shown = without(metadata, hiddenMetadataKeys);
11636
+ return hidesErrorDetails ? withoutErrorDetails(shown) : shown;
11637
+ };
11509
11638
  const withoutProviderData2 = (carrier) => hidesModel ? without(carrier, PROVIDER_METADATA_KEYS) : hidesReasoning ? withoutReasoningDetails(carrier) : carrier;
11510
11639
  return {
11511
11640
  showsEverything: false,
@@ -12216,10 +12345,18 @@ function createChatService(input) {
12216
12345
  scope.capabilities ??= askCapabilities(userId, chat);
12217
12346
  return scope.capabilities;
12218
12347
  }
12348
+ let warnedErrorDetailsWithoutModel = false;
12349
+ function warnErrorDetailsWithoutModel(given) {
12350
+ if (warnedErrorDetailsWithoutModel || given?.errorDetails !== true || given.modelDetails !== false) return;
12351
+ warnedErrorDetailsWithoutModel = true;
12352
+ logError("chatCapabilities: errorDetails: true \u043F\u0440\u0438 modelDetails: false \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u2014 \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438 \u0441\u0431\u043E\u044F \u043D\u0430\u0437\u044B\u0432\u0430\u044E\u0442 \u043C\u043E\u0434\u0435\u043B\u044C \u0438 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430.");
12353
+ }
12219
12354
  async function askCapabilities(userId, chat) {
12220
12355
  if (!options2.chatCapabilities) return resolveChatCapabilities(void 0);
12221
12356
  try {
12222
- return resolveChatCapabilities(await options2.chatCapabilities({ userId, chat }));
12357
+ const given = await options2.chatCapabilities({ userId, chat });
12358
+ warnErrorDetailsWithoutModel(given);
12359
+ return resolveChatCapabilities(given);
12223
12360
  } catch (error) {
12224
12361
  logError(`chatCapabilities \u0443\u043F\u0430\u043B \u0438\u043B\u0438 \u0432\u0435\u0440\u043D\u0443\u043B \u043D\u0435\u0432\u0435\u0440\u043D\u044B\u0435 \u043F\u0440\u0430\u0432\u0430 (\u0447\u0430\u0442 ${chat.id}): ${error instanceof Error ? error.message : String(error)}`);
12225
12362
  throw error;
@@ -14284,4 +14421,4 @@ export {
14284
14421
  InvalidWidgetAnswerError,
14285
14422
  createChatService
14286
14423
  };
14287
- //# sourceMappingURL=chunk-4Y26SCCM.js.map
14424
+ //# sourceMappingURL=chunk-EWEEYX6U.js.map