@ykurilov/agent-chat-core 0.63.1 → 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.
@@ -7,18 +7,22 @@ import type { Chat } from "../conversation/types.js";
7
7
  * `metadata: { showOnScreen: true }` и вызовы с запросом подтверждения видны всегда. Запрет `reasoning` и `toolDetails` прячет их из истории и потока, модель и
8
8
  * хранилище получают всё. `steer` — поправка к идущему ответу (`POST …/steer`). `modelDetails` — модель, провайдер,
9
9
  * уровень рассуждений, расход токенов, метаданные поставщика, владелец чата и текст сводок сжатия в истории и потоке;
10
- * запрет выключает и `modelChoice`: выбирать невидимую модель нельзя.
10
+ * запрет выключает и `modelChoice`: выбирать невидимую модель нельзя. `errorDetails` — подробности сбоя провайдера
11
+ * (провайдер, модель, код, причина, когда снимут предел) в истории и потоке; единственное право, выключенное по
12
+ * умолчанию: подробности видны любому через инструменты разработчика, открывать их — только админу. При
13
+ * `modelDetails: false` выключено и оно.
11
14
  */
12
- export type ChatCapability = "edit" | "regenerate" | "retry" | "branches" | "modelChoice" | "answerEditing" | "chatList" | "compact" | "attachments" | "voice" | "reasoning" | "toolDetails" | "steer" | "modelDetails";
15
+ export type ChatCapability = "edit" | "regenerate" | "retry" | "branches" | "modelChoice" | "answerEditing" | "chatList" | "compact" | "attachments" | "voice" | "reasoning" | "toolDetails" | "steer" | "modelDetails" | "errorDetails";
13
16
  /**
14
- * Права, как их отдаёт `resolveChatCapabilities`. `modelDetails` в типе необязательно (нет — `true`): свой литерал прав,
15
- * собранный до этого права, остаётся верным. Ядро и история заполняют его всегда.
17
+ * Права, как их отдаёт `resolveChatCapabilities`. `modelDetails` (нет — `true`) и `errorDetails` (нет — `false`) в типе
18
+ * необязательны: свой литерал прав, собранный до этих прав, остаётся верным. Ядро и история заполняют их всегда.
16
19
  */
