@ykurilov/agent-chat-core 0.56.0 → 0.58.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +2 -0
  3. package/dist/access.js +2 -2
  4. package/dist/{chunk-SNPF4BPB.js → chunk-FBULM4V6.js} +2 -2
  5. package/dist/{chunk-PWGI6NH6.js → chunk-FOAIG4DY.js} +1 -1
  6. package/dist/chunk-FOAIG4DY.js.map +1 -0
  7. package/dist/{chunk-7M7M7DAN.js → chunk-KAG3QTZC.js} +661 -46
  8. package/dist/chunk-KAG3QTZC.js.map +1 -0
  9. package/dist/index.js +77 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/storage-contract.js +87 -5
  12. package/dist/storage-contract.js.map +1 -1
  13. package/dist/types/agent-run/garbled-tail.d.ts +69 -0
  14. package/dist/types/agent-run/next-attempt.d.ts +12 -1
  15. package/dist/types/agent-run/reply-checks.d.ts +3 -1
  16. package/dist/types/agent-run/run-agent-turn.d.ts +6 -0
  17. package/dist/types/agent-run/staff-replies.d.ts +7 -0
  18. package/dist/types/bursts/message-bursts.d.ts +12 -0
  19. package/dist/types/chat-events/chat-events.d.ts +7 -0
  20. package/dist/types/chat-service.d.ts +37 -0
  21. package/dist/types/helpers/run-helper.d.ts +3 -0
  22. package/dist/types/index.d.ts +5 -3
  23. package/dist/types/provider-errors/classify-provider-error.d.ts +5 -0
  24. package/dist/types/provider-errors/error-signs.d.ts +2 -2
  25. package/dist/types/provider-errors/provider-outages.d.ts +41 -0
  26. package/dist/types/storage/chat-storage.d.ts +47 -0
  27. package/dist/types/storage/postgres-migrations.d.ts +28 -0
  28. package/dist/types/storage/storage-contract.d.ts +5 -0
  29. package/dist/types/turn-hooks/turn-gate.d.ts +2 -0
  30. package/dist/types/turn-hooks/turn-hooks.d.ts +7 -0
  31. package/package.json +1 -1
  32. package/dist/chunk-7M7M7DAN.js.map +0 -1
  33. package/dist/chunk-PWGI6NH6.js.map +0 -1
  34. /package/dist/{chunk-SNPF4BPB.js.map → chunk-FBULM4V6.js.map} +0 -0
@@ -0,0 +1,69 @@
1
+ import type { LanguageModel } from "ai";
2
+ import type { AgentChatMessage } from "../conversation/types.js";
3
+ type MessagePart = AgentChatMessage["parts"][number];
4
+ /**
5
+ * Признаки срыва ответа в кашу — модель потеряла границу своего хода и пишет дальше за обоих. Правило общее для всех
6
+ * моделей и смотрит каждый шаг ответа отдельно. Нужен опорный признак — он же место среза: `role` — строка с ролью сразу
7
+ * после законченного ответа (с начала строки, после конца предложения, без двоеточия). Пара `</think>` в шаге перед такой
8
+ * строкой переносит место среза на первый `</think>`. Второй признак зависит от строки с ролью:
9
+ * - `userНу…` (роль слиплась с репликой): `think` — служебная метка в шаге (`</think>`, `<|im_end|>`, `<|…|>`);
10
+ * `restart` — после неё ещё одна строка с ролью или склейка русских предложений «месяц.Считать»; `tool` — вызов
11
+ * инструмента в том же шаге после неё;
12
+ * - `user` одной строкой: `think` — служебная метка в шаге; `restart` — ещё одна строка с ролью после неё;
13
+ * - метка шаблона (`<|im_start|>user`, `<|User|>`): `think` — метка конца своего хода перед ней (`<|im_end|>`);
14
+ * `restart` — ещё одна строка с ролью или склейка после неё.
15
+ * Код (блоки ```, отступ в 4 пробела, `…`, ``…``) и цитаты (`> …`) не смотрятся. Роли за обоих (`user` и `assistant`
16
+ * строками или метками шаблона) — запись диалога или шаблона по просьбе, а не срыв. Метка в начале шага (DeepSeek
17
+ * ставит `</think>` перед текстом шага) — не след срыва.
18
+ */
19
+ export type GarbleSign = "think" | "role" | "restart" | "tool";
20
+ /** Место срыва: часть с текстом, знак в ней, с которого начинается хвост, и найденные признаки. */
21
+ export interface GarbledTail {
22
+ partIndex: number;
23
+ offset: number;
24
+ signs: GarbleSign[];
25
+ }
26
+ /** Ищет срыв в частях ответа. `null` — срыва нет. */
27
+ export declare function findGarbledTail(parts: readonly MessagePart[]): GarbledTail | null;
28
+ /** Ответ без хвоста срыва и что отрезано. */
29
+ export interface GarbledTailCut {
30
+ parts: MessagePart[];
31
+ signs: GarbleSign[];
32
+ /** Сколько знаков текста и рассуждений отрезано. */
33
+ removedChars: number;
34
+ /** Последний оставшийся текст до места срыва: индекс один и тот же в исходных частях и в обрезанных; -1 — нет. */
35
+ textIndex: number;
36
+ }
37
+ /**
38
+ * Ответ без хвоста срыва: в шаге срыва — текст до места срыва, вызовы инструментов и служебные данные (`data-*`);
39
+ * текст и рассуждения после места срыва в этом шаге отброшены. Следующие шаги — как были: вызовы и запросы
40
+ * подтверждения не пропадают. Обрыв по длине в последнем шаге снят — ответ законченный. `null` — срыва нет или без
41
+ * хвоста в ответе не осталось бы текста.
42
+ */
43
+ export declare function cutGarbledTail(parts: readonly MessagePart[]): GarbledTailCut | null;
44
+ /**
45
+ * История для модели без хвостов срыва в ответах ассистента. Только в запросе: запись не меняется. Из той же истории —
46
+ * те же байты (кэш модели). Нечего резать — тот же массив, сообщения без срыва — те же объекты.
47
+ */
48
+ export declare function withoutGarbledTails(history: readonly AgentChatMessage[]): readonly AgentChatMessage[];
49
+ /**
50
+ * Срыв, пока идёт поток ответа модели (каждый шаг попытки). Решение — по тому же правилу, что при сохранении, и только
51
+ * на целых строках: незаконченная строка ждёт конца строки или конца текста. Сработало — текст и рассуждения дальше не
52
+ * идут (ни на экран, ни в запись), вызовы инструментов после этого отброшены до запуска, шаг кончается без вызовов —
53
+ * нового шага нет. Отброшенный вызов при сохранении учитывается через `forCut`: запись и поток решают одинаково.
54
+ */
55
+ export declare function createGarbledTailGuard(): {
56
+ /** Новая попытка: шаги прежней стёрты — всё сначала. */
57
+ reset(): void;
58
+ /** Модель попытки под присмотром: каждый запрос — новый шаг. */
59
+ wrap(model: LanguageModel): LanguageModel;
60
+ /** Срыв уже замечен в этой попытке: нового шага не нужно. */
61
+ garbled: () => boolean;
62
+ /** Сколько вызовов отброшено до запуска. */
63
+ droppedCalls: () => number;
64
+ /** Части ответа для правила при сохранении: отброшенный вызов — на своём месте, в конце. */
65
+ forCut: (saved: readonly MessagePart[]) => readonly MessagePart[];
66
+ /** Заглушка отброшенного вызова из `forCut`: в запись не идёт. */
67
+ isDroppedCall: (part: MessagePart) => boolean;
68
+ };
69
+ export {};
@@ -1,4 +1,6 @@
1
1
  import type { ModelCatalog } from "../models/model-catalog.js";
