@ykurilov/agent-chat-core 0.63.2 → 0.65.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,17 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.65.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 814ee7f: Пометка «Агент написал первым — человек молчал 1 ч 7 мин» перед ответом, который агент написал сам. Ядро: право чата `appNotices` (по умолчанию выключено) — служебные пометки приложения (`kind: "app"`) уходят в историю без текста для модели и только через белый список `appNoticeViews` (имя → безопасные данные для экрана; без функции пометка не уходит); `modelDetails: false` выключает и `appNotices`. React: `noticeLines` у `AgentChat` (имя пометки → строка), договор скина 27 — необязательный `ServiceLine`. Сценарии: в данных пометки `silentMs` (от последнего сообщения человека) и `exit`, проекция для экрана регистрируется в `withChat`, готовые строки `flowNoticeLines()` с подписями `russianFlowNoticeLabels`/`englishFlowNoticeLabels` (`FlowLabels.notices`); `FlowTestChat` передаёт их в `renderChat(chatId, { noticeLines })`. Тандем рисует `ServiceLine`. Как подключить — CHANGES.md, раздел 0.65.0.
8
+
9
+ ## 0.64.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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`, миграций нет.
14
+
3
15
  ## 0.63.2
4
16
 
5
17
  ### 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`, `appNotices`; по умолчанию всё `true`, кроме `errorDetails` и `appNotices` — служебных пометок приложения на экране, без текста для модели и только через белый список `appNoticeViews` (имя пометки → безопасные данные для экрана; без функции пометка не уходит); `modelDetails: false` выключает `modelChoice`, `errorDetails` и `appNotices`). Неизвестное право или не булево значение — `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
  );
@@ -1542,12 +1545,14 @@ function publicAppStateParts(parts, hidden = isReservedAppStateKey) {
1542
1545
  return data === void 0 ? [] : [{ ...part, data }];
1543
1546
  });
1544
1547
  }