17
- export type ChatCapabilities = Record<Exclude<ChatCapability, "modelDetails">, boolean> & {
20
+ export type ChatCapabilities = Record<Exclude<ChatCapability, "modelDetails" | "errorDetails">, boolean> & {
18
21
  modelDetails?: boolean;
22
+ errorDetails?: boolean;
19
23
  };
20
24
  /**
21
- * Права в чате от приложения: частичный список, пропущенное — `true`. Тот же вход, что у `beforeTurn` (чат с данными
25
+ * Права в чате от приложения: частичный список, пропущенное — `true` (`errorDetails` — `false`). Тот же вход, что у `beforeTurn` (чат с данными
22
26
  * объекта). Не булево значение, неизвестное право или исключение — ошибка приложения.
23
27
  */
24
28
  export type ChatCapabilitiesHandler = (input: {
@@ -27,8 +31,8 @@ export type ChatCapabilitiesHandler = (input: {
27
31
  }) => Partial<ChatCapabilities> | void | Promise<Partial<ChatCapabilities> | void>;
28
32
  export declare const CHAT_CAPABILITIES: readonly ChatCapability[];
29
33
  /**
30
- * Все права разрешены и дополнены ответом приложения; `modelDetails: false` выключает и `modelChoice`. Не булево значение
31
- * или неизвестное право — `TypeError`.
34
+ * Все права, кроме `errorDetails`, разрешены и дополнены ответом приложения; `modelDetails: false` выключает и
35
+ * `modelChoice`, и `errorDetails`. Не булево значение или неизвестное право — `TypeError`.
32
36
  */
33
37
  export declare function resolveChatCapabilities(given: Partial<ChatCapabilities> | void | null): Required<ChatCapabilities>;
34
38
  /** Действие запрещено правами чата: сервер отвечает 403 `action_forbidden`. */
@@ -1,7 +1,7 @@
1
1
  import type { UIMessageChunk } from "ai";
2
2
  import type { ChatCapabilities } from "./chat-capabilities.js";
3
3
  /**
4
- * Что из ответа видит браузер по правам `reasoning`, `toolDetails` и `modelDetails`. Хранилище, модель и хуки получают
4
+ * Что из ответа видит браузер по правам `reasoning`, `toolDetails`, `modelDetails` и `errorDetails`. Хранилище, модель и хуки получают
5
5
  * ответ целиком: видимость — только на выходе к клиенту (история, поток, повтор кусков после переподключения).
6
6
  */
7
7
  export interface ClientVisibility {
@@ -38,4 +38,4 @@ export declare const SHOW_ON_SCREEN_KEY = "showOnScreen";
38
38
  * Видимость для прав зрителя. При `toolDetails: false` видны вызовы с пометкой `showOnScreen`, инструменты SDK, которым
39
39
  * нужен экран (`sdkScreenTool`: виджеты, браузерные), и вызовы, спросившие подтверждения.
40
40
  */
41
- export declare function clientVisibilityOf(capabilities: Pick<ChatCapabilities, "reasoning" | "toolDetails" | "modelDetails">, sdkScreenTool: (toolName: string) => boolean): ClientVisibility;
41
+ export declare function clientVisibilityOf(capabilities: Pick<ChatCapabilities, "reasoning" | "toolDetails" | "modelDetails" | "errorDetails">, sdkScreenTool: (toolName: string) => boolean): ClientVisibility;
@@ -1,7 +1,7 @@
1
1
  import type { ToolSet, UIMessageChunk } from "ai";
2
2
  import type { MessageAttachments } from "../attachments/message-attachments.js";
3
3
  import type { AgentAudience, AgentChannel, ChatSubject, GroupTurn, UsageSource } from "../conversation/types.js";
4
- import type { AgentChatMessage, ChatErrorKind, FinalAnswerStatus, TokenUsage } from "../conversation/types.js";
4
+ import type { AgentChatMessage, ChatError, FinalAnswerStatus, TokenUsage } from "../conversation/types.js";
5
5
  import type { AgentHelper } from "../helpers/helper-types.js";
6
6
  import type { DataChange, ScreenPlace } from "../page-context/page-context.js";
7
7
  import type { ModelCatalog, ModelProfile } from "../models/model-catalog.js";
@@ -261,9 +261,7 @@ export interface AgentTurnInput {
261
261
  status: FinalAnswerStatus;
262
262
  parts: AgentChatMessage["parts"];
263
263
  usage: TokenUsage | null;
264
- error: {
265
- kind: ChatErrorKind;
266
- } | null;
264
+ error: ChatError | null;
267
265
  providerId: string;
268
266
  }, stopWaiting: AbortSignal) => Promise<AppEvent[]>;
269
267
  /** Итог ответа записан в хранилище, до расхода и `afterTurn`: для срока хода, который не должен обрывать хук. */
@@ -6,6 +6,23 @@ export declare const CURRENT_FORMAT_VERSION = 1;
6
6
  export type AnswerStatus = "streaming" | "complete" | "stopped" | "error" | "interrupted" | "awaiting_approval";
7
7
  export type FinalAnswerStatus = Exclude<AnswerStatus, "streaming">;
8
8
  export type ChatErrorKind = "overloaded" | "rate_limited" | "insufficient_funds" | "invalid_key" | "context_overflow" | "other";
9
+ /**
10
+ * Подробности сбоя провайдера для того, кому приложение их открыло (право `errorDetails`): кто не ответил и почему.
11
+ * `reason` — одна строка до 300 знаков из сообщения провайдера, секреты и параметры адресов вычищены. `resetsAt` — когда
12
+ * провайдер обещал снять предел (ISO), если сообщил.
13
+ */
14
+ export interface ChatErrorDetails {
15
+ providerId: string;
16
+ modelId: string;
17
+ statusCode?: number;
18
+ reason?: string;
19
+ resetsAt?: string;
20
+ }
21
+ /** Сбой ответа: вид для подписи и, если есть, подробности (хранятся всегда, в браузер — по праву `errorDetails`). */
22
+ export interface ChatError {
23
+ kind: ChatErrorKind;
24
+ details?: ChatErrorDetails;
25
+ }
9
26
  export interface TokenUsage {
10
27
  inputTokens: number;
11
28
  cachedInputTokens: number;
@@ -97,9 +114,7 @@ export interface ChatMessageMetadata {
97
114
  usage?: TokenUsage;
98
115
  reasoningDurationsMs?: Record<string, number>;
99
116
  status?: AnswerStatus;
100
- error?: {
101
- kind: ChatErrorKind;
102
- };
117
+ error?: ChatError;
103
118
  /** Есть, если озвучку ответа оборвало перебивание. */
104
119
  voiceCut?: VoiceCut;
105
120
  /** Ответ не дошёл до человека (`markUndelivered`): модель его не видит, экран подписывает «не доставлено». */
@@ -118,9 +133,7 @@ export interface StoredMessage {
118
133
  providerId: string | null;
119
134
  usage: TokenUsage | null;
120
135
  reasoningDurationsMs: Record<string, number>;
121
- error: {
122
- kind: ChatErrorKind;
123
- } | null;
136
+ error: ChatError | null;
124
137
  /**
125
138
  * Отметка обрыва озвучки. Необязательное поле: старые данные и хранилища без голосового режима его не знают;
126
139
  * встроенные хранилища читают отсутствие как `null`.
@@ -1,5 +1,5 @@
1
1
  export { BUILT_IN_USAGE_SOURCES, CURRENT_FORMAT_VERSION } from "./conversation/types.js";
2
- export type { AfterTurnState, AgentAudience, AgentChannel, AgentChatMessage, AnswerAliveMark, AnswerStatus, Chat, ChatErrorKind, ChatMessageMetadata, ChatSubject, ExternalChannel, FinalAnswerStatus, GroupTurn, PendingStepUsage, StoredBranchSelection, StoredCompaction, StoredMessage, TokenUsage, UsageByChatFilter, UsageChatSum, UsageFilter, UsageRecord, UsageSource, BuiltInUsageSource, UsageSum, VoiceCut, } from "./conversation/types.js";
2
+ export type { AfterTurnState, AgentAudience, AgentChannel, AgentChatMessage, AnswerAliveMark, AnswerStatus, Chat, ChatError, ChatErrorDetails, ChatErrorKind, ChatMessageMetadata, ChatSubject, ExternalChannel, FinalAnswerStatus, GroupTurn, PendingStepUsage, StoredBranchSelection, StoredCompaction, StoredMessage, TokenUsage, UsageByChatFilter, UsageChatSum, UsageFilter, UsageRecord, UsageSource, BuiltInUsageSource, UsageSum, VoiceCut, } from "./conversation/types.js";
3
3
  export type { AgentHelper, HelperPendingApproval, HelperProgress, HelperStep, HelperStepReport, RunHelper, RunHelperInput, RunHelperResult, } from "./helpers/helper-types.js";
4
4
  export { runHelper } from "./helpers/run-helper.js";
5
5
  export { AGENT_NOTICE_PART_TYPE } from "./notices/agent-notice.js";
@@ -11,3 +11,5 @@ export declare function classifyProviderError(error: unknown, provider?: Pick<Pr
11
11
  * ответить. Модерация — не такой отказ: дело в содержимом, и другому провайдеру оно не передаётся.
12
12
  */
13
13
  export declare function isAccessRefusal(error: unknown): boolean;
14
+ /** Отказ — исчерпанный предел расходов ключа, а не пустой счёт: для громкой строки владельцу. */
15
+ export declare function isKeyLimit(error: unknown): boolean;
@@ -0,0 +1,18 @@
1
+ import type { ChatErrorDetails } from "../conversation/types.js";
2
+ import type { ModelCatalog } from "../models/model-catalog.js";
3
+ export interface ProviderErrorDetailsContext {
4
+ providerId: string;
5
+ modelId: string;
6
+ /** Окружение сервера: значения ключей провайдеров вычищаются из причины. */
7
+ env: Record<string, string | undefined>;
8
+ catalog: ModelCatalog;
9
+ /** Часы хода, мс: «через N секунд» и проверка, что дата обнуления в будущем. По умолчанию — сейчас. */
10
+ now?: () => number;
11
+ }
12
+ /**
13
+ * Подробности сбоя для админа: провайдер, модель, код, причина одной строкой (сообщение провайдера из тела ответа, без
14
+ * секретов и параметров адресов) и когда провайдер снимет предел — из метаданных (`limit_reset`, `resets_at`) или
15
+ * текста сообщения о пределе («Key limit exceeded … resets 2026-11-01»). Дата принимается только в будущем и не дальше
16
+ * двух лет; время без пояса — UTC.
17
+ */
18
+ export declare function providerErrorDetails(error: unknown, context: ProviderErrorDetailsContext): ChatErrorDetails;
@@ -17,13 +17,15 @@ export interface ProviderOutages {
17
17
  /**
18
18
  * Провайдер отказал: пауза на `PROVIDER_OUTAGE_PAUSE_MS` и, не чаще раза в `PROVIDER_OUTAGE_ALERT_EVERY_MS`, строка
19
19
  * `[ВНИМАНИЕ провайдер]` в журнал. `fallbackProviderId` — кто отвечает вместо; нет — ходы падают, а
20
- * `fallbacksPaused` говорит, что запасные есть, но тоже на паузе.
20
+ * `fallbacksPaused` говорит, что запасные есть, но тоже на паузе. `keyLimit` — деньги не на счёте кончились, а
21
+ * исчерпан предел расходов ключа: строка говорит об этом.
21
22
  */
22
23
  report(input: {
23
24
  providerId: string;
24
25
  kind: ProviderOutageKind;
25
26
  fallbackProviderId?: string;
26
27
  fallbacksPaused?: boolean;
28
+ keyLimit?: boolean;
27
29
  }): void;
28
30
  /** Вид отказа, пока провайдер на паузе или его пробует другой ход; иначе `undefined`. Ничего не меняет. */
29
31
  pausedKind(providerId: string): ProviderOutageKind | undefined;
@@ -4,7 +4,9 @@ export declare const MIN_SECRET_LENGTH = 8;
4
4
  /**
5
5
  * Вычищает секреты из текста, который уходит человеку, в базу, в журнал или модели: известные значения
6
6
  * (`knownSecrets`, например ключи провайдеров из окружения), пароль в адресе `схема://логин:пароль@`,
7
- * заголовок `Authorization`, `?key=…`, поля вроде `password=…`, `password: …` и `"token": "…"`, узнаваемые ключи сервисов.
7
+ * заголовок `Authorization`, `Bearer …` в любом регистре, `key=…`, поля вроде `password=…`, `password: …` и `"token": "…"`,
8
+ * слово ключа через пробел перед похожим на секрет значением (`token abc123def456`), узнаваемые ключи сервисов и их
9
+ * маски со звёздочками.
8
10
  */
9
11
  export declare function scrubSecrets(text: string, knownSecrets?: Iterable<string | undefined>): string;
10
12
  /**
@@ -1,4 +1,4 @@
1
- import type { AgentChannel, Chat, ChatErrorKind, ChatSubject, FinalAnswerStatus, StoredMessage, TokenUsage } from "../conversation/types.js";
1
+ import type { AgentChannel, Chat, ChatError, ChatSubject, FinalAnswerStatus, StoredMessage, TokenUsage } from "../conversation/types.js";
2
2
  /** Что человек делает с чатом, когда приложение решает, пускать ли ход. */
3
3
  export type TurnAction = "send" | "edit" | "retry" | "regenerate" | "approval" | "widget" | "notice";
4
4
  /** Отказ приложения в ходе: код для программы, текст для человека. */
@@ -130,9 +130,8 @@ export interface TurnResult {
130
130
  usage: TokenUsage | null;
131
131
  /** Цена хода в долларах; `null` — у модели нет цены. */
132
132
  costUsd: number | null;
133
- error?: {
134
- kind: ChatErrorKind;
135
- };
133
+ /** Вид сбоя и подробности провайдера (`details`): хук на сервере получает их всегда. */
134
+ error?: ChatError;
136
135
  /**
137
136
  * Каким действием начат ход (см. `TurnAction`). `null` — итог доделан после перезапуска (`recovered`), а ядро не
138
137
  * помнит действие; ход по служебному сообщению и тогда — `notice`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykurilov/agent-chat-core",
3
- "version": "0.63.1",
3
+ "version": "0.64.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/turn-hooks/turn-hooks.ts"],"sourcesContent":["import type { AgentChannel, Chat, ChatErrorKind, 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 error?: { kind: ChatErrorKind };\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":";AA8KO,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":[]}