2
+ import type { ProviderOutageKind } from "../provider-errors/provider-outages.js";
3
+ import type { ChatErrorKind } from "../conversation/types.js";
2
4
  import type { AttemptResult } from "./turn-attempt.js";
3
5
  /**
4
6
  * Запасные провайдеры той же модели (FR-030): `false` — выключены; `byModel` — свой порядок для модели или `false` для
@@ -68,7 +70,10 @@ export declare function fallbackProviders(input: {
68
70
  setting?: ProviderFallbackSetting;
69
71
  }): string[];
70
72
  /**
71
- * Переход к запасному — только при перегрузке или лимите, до первого куска ответа и один раз на ход (FR-031, FR-032).
73
+ * Кончились деньги или не принят ключ — провайдер не ответит ни на какой запрос: сразу следующий из списка, в любом
74
+ * месте хода и сколько угодно раз (список только убывает). Такой переход не тратит единственный переход по перегрузке.
75
+ * Переход к запасному при перегрузке, лимите или отказе по доступу (403: регион, модель) — до первого куска ответа и
76
+ * один раз на ход (FR-031, FR-032).
72
77
  * Переполнение — сжатие: сначала истории чата, затем первых шагов ответа, каждое не больше раза на ход (FR-012).
73
78
  * После первого куска та же перегрузка или лимит — автопродолжение у того же провайдера, до двух на ход и не при
74
79
  * запросе подтверждения (FR-037, FR-042, FR-044). Запасной после первого куска не выбирается никогда.
@@ -102,5 +107,11 @@ export declare function decideNextAttempt(result: Extract<AttemptResult, {
102
107
  nextProviderId?: string | undefined;
103
108
  };
104
109
  }): NextAttempt;
110
+ /**
111
+ * Вид ошибки для человека, когда не ответил никто. Был отказ по деньгам или ключу (`firstOutage` — первый в ходе), а
112
+ * последний провайдер лишь занят или запрос велик — его вид: «попробуйте позже» или «короче» поможет. Иначе — исходная
113
+ * причина у основного: её и чинить владельцу.
114
+ */
115
+ export declare function errorKindForPerson(lastKind: ChatErrorKind, firstOutage: ProviderOutageKind | undefined): ChatErrorKind;
105
116
  /** Проверка настройки при создании службы: неверная форма, неизвестная модель или чужой провайдер — `TypeError`. */
106
117
  export declare function checkProviderFallback(setting: ProviderFallbackSetting | undefined, catalog: ModelCatalog): void;
