@ykurilov/agent-chat-core 0.56.0 → 0.57.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.
@@ -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 {};
@@ -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` шагов с конца и шаги от
@@ -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 {
@@ -792,10 +801,19 @@ export interface ChatService {
792
801
  * на пачку с `burstMessageIds`, касания сценария (`deliverNotices` с `wakeAgent`) пока ждут (`busy`). Решение — по
793
802
  * общей истории: годится для нескольких копий сервера. Повтор с тем же id (сервер упал посреди окна) не пишет копию,
794
803
  * а доводит ход. Права и закрытый чат — до записи, как у `sendMessage`; лимит расходов — при начале хода.
804
+ * Окно идёт по часам службы (`now`, ручной обход — `tickBursts`). С `answerAfterRestart` пачка переживает перезапуск
805
+ * без повтора вызова: ответит обход, событием `turn-started` с `unattended: true`.
795
806
  * @throws ChatBusyError ответ идёт на другой копии сервера или служба останавливается.
796
807
  * @throws TypeError неверное `quietMs`.
797
808
  */
798
809
  sendBurstMessage(userId: string, chatId: string, message: IncomingUserMessage, options?: SendBurstOptions): Promise<BurstMessageResult>;
810
+ /**
811
+ * Обход пачек сейчас: окна этой копии решают заново по часам службы (`now`), брошенные пачки из хранилища (их копия
812
+ * упала, срок удержания `BURST_HOLD_MS` после окна вышел) получают ход. Сам идёт раз в `STALE_ANSWERS_SWEEP_EVERY_MS`
813
+ * после `recoverInterruptedAnswers`. Звать вручную — когда часы службы подменены (тесты, прогон сценариев): сдвинули
814
+ * часы, вызвали, ход начался без настоящего ожидания.
815
+ */
816
+ tickBursts(): Promise<void>;
799
817
  /**
800
818
  * Поправка, пока агент отвечает: сообщение человека пишется сразу (ребёнок идущего ответа или прошлой поправки), агент
801
819
  * берёт его на ближайшей границе шага — ответ закрывается, и ход продолжается новым ответом на поправки (поток —
@@ -39,7 +39,7 @@ 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 type { AwaitingMessagePatch, ChatListOptions, ChatStorage, ChatSubjectFilter, FinishedMessagePatch, ImportedChat, MessageLink, MessagePartsLink, NewChatInput, SpendingLimitStateRecord, StoredPendingBurst, StreamingMessagePatch, } from "./storage/chat-storage.js";
43
43
  export { SpendingLimitReachedError } from "./spending/spending-limits.js";
44
44
  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
45
  export { DEFAULT_AFTER_TURN_ATTEMPTS, DEFAULT_AFTER_TURN_TIMEOUT_MS } from "./turn-hooks/after-turn.js";
@@ -71,7 +71,7 @@ export type { RunningAnswerRegistry } from "./running-answers/running-answer-reg
71
71
  export { ChatNotFoundError, createChatService, InvalidMessageError, InvalidModelChoiceError, NothingToRetryError, } from "./chat-service.js";
72
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";
73
73
  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";
74
+ export { BURST_HOLD_MS, BURST_PART_TYPE, BURST_SWEEP_LIMIT, DEFAULT_BURST_QUIET_MS, isBurstMessage } from "./bursts/message-bursts.js";
75
75
  export { GREETING_LEAD } from "./agent-run/greeting-lead.js";
76
76
  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
77
  export type { ChatGreeting, ImportChatInput, ImportedImagePart, ImportedMessageInput, OpenSubjectChatInput, OpenSubjectChatResult, } from "./subject-chats/subject-chats.js";
@@ -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
+ }
@@ -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.57.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "sideEffects": false,