1545
- function publicMessages(messages, hidden = isReservedAppStateKey) {
1548
+ function publicMessages(messages, hidden = isReservedAppStateKey, options2 = {}) {
1549
+ const views = options2.appNoticeViews;
1550
+ const toScreen = views ? (part) => appNoticesForScreen(part, views) : withoutAppNotices;
1546
1551
  const shown = [];
1547
1552
  let waiting = [];
1548
1553
  for (const message of messages) {
1549
1554
  const hasService = message.parts.some((part) => part.type === APP_STATE_PART_TYPE || isModelOnlyPart(part) || hasAppNotice(part));
1550
- const parts = hasService ? publicAppStateParts(message.parts.filter((part) => !isModelOnlyPart(part)).flatMap(withoutAppNotices), hidden) : [...message.parts];
1555
+ const parts = hasService ? publicAppStateParts(message.parts.filter((part) => !isModelOnlyPart(part)).flatMap(toScreen), hidden) : [...message.parts];
1551
1556
  if (message.role === "user" && parts.length === 0) continue;
1552
1557
  const visible = hasService ? { ...message, parts } : message;
1553
1558
  if (isStateMessage(message)) {
@@ -1571,6 +1576,24 @@ function withoutAppNotices(part) {
1571
1576
  const notices = noticesOfPart(part).filter((notice) => notice.kind !== "app");
1572
1577
  return notices.length === 0 ? [] : [{ ...part, data: { ...part.data, notices } }];
1573
1578
  }
1579
+ function appNoticesForScreen(part, views) {
1580
+ if (part.type !== AGENT_NOTICE_PART_TYPE || !hasAppNotice(part)) return [part];
1581
+ const notices = noticesOfPart(part).flatMap((notice) => {
1582
+ if (notice.kind !== "app") return [notice];
1583
+ const data = screenDataOf(notice.name, notice.data, views);
1584
+ return data === void 0 ? [] : [{ kind: "app", name: notice.name, data }];
1585
+ });
1586
+ return notices.length === 0 ? [] : [{ ...part, data: { ...part.data, notices } }];
1587
+ }
1588
+ function screenDataOf(name, data, views) {
1589
+ const view = name !== void 0 && Object.hasOwn(views, name) ? views[name] : void 0;
1590
+ if (!view) return void 0;
1591
+ try {
1592
+ return view(data);
1593
+ } catch {
1594
+ return void 0;
1595
+ }
1596
+ }
1574
1597
  function isModelOnlyPart(part) {
1575
1598
  return part.type === FROZEN_CONTEXT_PART_TYPE || part.type === END_TURN_PART_TYPE;
1576
1599
  }
@@ -5326,6 +5349,100 @@ ${summary}
5326
5349
  </answer_progress>`;
5327
5350
  }
5328
5351
 
5352
+ // src/provider-errors/provider-error-details.ts
5353
+ import { APICallError as APICallError4 } from "ai";
5354
+ var MAX_REASON_LENGTH = 300;
5355
+ var MAX_RESET_AHEAD_MS = 2 * 366 * 24 * 60 * 60 * 1e3;
5356
+ function providerErrorDetails(error, context) {
5357
+ const cause = underlyingError(error);
5358
+ const statusCode = errorStatusCode(cause);
5359
+ const { message, metadata } = providerMessageOf(cause);
5360
+ const reason = message === void 0 ? void 0 : shownReason(message, environmentSecrets(context.env, context.catalog));
5361
+ const nowMs = (context.now ?? Date.now)();
5362
+ const found = resetFromMetadata(metadata, nowMs) ?? (message === void 0 ? void 0 : resetFromText(message));
5363
+ const resetsAt = found !== void 0 && found > nowMs && found - nowMs <= MAX_RESET_AHEAD_MS ? new Date(found).toISOString() : void 0;
5364
+ return {
5365
+ providerId: context.providerId,
5366
+ modelId: context.modelId,
5367
+ ...statusCode !== void 0 && { statusCode },
5368
+ ...reason !== void 0 && reason !== "" && { reason },
5369
+ ...resetsAt !== void 0 && { resetsAt }
5370
+ };
5371
+ }
5372
+ function providerMessageOf(error) {
5373
+ if (APICallError4.isInstance(error)) {
5374
+ const fromBody = typeof error.responseBody === "string" ? fromJson(error.responseBody) : void 0;
5375
+ if (fromBody?.message !== void 0) return fromBody;
5376
+ const raw = [error.responseBody, error.message].find(isText);
5377
+ return { ...raw !== void 0 && { message: raw }, metadata: fromBody?.metadata };
5378
+ }
5379
+ if (typeof error === "string") return isText(error) ? { message: error } : {};
5380
+ return fromObject(error) ?? {};
5381
+ }
5382
+ function fromJson(body) {
5383
+ try {
5384
+ return fromObject(JSON.parse(body));
5385
+ } catch {
5386
+ return void 0;
5387
+ }
5388
+ }
5389
+ function fromObject(value) {
5390
+ if (typeof value !== "object" || value === null) return void 0;
5391
+ const fields = value;
5392
+ if (typeof fields.error === "object" && fields.error !== null) {
5393
+ const nested = fields.error;
5394
+ if (isText(nested.message)) return { message: nested.message, metadata: nested.metadata };
5395
+ }
5396
+ if (isText(fields.error)) return { message: fields.error, metadata: fields.metadata };
5397
+ if (isText(fields.message)) return { message: fields.message, metadata: fields.metadata };
5398
+ return void 0;
5399
+ }
5400
+ var isText = (value) => typeof value === "string" && value.trim() !== "";
5401
+ var URL_WITH_QUERY = /(https?:\/\/[^\s?#"'<>]+)[?#][^\s"'<>]*/g;
5402
+ var ACCOUNT_IDS = [/\borg-[A-Za-z0-9]{6,}/g, /\bproj_[A-Za-z0-9]{6,}/g, /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g];
5403
+ function shownReason(message, knownSecrets) {
5404
+ const oneLine2 = message.replace(/\s*[\r\n]+\s*/g, " ").trim();
5405
+ let shown = scrubSecrets(oneLine2.replace(URL_WITH_QUERY, "$1"), knownSecrets);
5406
+ for (const pattern of ACCOUNT_IDS) shown = shown.replace(pattern, "[\u0441\u043A\u0440\u044B\u0442\u043E]");
5407
+ return cutAtCharBoundary(shown, MAX_REASON_LENGTH);
5408
+ }
5409
+ var RESET_FIELD = /reset/i;
5410
+ var RELATIVE_FIELD = /sec|after|in$|_in_|ms$|_ms|Ms$/i;
5411
+ var MS_FIELD = /ms$|_ms|Ms$/;
5412
+ var MIN_UNIX_SECONDS = 1e9;
5413
+ function resetFromMetadata(metadata, nowMs) {
5414
+ if (typeof metadata !== "object" || metadata === null) return void 0;
5415
+ for (const [key, value] of Object.entries(metadata)) {
5416
+ if (!RESET_FIELD.test(key)) continue;
5417
+ const parsed = typeof value === "number" ? fromNumber(key, value, nowMs) : typeof value === "string" ? fromDateText(value) : void 0;
5418
+ if (parsed !== void 0) return parsed;
5419
+ }
5420
+ return void 0;
5421
+ }
5422
+ function fromNumber(key, value, nowMs) {
5423
+ if (!Number.isFinite(value) || value <= 0) return void 0;
5424
+ if (RELATIVE_FIELD.test(key) || value < MIN_UNIX_SECONDS) return nowMs + (MS_FIELD.test(key) ? value : value * 1e3);
5425
+ return value < 1e12 ? value * 1e3 : value;
5426
+ }
5427
+ var LIMIT_MESSAGE_START = /^[^'"«“]{0,80}?\b(?:limit|budget|quota)\b/i;
5428
+ var QUOTED = /'[^']*'|"[^"]*"|«[^»]*»|“[^”]*”/g;
5429
+ 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;
5430
+ function resetFromText(message) {
5431
+ if (!LIMIT_MESSAGE_START.test(message)) return void 0;
5432
+ const found = RESET_IN_TEXT.exec(message.replace(QUOTED, " "))?.[1];
5433
+ return found === void 0 ? void 0 : fromDateText(found);
5434
+ }
5435
+ function fromDateText(text) {
5436
+ const trimmed = text.trim();
5437
+ const match = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)(Z|[+-]\d{2}:?\d{2})?)?$/i.exec(trimmed);
5438
+ if (!match) return void 0;
5439
+ const [, year, month, day, time, zone] = match;
5440
+ const calendarDay = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
5441
+ if (calendarDay.getUTCMonth() !== Number(month) - 1 || calendarDay.getUTCDate() !== Number(day)) return void 0;
5442
+ const parsed = Date.parse(`${year}-${month}-${day}${time === void 0 ? "" : `T${time}${zone ?? "Z"}`}`);
5443
+ return Number.isNaN(parsed) ? void 0 : parsed;
5444
+ }
5445
+
5329
5446
  // src/agent-run/answer-saver.ts
5330
5447
  import { readUIMessageStream } from "ai";
5331
5448
 
@@ -6608,6 +6725,14 @@ async function runTurn(turnInput, emitRaw) {
6608
6725
  let errors = [];
6609
6726
  let terminal = [];
6610
6727
  let errorKind;
6728
+ let errorDetails;
6729
+ const detailsOf2 = (error2, failedProviderId) => providerErrorDetails(error2, {
6730
+ providerId: failedProviderId,
6731
+ modelId,
6732
+ env: input.env,
6733
+ catalog: input.catalog,
6734
+ now: () => (input.now ?? (() => /* @__PURE__ */ new Date()))().getTime()
6735
+ });
6611
6736
  let providerId = input.providerId;
6612
6737
  let answeredProviderId = input.providerId;
6613
6738
  let viewer;
@@ -6650,6 +6775,7 @@ async function runTurn(turnInput, emitRaw) {
6650
6775
  });
6651
6776
  const fallbackProviderIds = allFallbackIds.filter((candidate) => outages.pausedKind(candidate) === void 0);
6652
6777
  let firstOutage;
6778
+ let firstOutageDetails;
6653
6779
  const primaryPaused = outages.admit(input.providerId);
6654
6780
  if (primaryPaused !== void 0 && fallbackProviderIds[0] !== void 0) {
6655
6781
  providerId = fallbackProviderIds[0];
@@ -6746,7 +6872,9 @@ async function runTurn(turnInput, emitRaw) {
6746
6872
  } catch (error2) {
6747
6873
  if (error2 instanceof MissingProviderKeyError) logError(error2.message);
6748
6874
  if (!input.abortSignal.aborted) {
6749
- errorKind = errorKindForPerson(classify(error2), firstOutage);
6875
+ const kind = classify(error2);
6876
+ errorKind = errorKindForPerson(kind, firstOutage);
6877
+ errorDetails = errorKind === firstOutage ? firstOutageDetails : detailsOf2(error2, providerId);
6750
6878
  errors = [{ type: "error", errorText: errorKind }];
6751
6879
  }
6752
6880
  break;
@@ -6790,7 +6918,10 @@ async function runTurn(turnInput, emitRaw) {
6790
6918
  let next = stepsDone < maxSteps ? decideNextAttempt(result, nextState()) : { action: "fail" };
6791
6919
  const outage = isProviderOutage(result.kind) ? result.kind : void 0;
6792
6920
  if (outage) {
6793
- if (providerId === input.providerId) firstOutage ??= outage;
6921
+ if (providerId === input.providerId && firstOutage === void 0) {
6922
+ firstOutage = outage;
6923
+ firstOutageDetails = detailsOf2(result.error, providerId);
6924
+ }
6794
6925
  outages.report({
6795
6926
  providerId,
6796
6927
  kind: outage,
@@ -6879,6 +7010,7 @@ async function runTurn(turnInput, emitRaw) {
6879
7010
  continue;
6880
7011
  }
6881
7012
  errorKind = errorKindForPerson(result.kind, firstOutage);
7013
+ errorDetails = errorKind === firstOutage ? firstOutageDetails : detailsOf2(result.error, providerId);
6882
7014
  errors = errorKind === result.kind ? attempt.errors : [{ type: "error", errorText: errorKind }];
6883
7015
  terminal = attempt.terminal;
6884
7016
  logError(
@@ -6916,6 +7048,7 @@ async function runTurn(turnInput, emitRaw) {
6916
7048
  if (!input.abortSignal.aborted) {
6917
7049
  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
7050
  errorKind = error2 instanceof AgentViewerError ? "other" : classifyProviderError(error2, void 0);
7051
+ errorDetails = void 0;
6919
7052
  errors = [{ type: "error", errorText: errorKind }];
6920
7053
  }
6921
7054
  }
@@ -6986,7 +7119,7 @@ async function runTurn(turnInput, emitRaw) {
6986
7119
  for (const chunk of heldText?.release() ?? []) emitShown(chunk);
6987
7120
  turnUsage.closeAttempt();
6988
7121
  const usage = messageUsage();
6989
- const error = status === "error" ? { kind: errorKind ?? "other" } : null;
7122
+ const error = status === "error" ? { kind: errorKind ?? "other", ...errorDetails && { details: errorDetails } } : null;
6990
7123
  const answeredByBackup = answeredProviderId !== input.providerId && { providerId: answeredProviderId };
6991
7124
  let finished = false;
6992
7125
  if (!takeover.signal.aborted) {
@@ -9419,10 +9552,13 @@ var CHAT_CAPABILITIES = [
9419
9552
  "reasoning",
9420
9553
  "toolDetails",
9421
9554
  "steer",
9422
- "modelDetails"
9555
+ "modelDetails",
9556
+ "errorDetails",
9557
+ "appNotices"
9423
9558
  ];
9559
+ var OFF_BY_DEFAULT = /* @__PURE__ */ new Set(["errorDetails", "appNotices"]);
9424
9560
  function resolveChatCapabilities(given) {
9425
- const resolved = Object.fromEntries(CHAT_CAPABILITIES.map((capability) => [capability, true]));
9561
+ const resolved = Object.fromEntries(CHAT_CAPABILITIES.map((capability) => [capability, !OFF_BY_DEFAULT.has(capability)]));
9426
9562
  if (given === void 0 || given === null) return resolved;
9427
9563
  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
9564
  const unknown = Object.keys(given).find((key) => !CHAT_CAPABILITIES.includes(key));
@@ -9433,7 +9569,11 @@ function resolveChatCapabilities(given) {
9433
9569
  if (typeof value !== "boolean") throw new TypeError(`\u041F\u0440\u0430\u0432\u043E \xAB${capability}\xBB \u2014 true \u0438\u043B\u0438 false`);
9434
9570
  resolved[capability] = value;
9435
9571
  }
9436
- if (!resolved.modelDetails) resolved.modelChoice = false;
9572
+ if (!resolved.modelDetails) {
9573
+ resolved.modelChoice = false;
9574
+ resolved.errorDetails = false;
9575
+ resolved.appNotices = false;
9576
+ }
9437
9577
  return resolved;
9438
9578
  }
9439
9579
  var ActionForbiddenError = class extends Error {
@@ -11465,6 +11605,12 @@ var SHOWS_EVERYTHING = {
11465
11605
  };
11466
11606
  var MODEL_METADATA_KEYS = ["modelId", "providerId", "usage"];
11467
11607
  var MODEL_CHAT_KEYS = ["ownerId", "modelId", "providerId", "reasoning"];
11608
+ var withoutErrorDetails = (metadata) => {
11609
+ const error = metadata.error;
11610
+ if (typeof error !== "object" || error === null || !("details" in error)) return metadata;
11611
+ const { details: _details, ...rest } = error;
11612
+ return { ...metadata, error: rest };
11613
+ };
11468
11614
  var without = (carrier, keys) => {
11469
11615
  if (!keys.some((key) => key in carrier)) return carrier;
11470
11616
  const rest = { ...carrier };
@@ -11502,10 +11648,15 @@ function clientVisibilityOf(capabilities, sdkScreenTool) {
11502
11648
  const hidesReasoning = !capabilities.reasoning;
11503
11649
  const hidesTools = !capabilities.toolDetails;
11504
11650
  const hidesModel = capabilities.modelDetails === false;
11505
- if (!hidesReasoning && !hidesTools && !hidesModel) return SHOWS_EVERYTHING;
11651
+ const hidesErrorDetails = hidesModel || capabilities.errorDetails !== true;
11652
+ if (!hidesReasoning && !hidesTools && !hidesModel && !hidesErrorDetails) return SHOWS_EVERYTHING;
11506
11653
  const hidesCall = (toolName, call) => hidesTools && !sdkScreenTool(toolName) && !markedForScreen(call);
11507
11654
  const hiddenMetadataKeys = [...hidesReasoning ? ["reasoningDurationsMs"] : [], ...hidesModel ? MODEL_METADATA_KEYS : []];
11508
- const visibleMetadata = (metadata) => typeof metadata === "object" && metadata !== null ? without(metadata, hiddenMetadataKeys) : metadata;
11655
+ const visibleMetadata = (metadata) => {
11656
+ if (typeof metadata !== "object" || metadata === null) return metadata;
11657
+ const shown = without(metadata, hiddenMetadataKeys);
11658
+ return hidesErrorDetails ? withoutErrorDetails(shown) : shown;
11659
+ };
11509
11660
  const withoutProviderData2 = (carrier) => hidesModel ? without(carrier, PROVIDER_METADATA_KEYS) : hidesReasoning ? withoutReasoningDetails(carrier) : carrier;
11510
11661
  return {
11511
11662
  showsEverything: false,
@@ -12216,10 +12367,25 @@ function createChatService(input) {
12216
12367
  scope.capabilities ??= askCapabilities(userId, chat);
12217
12368
  return scope.capabilities;
12218
12369
  }
12370
+ let warnedErrorDetailsWithoutModel = false;
12371
+ let warnedAppNoticesWithoutModel = false;
12372
+ function warnErrorDetailsWithoutModel(given) {
12373
+ if (given?.modelDetails !== false) return;
12374
+ if (!warnedErrorDetailsWithoutModel && given.errorDetails === true) {
12375
+ warnedErrorDetailsWithoutModel = true;
12376
+ 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.");
12377
+ }
12378
+ if (!warnedAppNoticesWithoutModel && given.appNotices === true) {
12379
+ warnedAppNoticesWithoutModel = true;
12380
+ logError("chatCapabilities: appNotices: true \u043F\u0440\u0438 modelDetails: false \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u2014 \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u043F\u043E\u043C\u0435\u0442\u043A\u0438 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u044E\u0442 \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u044F.");
12381
+ }
12382
+ }
12219
12383
  async function askCapabilities(userId, chat) {
12220
12384
  if (!options2.chatCapabilities) return resolveChatCapabilities(void 0);
12221
12385
  try {
12222
- return resolveChatCapabilities(await options2.chatCapabilities({ userId, chat }));
12386
+ const given = await options2.chatCapabilities({ userId, chat });
12387
+ warnErrorDetailsWithoutModel(given);
12388
+ return resolveChatCapabilities(given);
12223
12389
  } catch (error) {
12224
12390
  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
12391
  throw error;
@@ -12422,6 +12588,7 @@ function createChatService(input) {
12422
12588
  const pending = runningAt < 0 ? [] : branch.slice(runningAt + 1);
12423
12589
  const capabilities = await capabilitiesOf(userId, withClosure(stored, closed), scope);
12424
12590
  const visibility = clientVisibilityOf(capabilities, isScreenTool);
12591
+ const screenNotices = capabilities.appNotices && options2.appNoticeViews ? { appNoticeViews: options2.appNoticeViews } : {};
12425
12592
  const undelivered = undeliveredIdsOf(branch);
12426
12593
  const markedUndelivered = (message) => undelivered.has(message.id) ? { ...message, metadata: { ...message.metadata, undelivered: true } } : message;
12427
12594
  const visible = (list) => visibility.showsEverything ? list : list.map((message) => visibility.visibleMessage(message));
@@ -12433,10 +12600,13 @@ function createChatService(input) {
12433
12600
  messages: visible(
12434
12601
  publicMessages(
12435
12602
  shown.filter((message) => message.id !== runningAnswerId || resumeBases.get(chatId)?.id === runningAnswerId).map((message) => message.id === runningAnswerId ? resumeBases.get(chatId) : markedUndelivered(toAgentChatMessage(message))),
12436
- hiddenStateKey
12603
+ hiddenStateKey,
12604
+ screenNotices
12437
12605
  )
12438
12606
  ),
12439
- ...pending.length > 0 && { pendingMessages: visible(publicMessages(pending.map(toAgentChatMessage), hiddenStateKey)) },
12607
+ ...pending.length > 0 && {
12608
+ pendingMessages: visible(publicMessages(pending.map(toAgentChatMessage), hiddenStateKey, screenNotices))
12609
+ },
12440
12610
  // Ход передаётся новому ответу на поправки: он вот-вот пойдёт, поток подождёт его.
12441
12611
  running: runningId !== null || handOffs.has(chatId),
12442
12612
  detachedAnswer: isDetachedAnswer(answerBeforeCorrections(branch), runningId),
@@ -14284,4 +14454,4 @@ export {
14284
14454
  InvalidWidgetAnswerError,
14285
14455
  createChatService
14286
14456
  };
14287
- //# sourceMappingURL=chunk-4Y26SCCM.js.map
14457
+ //# sourceMappingURL=chunk-SA3DXJYB.js.map