@@ -86,7 +86,9 @@ export declare function withTextAt(parts: readonly MessagePart[], index: number,
86
86
  export declare function createShownSteps(): {
87
87
  observe(chunk: UIMessageChunk): void;
88
88
  /** Сколько шагов стереть с конца, чтобы убрать шаг последнего текста; `null` — текста на экране нет. */
89
- resetsToLastText(): number | null;
89
+ resetsToLastText: () => number | null;
90
+ /** То же для текста `fromEnd`-го с конца (0 — последний); `null` — такого текста на экране нет. */
91
+ resetsToText: (fromEnd: number) => number | null;
90
92
  };
91
93
  /**
92
94
  * Куски, которыми экран заменяет показанное исправленным (`web: "replace"`): сброс `resets` шагов с конца и шаги от
@@ -5,6 +5,7 @@ import type { AgentChatMessage, ChatErrorKind, FinalAnswerStatus, TokenUsage } f
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";
8
+ import type { ProviderOutages } from "../provider-errors/provider-outages.js";
8
9
  import type { LongTermMemory } from "../memory/long-term-memory.js";
9
10
  import type { ModelSilenceLimits } from "../provider-errors/model-silence.js";
10
11
  import type { ChatStorage } from "../storage/chat-storage.js";
@@ -279,6 +280,11 @@ export interface AgentTurnInput {
279
280
  toolResultLimit?: ResolvedToolResultLimit;
280
281
  /** Запасные провайдеры той же модели, уже проверенные. Нет — значение по умолчанию из `next-attempt`. */
281
282
  providerFallback?: ProviderFallbackSetting;
283
+ /**
284
+ * Память службы об отказах провайдеров по деньгам и ключу: основной на паузе — ход сразу у запасного. Нет — своя на
285
+ * ход: громкая строка в журнал есть, паузы между ходами нет.
286
+ */
287
+ providerOutages?: ProviderOutages;
282
288
  /** Сроки ответа серверных инструментов по имени, уже проверенные. Нет — 120 с всем. */
283
289
  toolTimeout?: ResolvedToolTimeout;
284
290
  /** Серверные инструменты, которые идут по одному (FR-049), уже проверенные. Нет — все вместе. */
@@ -0,0 +1,7 @@
1
+ import type { AgentChatMessage } from "../conversation/types.js";
2
+ /**
3
+ * Реплика сотрудника — не вопрос к агенту и не его ход: в запросе она идёт в одном сообщении с соседними словами человека
4
+ * (до неё и после неё), через пустую строку. Иначе два сообщения человека подряд получили бы между собой пометку
5
+ * «ход оборвался» (`withUnansweredMarked`), хотя хода не было. Склейка зависит только от истории: на каждом ходе те же байты.
6
+ */
7
+ export declare function withStaffRepliesJoined(history: readonly AgentChatMessage[]): readonly AgentChatMessage[];
@@ -16,6 +16,16 @@ export declare const MAX_BURST_QUIET_MS = 600000;
16
16
  export declare const BURST_PENDING_GRACE_MS = 120000;
17
17
  /** Чат занят другой копией сервера (её ход или аренда): через столько пачка пробует снова. */
18
18
  export declare const BURST_BUSY_RETRY_MS = 1000;
19
+ /**
20
+ * Запись ждущей пачки в хранилище (`savePendingBurst`) удерживается копией, что ждёт окно, ещё столько после конца окна;
21
+ * пока копия ждёт или решает, она продлевает удержание каждые `BURST_HOLD_RENEW_MS`. Срок вышел — пачку считают
22
+ * брошенной (копия упала): её берёт обход другой копии или следующего запуска.
23
+ */
24
+ export declare const BURST_HOLD_MS = 30000;
25
+ /** Как часто живая копия продлевает удержание своей ждущей пачки. */
26
+ export declare const BURST_HOLD_RENEW_MS = 10000;
27
+ /** Сколько брошенных пачек одна копия решает одновременно: обход берёт их порциями, пока не кончатся. */
28
+ export declare const BURST_SWEEP_LIMIT = 10;
19
29
  /** `at` — время записи сообщения, мс: по нему решается, одна ли пачка у соседей (`continuesBurst`). */
20
30
  export declare function burstPart(quietMs: number, at: number): AgentChatMessage["parts"][number];
21
31
  export declare function isBurstMessage(message: Pick<AgentChatMessage, "role" | "parts">): boolean;
@@ -74,6 +84,8 @@ export interface BurstWaits {
74
84
  wait(chatId: string, ms: number, cancelled: () => Error): Promise<void>;
75
85
  /** Кончает ожидания чата сейчас: их пачки решают заново (например, после «Стоп»). */
76
86
  wake(chatId: string): void;
87
+ /** Кончает все ожидания сейчас: пачки решают заново по часам службы (`tickBursts`). */
88
+ wakeAll(): void;
77
89
  /** Ждёт ли в чате хоть одно окно или решение по пачке (`holding`). */
78
90
  has(chatId: string): boolean;
79
91
  /** Решение по пачке идёт: чат считается ждущим, пока `work` не кончится. */
@@ -18,6 +18,11 @@ export type ChatEvent = {
18
18
  * мессенджер его не шлют. Нет — доставляют каналы, как прежде.
19
19
  */
20
20
  deliveredByApp?: true;
21
+ /**
22
+ * Ход начал сервер сам, и потока у вызова нет: ответ на пачку, брошенную упавшей или перезапущенной копией
23
+ * (`sendBurstMessage`). Каналы провожают такой ход, как ход по служебному сообщению. Нет — поток у вызова.
24
+ */
25
+ unattended?: true;
21
26
  } | {
22
27
  type: "turn-finished";
23
28
  ownerId: string;
@@ -121,6 +126,8 @@ export interface ChatEvents {
121
126
  /** Возвращает отписку. */
122
127
  subscribe(listener: (event: ChatEvent) => void): () => void;
123
128
  emit(event: ChatEvent): void;
129
+ /** Есть ли слушатели. Необязательный: своя рассылка (общая шина копий) может не знать — тогда не проверяется. */
130
+ hasListeners?(): boolean;
124
131
  }
125
132
  /** Рассылка событий в памяти процесса. Упавший слушатель не мешает остальным. */
126
133
  export declare function createChatEvents(logError?: (error: unknown) => void): ChatEvents;
@@ -256,6 +256,15 @@ export interface SendBurstOptions extends SendMessageOptions {
256
256
  * умолчанию 3000; целое от 0 до 600 000. 0 — без ожидания, но сообщения, пришедшие во время хода, всё равно склеиваются.
257
257
  */
258
258
  quietMs?: number;
259
+ /**
260
+ * Пачка переживает перезапуск без повтора вызова: хранилище с ждущими пачками (`savePendingBurst`) помнит её до
261
+ * ответа, а упала или перезапустилась копия посреди окна — ответит обход (`recoverInterruptedAnswers`, `tickBursts`).
262
+ * Такой ход приходит событием `turn-started` с `unattended: true`; поток — `subscribe` (ход уже кончился — `null`:
263
+ * ответ в истории). Запись пачки не удалась — вызов отказывает до записи сообщения. Своей очереди повторов каналу
264
+ * не нужно. Нет — как раньше: после перезапуска канал повторяет вызов с тем же `message.id` (так делают мессенджеры
265
+ * SDK; обход и повтор вместе дали бы ответ, который повтор не напечатает).
266
+ */
267
+ answerAfterRestart?: boolean;
259
268
  }
260
269
  /** Сообщение пачки записано; ход на пачку — позже. */
261
270
  export interface BurstMessageResult {
@@ -376,6 +385,12 @@ export interface ChatServiceOptions {
376
385
  lockedToolOutputs?: readonly string[];
377
386
  /** События чата вне потока ответа. Нет — служба создаёт свои (`createChatServer` создаёт сам). */
378
387
  chatEvents?: ChatEvents;
388
+ /**
389
+ * Несколько служб чата на одном хранилище (своя инструкция на версию продукта и т. п.): какой службе отдать брошенную
390
+ * пачку (`answerAfterRestart`), найденную обходом этой службы (при старте, регулярно, `tickBursts`). Нет — отвечает
391
+ * сама эта служба. Задайте один и тот же `serviceFor` всем службам на этом хранилище: брошенные проверяет любая.
392
+ */
393
+ abandonedBursts?: AbandonedBurstsOptions;
379
394
  /** Фоновые задачи (пакет `@ykurilov/agent-chat-background`). Нет — запрос к модели побайтно прежний. */
380
395
  background?: BackgroundTasks;
381
396
  /**
@@ -568,6 +583,19 @@ export declare class WidgetNotPendingError extends Error {
568
583
  export declare class InvalidWidgetAnswerError extends Error {
569
584
  constructor(toolCallId: string);
570
585
  }
586
+ /** Кому отдать брошенную пачку (`ChatServiceOptions.abandonedBursts`). */
587
+ export interface AbandonedBurstsOptions {
588
+ /**
589
+ * Служба чата этой пачки: эта же, другая служба приложения или новая, созданная сейчас (`createChatService` или
590
+ * `createChatServer`). `null` — нужной службы нет: пачка никому не отдаётся и ждёт следующего обхода (через 30 с).
591
+ * Бросок — то же, со строкой в журнал. Зовётся на каждую взятую пачку, до хода; `chat` — чат из хранилища.
592
+ */
593
+ serviceFor(burst: {
594
+ ownerId: string;
595
+ chatId: string;
596
+ chat: Chat;
597
+ }): ChatService | null | undefined | Promise<ChatService | null | undefined>;
598
+ }
571
599
  /** Ответ человека на запрос подтверждения. */
572
600
  export type ApprovalAnswer = "allow" | "deny" | "always";
573
601
  /** Ответ человека на ждущий виджет. */
@@ -792,10 +820,19 @@ export interface ChatService {
792
820
  * на пачку с `burstMessageIds`, касания сценария (`deliverNotices` с `wakeAgent`) пока ждут (`busy`). Решение — по
793
821
  * общей истории: годится для нескольких копий сервера. Повтор с тем же id (сервер упал посреди окна) не пишет копию,
794
822
  * а доводит ход. Права и закрытый чат — до записи, как у `sendMessage`; лимит расходов — при начале хода.
823
+ * Окно идёт по часам службы (`now`, ручной обход — `tickBursts`). С `answerAfterRestart` пачка переживает перезапуск
824
+ * без повтора вызова: ответит обход, событием `turn-started` с `unattended: true`.
795
825
  * @throws ChatBusyError ответ идёт на другой копии сервера или служба останавливается.
796
826
  * @throws TypeError неверное `quietMs`.
797
827
  */
798
828
  sendBurstMessage(userId: string, chatId: string, message: IncomingUserMessage, options?: SendBurstOptions): Promise<BurstMessageResult>;
829
+ /**
830
+ * Обход пачек сейчас: окна этой копии решают заново по часам службы (`now`), брошенные пачки из хранилища (их копия
831
+ * упала, срок удержания `BURST_HOLD_MS` после окна вышел) получают ход. Сам идёт раз в `STALE_ANSWERS_SWEEP_EVERY_MS`
832
+ * после `recoverInterruptedAnswers`. Звать вручную — когда часы службы подменены (тесты, прогон сценариев): сдвинули
833
+ * часы, вызвали, ход начался без настоящего ожидания.
834
+ */
835
+ tickBursts(): Promise<void>;
799
836
  /**
800
837
  * Поправка, пока агент отвечает: сообщение человека пишется сразу (ребёнок идущего ответа или прошлой поправки), агент
801
838
  * берёт его на ближайшей границе шага — ответ закрывается, и ход продолжается новым ответом на поправки (поток —
@@ -1,6 +1,7 @@
1
1
  import type { ModelMessage } from "ai";
2
2
  import type { UsageSource } from "../conversation/types.js";
3
3
  import type { ModelCatalog } from "../models/model-catalog.js";
4
+ import type { ProviderOutages } from "../provider-errors/provider-outages.js";
4
5
  import type { ModelSilenceLimits } from "../provider-errors/model-silence.js";
5
6
  import type { ChatStorage } from "../storage/chat-storage.js";
6
7
  import type { ProviderFallbackSetting } from "../agent-run/next-attempt.js";
@@ -28,6 +29,8 @@ export interface HelperRunContext {
28
29
  /** Очередь инструментов «по одному» чата: помощник делит её с основным агентом. */
29
30
  toolQueue?: ToolQueue;
30
31
  providerFallback?: ProviderFallbackSetting;
32
+ /** Память службы об отказах провайдеров по деньгам и ключу. Нет — своя на запуск. */
33
+ providerOutages?: ProviderOutages;
31
34
  modelSilence?: ModelSilenceLimits;
32
35
  turnObserver?: TurnObserver;
33
36
  /** Проверка лимита расходов перед каждым шагом; бросает `SpendingLimitReachedError`. Нет — без лимита. */
@@ -39,7 +39,9 @@ export type { OpenRouterMaxPrice, OpenRouterProviderOptions } from "./models/pro
39
39
  export { aliveAtByLocalClock, assertKnownFormatVersion, ChatAlreadyExistsError, DuplicateChatSubjectError, DuplicateMessageError, MessageAlreadyFinishedError, MessageNotAwaitingError, RELEASED_ANSWER_ALIVE_AT, sameMessageParts, silenceBoundaryByLocalClock, UnsupportedFormatVersionError, } from "./storage/chat-storage.js";
40
40
  export { assertChatData, assertChatDataFilter, CHAT_DATA_MAX_BYTES, ChatDataTooLargeError, chatDataFields, chatDataMatches, chatDataOfInput, mergeChatData } from "./storage/chat-data.js";
41
41
  export type { ChatDataFilter, ChatDataFilterValue } from "./storage/chat-data.js";
42
- export type { AwaitingMessagePatch, ChatListOptions, ChatStorage, ChatSubjectFilter, FinishedMessagePatch, ImportedChat, MessageLink, MessagePartsLink, NewChatInput, SpendingLimitStateRecord, StreamingMessagePatch, } from "./storage/chat-storage.js";
42
+ export { migratePostgresSchema } from "./storage/postgres-migrations.js";
43
+ export type { PostgresMigrationConnection, PostgresMigrationOptions } from "./storage/postgres-migrations.js";
44
+ export type { AwaitingMessagePatch, ChatListOptions, ChatStorage, ChatSubjectFilter, FinishedMessagePatch, ImportedChat, MessageLink, MessagePartsLink, NewChatInput, SpendingLimitStateRecord, StoredPendingBurst, StreamingMessagePatch, } from "./storage/chat-storage.js";
43
45
  export { SpendingLimitReachedError } from "./spending/spending-limits.js";
44
46
  export { APP_EVENT_DATA_MAX_BYTES, APP_EVENT_TYPE_MAX_LENGTH, isTurnRefusalCode, normalizeTurnRefusal, TURN_REFUSAL_MESSAGE_MAX_LENGTH, TurnRefusedError, validateAppEvent, } from "./turn-hooks/turn-hooks.js";
45
47
  export { DEFAULT_AFTER_TURN_ATTEMPTS, DEFAULT_AFTER_TURN_TIMEOUT_MS } from "./turn-hooks/after-turn.js";
@@ -69,9 +71,9 @@ export { createInMemoryChatStorage } from "./storage/in-memory-storage.js";
69
71
  export { ChatBusyError, createRunningAnswerRegistry } from "./running-answers/running-answer-registry.js";
70
72
  export type { RunningAnswerRegistry } from "./running-answers/running-answer-registry.js";
71
73
  export { ChatNotFoundError, createChatService, InvalidMessageError, InvalidModelChoiceError, NothingToRetryError, } from "./chat-service.js";
72
- export type { ActionOptions, AgentSettings, ChatBranchForks, ChatCompactions, ChatHistory, ChatListItem, ChatService, ChatInstructions, ChatInstructionsTurn, ChatState, CompactionMarker, CompactionOutcome, CompactionSettings, FlushOptions, CheckReplyInput, CheckReplyResult, IncomingUserMessage, ModelChoice, ModelChosenHandler, ModelMenu, RecoverInterruptedAnswersOptions, SendMessageOptions, SendBurstOptions, BurstMessageResult, SteerResult, } from "./chat-service.js";
74
+ export type { AbandonedBurstsOptions, ActionOptions, AgentSettings, ChatBranchForks, ChatCompactions, ChatHistory, ChatListItem, ChatService, ChatInstructions, ChatInstructionsTurn, ChatState, CompactionMarker, CompactionOutcome, CompactionSettings, FlushOptions, CheckReplyInput, CheckReplyResult, IncomingUserMessage, ModelChoice, ModelChosenHandler, ModelMenu, RecoverInterruptedAnswersOptions, SendMessageOptions, SendBurstOptions, BurstMessageResult, SteerResult, } from "./chat-service.js";
73
75
  export { STEER_PART_TYPE, isCorrection } from "./steering/turn-steering.js";
74
- export { BURST_PART_TYPE, DEFAULT_BURST_QUIET_MS, isBurstMessage } from "./bursts/message-bursts.js";
76
+ export { BURST_HOLD_MS, BURST_PART_TYPE, BURST_SWEEP_LIMIT, DEFAULT_BURST_QUIET_MS, isBurstMessage } from "./bursts/message-bursts.js";
75
77
  export { GREETING_LEAD } from "./agent-run/greeting-lead.js";
76
78
  export { GREETING_MAX_LENGTH, IMPORT_MAX_IMAGE_BYTES, IMPORT_MAX_IMAGES, IMPORT_MAX_MESSAGES, SUBJECT_DATA_MAX_BYTES, SUBJECT_KEY_MAX_LENGTH } from "./subject-chats/subject-chats.js";
77
79
  export type { ChatGreeting, ImportChatInput, ImportedImagePart, ImportedMessageInput, OpenSubjectChatInput, OpenSubjectChatResult, } from "./subject-chats/subject-chats.js";
@@ -6,3 +6,8 @@ import type { ProviderDescription } from "../models/model-catalog.js";
6
6
  * Порядок общий с тихими повторами: «не повторять» → переполнение → «повторять».
7
7
  */
8
8
  export declare function classifyProviderError(error: unknown, provider?: Pick<ProviderDescription, "errorHints">): ChatErrorKind;
9
+ /**
10
+ * Провайдер отказал по доступу (403 без слов о ключе и деньгах): регион, доступ к модели. Другой провайдер может
11
+ * ответить. Модерация — не такой отказ: дело в содержимом, и другому провайдеру оно не передаётся.
12
+ */
13
+ export declare function isAccessRefusal(error: unknown): boolean;
@@ -1,6 +1,6 @@
1
1
  /** Запрос не влез в окно модели: признаки в тексте или код 413. Признаки лимита запросов — не переполнение. */
2
2
  export declare function isContextOverflow(text: string, statusCode?: number): boolean;
3
- /** Квота, бюджет или оплата исчерпаны: повторять не нужно. */
4
- export declare function isNotRetryable(text: string): boolean;
3
+ /** Квота, бюджет или оплата исчерпаны: повторять не нужно. При коде 429 — только явные признаки пустого счёта. */
4
+ export declare function isNotRetryable(text: string, statusCode?: number): boolean;
5
5
  /** Текст похож на временный сбой провайдера или сети. */
6
6
  export declare function looksTransient(text: string): boolean;
@@ -0,0 +1,41 @@
1
+ import type { ChatErrorKind } from "../conversation/types.js";
2
+ /** Отказ, после которого провайдер не ответит ни на какой запрос: кончились деньги или не принят ключ. */
3
+ export type ProviderOutageKind = Extract<ChatErrorKind, "insufficient_funds" | "invalid_key">;
4
+ export declare function isProviderOutage(kind: ChatErrorKind): kind is ProviderOutageKind;
5
+ /** Сколько провайдер на паузе после отказа: новые ходы это время сразу идут к запасному. */
6
+ export declare const PROVIDER_OUTAGE_PAUSE_MS: number;
7
+ /** Громкая строка о провайдере — не чаще. */
8
+ export declare const PROVIDER_OUTAGE_ALERT_EVERY_MS: number;
9
+ /** Пауза кончилась: один ход пробует провайдера, остальные это время идут мимо. Проба без итога — через этот срок следующая. */
10
+ export declare const PROVIDER_OUTAGE_PROBE_MS = 60000;
11
+ /**
12
+ * Память службы об отказах провайдеров по деньгам и ключу. Пустой счёт за минуты не пополнится: ходы на паузе не
13
+ * стучат в него каждый раз, а после паузы его пробует один ход, не все разом. Кэш при этом не теряется — ответить
14
+ * основной всё равно не может, а запасной на паузе получает все запросы подряд, и его кэш тёплый.
15
+ */
16
+ export interface ProviderOutages {
17
+ /**
18
+ * Провайдер отказал: пауза на `PROVIDER_OUTAGE_PAUSE_MS` и, не чаще раза в `PROVIDER_OUTAGE_ALERT_EVERY_MS`, строка
19
+ * `[ВНИМАНИЕ провайдер]` в журнал. `fallbackProviderId` — кто отвечает вместо; нет — ходы падают, а
20
+ * `fallbacksPaused` говорит, что запасные есть, но тоже на паузе.
21
+ */
22
+ report(input: {
23
+ providerId: string;
24
+ kind: ProviderOutageKind;
25
+ fallbackProviderId?: string;
26
+ fallbacksPaused?: boolean;
27
+ }): void;
28
+ /** Вид отказа, пока провайдер на паузе или его пробует другой ход; иначе `undefined`. Ничего не меняет. */
29
+ pausedKind(providerId: string): ProviderOutageKind | undefined;
30
+ /**
31
+ * Ход хочет к провайдеру: `undefined` — можно, вид отказа — нельзя. Пауза кончилась — первый получает пробу
32
+ * на `PROVIDER_OUTAGE_PROBE_MS`, остальные до её итога — вид отказа.
33
+ */
34
+ admit(providerId: string): ProviderOutageKind | undefined;
35
+ /** Провайдер ответил: пауза снята. Счёт громких строк остаётся — «мигающий» провайдер не шумит. */
36
+ recovered(providerId: string): void;
37
+ }
38
+ export declare function createProviderOutages(options: {
39
+ now: () => number;
40
+ logError: (message: string) => void;
41
+ }): ProviderOutages;
@@ -1,6 +1,7 @@
1
1
  import type { ChatClosure } from "../access/chat-closure.js";
2
2
  import type { ModelChoice } from "../models/model-choice.js";
3
3
  import type { ChatDataFilter } from "./chat-data.js";
4
+ import type { TurnRequestContext } from "../page-context/page-context.js";
4
5
  import type { MemoryError, MemoryNote, MemoryNoteInput, MemoryRevertGuard, MemoryWriteGuard } from "../memory/long-term-memory.js";
5
6
  import type { ExternalChannel } from "../conversation/types.js";
6
7
  import type { MemoryMergeApplyResult, MemoryMergeInput, MemoryMergeRevertResult, MemoryOptimizationOutcome, MemoryOptimizationRules, MemoryOptimizationRun, MemoryOptimizationStatus, MergedMemoryNote } from "../memory/memory-optimization.js";
@@ -208,6 +209,38 @@ export interface ChatStorage {
208
209
  * Необязательный: без него такие чаты свободны через срок аренды.
209
210
  */
210
211
  clearChatTurnLeases?(): Promise<void>;
212
+ /**
213
+ * Пачка сообщений человека ждёт хода (`sendBurstMessage`): запись на чат, новая заменяет прежнюю. Нужна, чтобы пачку,
214
+ * чьё окно тишины ждала упавшая или перезапущенная копия сервера, ответила другая копия или следующий запуск.
215
+ *
216
+ * Необязательный (вместе с `takeAbandonedBursts`, `renewPendingBurst` и `removePendingBurst`): без него окно тишины живёт только в памяти
217
+ * копии — после перезапуска пачка ответит, только когда приложение повторит вызов.
218
+ */
219
+ savePendingBurst?(burst: StoredPendingBurst): Promise<void>;
220
+ /**
221
+ * Брошенные пачки — `heldUntil` не позже `abandonedBefore` — атомарно переходят к `holder` до `heldUntil` и
222
+ * возвращаются, не больше `limit`, ранние `dueAt` первыми. Две копии разом одну пачку не получат. `exceptHolder` —
223
+ * записи этого держателя не берутся, каким бы ни был их срок.
224
+ */
225
+ takeAbandonedBursts?(take: {
226
+ abandonedBefore: string;
227
+ holder: string;
228
+ heldUntil: string;
229
+ limit: number;
230
+ exceptHolder?: string;
231
+ }): Promise<StoredPendingBurst[]>;
232
+ /**
233
+ * Продлить удержание до `heldUntil`, если запись чата всё ещё о сообщении `messageId` и держит её `holder`. Продлено —
234
+ * `true`; запись другая, взята другой копией или снята — `false`, ничего не меняется.
235
+ */
236
+ renewPendingBurst?(renew: {
237
+ chatId: string;
238
+ messageId: string;
239
+ holder: string;
240
+ heldUntil: string;
241
+ }): Promise<boolean>;
242
+ /** Снять запись пачки чата, если она всё ещё о сообщении `messageId`; о другом сообщении или нет — тихо ничего. */
243
+ removePendingBurst?(chatId: string, messageId: string): Promise<void>;
211
244
  /** Новая запись сжатия. Записи только добавляются; сообщения не меняются. Повтор id — ошибка. */
212
245
  insertCompaction(record: StoredCompaction): Promise<void>;
213
246
  /** Записи сжатия чата в порядке добавления. */
@@ -370,3 +403,17 @@ export declare class UnsupportedFormatVersionError extends Error {
370
403
  }
371
404
  /** Для адаптеров: сообщение из будущей версии формата прочитать нельзя. */
372
405
  export declare function assertKnownFormatVersion(message: Pick<StoredMessage, "id" | "formatVersion">): void;
406
+ /** Пачка, ждущая хода (`ChatStorage.savePendingBurst`). Время — ISO 8601 по часам службы. */
407
+ export interface StoredPendingBurst {
408
+ chatId: string;
409
+ ownerId: string;
410
+ /** Последнее сообщение пачки: на него ход. */
411
+ messageId: string;
412
+ /** Конец окна тишины: раньше ход не начинается. */
413
+ dueAt: string;
414
+ /** Копия сервера, которая ждёт окно, и до какого времени её ожидание считается живым. Прошло — пачка брошена. */
415
+ holder: string;
416
+ heldUntil: string;
417
+ /** Страница хода (`SendMessageOptions.page`): ход после перезапуска идёт с ней. */
418
+ page?: TurnRequestContext;
419
+ }
@@ -0,0 +1,28 @@
1
+ /** Соединение PostgreSQL, которое нужно помощнику миграций. Подходит `PoolClient` из `pg`: core от `pg` не зависит. */
2
+ export interface PostgresMigrationConnection {
3
+ query(text: string, values?: unknown[]): Promise<{
4
+ rows: unknown[];
5
+ }>;
6
+ release(): void;
7
+ }
8
+ export interface PostgresMigrationOptions<Connection extends PostgresMigrationConnection> {
9
+ /** Новое соединение из пула, например `() => pool.connect()`; на каждую попытку — своё. */
10
+ connect(): Promise<Connection>;
11
+ /** Своя блокировка пакета: одновременно мигрирует только одна копия этого пакета. */
12
+ lockKey: number;
13
+ /** Миграции пакета на этом соединении, например `migrate(drizzle(connection), …)`. */
14
+ migrate(connection: Connection): Promise<void>;
15
+ /** Паузы перед повторами, если всё же столкнулись с соседом. */
16
+ retryDelaysMs?: readonly number[];
17
+ }
18
+ /** Схема всех пакетов SDK. */
19
+ export declare const AGENT_CHAT_SCHEMA = "agent_chat";
20
+ /** Общая блокировка на создание схемы: одна на все пакеты, держится только на `create schema`. */
21
+ export declare const AGENT_CHAT_SCHEMA_LOCK_KEY = 7311900441;
22
+ /**
23
+ * Создаёт схему `agent_chat` и применяет миграции пакета так, чтобы пакеты могли стартовать одновременно на пустой базе.
24
+ * Схема создаётся под общей блокировкой, затем она отпускается и берётся своя блокировка пакета: блокировки не
25
+ * вложены, поэтому взаимной блокировки нет, а разные пакеты ждут друг друга только на создании схемы.
26
+ * Столкнулись всё же («уже есть») — повтор с начала на новом соединении.
27
+ */
28
+ export declare function migratePostgresSchema<Connection extends PostgresMigrationConnection>(options: PostgresMigrationOptions<Connection>): Promise<void>;
@@ -33,4 +33,9 @@ export declare function describeChatStorageContract(createStorage: () => ChatSto
33
33
  * `clearChatTurnLeases`) обязана быть.
34
34
  */
35
35
  chatTurnLease?: "required";
36
+ /**
37
+ * `pendingBursts: "required"` — ждущие пачки (`savePendingBurst`, `takeAbandonedBursts`, `renewPendingBurst`,
38
+ * `removePendingBurst`) обязаны быть.
39
+ */
40
+ pendingBursts?: "required";
36
41
  }): void;
@@ -9,6 +9,8 @@ export interface TurnGateInput extends AbandonedAnswers {
9
9
  messageId?: string;
10
10
  /** Ход на пачку сообщений (`sendBurstMessage`): их id по порядку, последнее — `messageId`. */
11
11
  burstMessageIds?: string[];
12
+ /** Ход на пачку без вызова (обход ждущих пачек). */
13
+ unattended?: true;
12
14
  }
13
15
  /**
14
16
  * С чем пропущен ход: обычный ход модели, «записать и промолчать» (`hold`) или готовый текст без модели (`reply`).
@@ -49,6 +49,11 @@ export type BeforeTurnHandler = (input: {
49
49
  * `text` — их тексты через пустую строку. Одно решение на всю пачку; счёт сообщений — по длине списка.
50
50
  */
51
51
  burstMessageIds?: string[];
52
+ /**
53
+ * Ход на пачку начал сам сервер после перезапуска или за упавшую копию (`answerAfterRestart`): вызова, который ждёт
54
+ * ответ или отказ, нет. Отказ дойдёт только до журнала — сообщить человеку может само приложение.
55
+ */
56
+ unattended?: true;
52
57
  } & AbandonedAnswers) => Promise<BeforeTurnDecision | void> | BeforeTurnDecision | void;
53
58
  /**
54
59
  * Ответы агента, которые уйдут с текущей ветки, если ход начнётся (`regenerate`, `edit`, `retry`): по ним приложение откатывает
@@ -94,6 +99,8 @@ export type TurnNotStartedHandler = (input: {
94
99
  text?: string;
95
100
  /** Пачка сообщений, как в `beforeTurn`. */
96
101
  burstMessageIds?: string[];
102
+ /** Ход без вызова, как в `beforeTurn`: об отказе (например, `spending_limit`) человеку может сказать только приложение. */
103
+ unattended?: true;
97
104
  reason: TurnNotStartedReason;
98
105
  } & AbandonedAnswers) => Promise<void> | void;
99
106
  /** Кто остановил ход: человек («Стоп»), общий срок хода (`agent.turnTimeoutMs`) или остановка службы (`flush`). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ykurilov/agent-chat-core",
3
- "version": "0.56.0",
3
+ "version": "0.58.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,