@ykurilov/agent-chat-storage-postgres 0.0.0-stage → 0.55.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 (46) hide show
  1. package/CHANGELOG.md +1213 -0
  2. package/LICENSE +21 -0
  3. package/README.md +37 -2
  4. package/dist/index.js +801 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/types/drizzle-chat-storage.d.ts +1604 -0
  7. package/dist/types/index.d.ts +2 -0
  8. package/dist/types/migrate.d.ts +6 -0
  9. package/dist/types/postgres-storage.d.ts +18 -0
  10. package/dist/types/schema.d.ts +1678 -0
  11. package/drizzle/0000_init.sql +32 -0
  12. package/drizzle/0001_chat_reasoning.sql +1 -0
  13. package/drizzle/0002_compactions.sql +16 -0
  14. package/drizzle/0003_branch_selections.sql +10 -0
  15. package/drizzle/0004_chat_title.sql +22 -0
  16. package/drizzle/0005_usage_records.sql +19 -0
  17. package/drizzle/0006_user_model_choices.sql +7 -0
  18. package/drizzle/0007_voice_dialog.sql +7 -0
  19. package/drizzle/0008_memory_notes.sql +11 -0
  20. package/drizzle/0009_memory_optimization.sql +15 -0
  21. package/drizzle/0010_usage_estimated.sql +1 -0
  22. package/drizzle/0011_message_alive_at.sql +1 -0
  23. package/drizzle/0012_spending_limit_states.sql +6 -0
  24. package/drizzle/0013_chat_origin.sql +1 -0
  25. package/drizzle/0014_chat_subject_after_turn.sql +5 -0
  26. package/drizzle/0015_chat_closed.sql +1 -0
  27. package/drizzle/0016_chat_turn_leases.sql +5 -0
  28. package/drizzle/meta/0000_snapshot.json +259 -0
  29. package/drizzle/meta/0001_snapshot.json +265 -0
  30. package/drizzle/meta/0002_snapshot.json +392 -0
  31. package/drizzle/meta/0003_snapshot.json +483 -0
  32. package/drizzle/meta/0004_snapshot.json +489 -0
  33. package/drizzle/meta/0005_snapshot.json +629 -0
  34. package/drizzle/meta/0006_snapshot.json +672 -0
  35. package/drizzle/meta/0007_snapshot.json +709 -0
  36. package/drizzle/meta/0008_snapshot.json +787 -0
  37. package/drizzle/meta/0009_snapshot.json +908 -0
  38. package/drizzle/meta/0010_snapshot.json +915 -0
  39. package/drizzle/meta/0011_snapshot.json +921 -0
  40. package/drizzle/meta/0012_snapshot.json +958 -0
  41. package/drizzle/meta/0013_snapshot.json +964 -0
  42. package/drizzle/meta/0014_snapshot.json +1019 -0
  43. package/drizzle/meta/0015_snapshot.json +1025 -0
  44. package/drizzle/meta/0016_snapshot.json +1056 -0
  45. package/drizzle/meta/_journal.json +125 -0
  46. package/package.json +46 -3
package/CHANGELOG.md ADDED
@@ -0,0 +1,1213 @@
1
+ # @ykurilov/agent-chat-storage-postgres
2
+
3
+ ## 0.55.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [51f7b38]
8
+ - Updated dependencies [cdc7dd7]
9
+ - @ykurilov/agent-chat-core@0.55.0
10
+
11
+ ## 0.54.0
12
+
13
+ ### Patch Changes
14
+
15
+ - Updated dependencies [fa90219]
16
+ - @ykurilov/agent-chat-core@0.54.0
17
+
18
+ ## 0.53.0
19
+
20
+ ### Minor Changes
21
+
22
+ - b6a7786: Сценарии: конец разговора без сбоев, «не сейчас», пояс по умолчанию, реплика сотрудника, тихое утро, касания с
23
+ карточками; расход по списку чатов.
24
+
25
+ - Конец разговора («срок вышел», «не завершил», прощание) больше не зависит от того, начался ли ход касания. Касания
26
+ сценария не режет потолок ядра «5 ходов сервера подряд»; у `deliverNotices` — свой потолок `maxServerTurnsInRow`
27
+ (число или `false`, по умолчанию 5, `MAX_SERVER_TURNS_IN_ROW`).
28
+ - Мягкий потолок сценария: 50 ходов сервера подряд без человека (`timers.maxServerTurnsInRow`), дальше касания ждут
29
+ его сообщения, конец разговора — в свой срок. Срок без причин писать не повторяется каждый час.
30
+ - `canWriteNow` → `false` — «не сейчас»: касание через час; `"never"` — молчать до сообщения человека; `{ retryAt: Date }`
31
+ — в свой момент (негодная дата — как `false`, прошлая — через минуту). Сроки конца разговора не снимаются.
32
+ - Человек писал до начала дня меньше 3 часов назад (`skipMorningIfHumanWithinMin`) — касания в начале дня ждут, не только
33
+ утреннее. Переход ноды по сроку и ответ на ночные сообщения не ждут.
34
+ - Сообщения после жёсткой рамки дня ставят срок на утро сами: ответ на накопленное приходит в `dayStart` при любых
35
+ касаниях. Их время — в данных чата (`flowHeldHuman`); для правила 3 часов, «не завершил» и перехода «человек молчит»
36
+ они — сообщения человека.
37
+ - `attach.deliver` получает `parts` — все части ответа для человека (карточка ролика, картинка, файл, виджет), `text`
38
+ прежний; `deliverNotices({ awaitAnswer }).answer.parts`; `deliverableParts` из ядра.
39
+ - `storage.sumUsageByChat({ chatIds, ownerId?, source?, since? })` — расход по списку чатов одним запросом (память,
40
+ SQLite, PostgreSQL; общие тесты хранилищ).
41
+ - `createAgentFlow({ defaultTimeZone })` — пояс, пока пояс человека неизвестен. Порядок: человек → страница →
42
+ `defaultTimeZone` → пояс службы → сервер. `prepareTurn` получает `pageTimeZone` — пояс, присланный страницей.
43
+ - `service.appendStaffMessage({ ownerId, chatId, text, staffName?, wakeAgent? })` — реплика живого сотрудника: модель
44
+ видит её словами сотрудника, `person_link_view` словами человека её не считает, проверки ответа агента её текст знают,
45
+ ход сама не начинает.
46
+ `flow.noteStaffReply` пишет тем же путём и принимает `staffName`. Экран: необязательный `StaffMessage` скина (договор 25),
47
+ без него — ответ агента с подписью `labels.notices.staffSaid`.
48
+ - `flow.tick()` в публичном типе, `factId` из пакета, перемотка до утра у чата, закреплённого через `adopt` без хода.
49
+
50
+ Как обновить приложение: снимите `it.fails` у теста «молчун все три дня»; ответ эксперта кладите
51
+ `appendStaffMessage` / `noteStaffReply` вместо сообщения собеседника и уберите обёртку `withoutNonPersonWords`; по желанию
52
+ `defaultTimeZone` и `StaffMessage` в своём скине; снимите `it.fails` у теста «касание, отложенное ночью до утра»;
53
+ доставляйте `message.parts` в своей доставке касаний; расход по чатам — `sumUsageByChat`.
54
+
55
+ ### Patch Changes
56
+
57
+ - Updated dependencies [b6a7786]
58
+ - @ykurilov/agent-chat-core@0.53.0
59
+
60
+ ## 0.52.0
61
+
62
+ ### Patch Changes
63
+
64
+ - @ykurilov/agent-chat-core@0.52.0
65
+
66
+ ## 0.51.0
67
+
68
+ ### Patch Changes
69
+
70
+ - Updated dependencies [41b1726]
71
+ - @ykurilov/agent-chat-core@0.51.0
72
+
73
+ ## 0.50.0
74
+
75
+ ### Patch Changes
76
+
77
+ - @ykurilov/agent-chat-core@0.50.0
78
+
79
+ ## 0.49.0
80
+
81
+ ### Patch Changes
82
+
83
+ - Updated dependencies [084cef7]
84
+ - @ykurilov/agent-chat-core@0.49.0
85
+
86
+ ## 0.48.0
87
+
88
+ ### Patch Changes
89
+
90
+ - Updated dependencies [fa10d5a]
91
+ - @ykurilov/agent-chat-core@0.48.0
92
+
93
+ ## 0.47.0
94
+
95
+ ### Patch Changes
96
+
97
+ - Updated dependencies [5a59c33]
98
+ - @ykurilov/agent-chat-core@0.47.0
99
+
100
+ ## 0.46.2
101
+
102
+ ### Patch Changes
103
+
104
+ - Updated dependencies [3d0d2f2]
105
+ - @ykurilov/agent-chat-core@0.46.2
106
+
107
+ ## 0.46.1
108
+
109
+ ### Patch Changes
110
+
111
+ - @ykurilov/agent-chat-core@0.46.1
112
+
113
+ ## 0.46.0
114
+
115
+ ### Patch Changes
116
+
117
+ - Updated dependencies [56d7528]
118
+ - Updated dependencies [a1933fd]
119
+ - Updated dependencies [52cc67d]
120
+ - @ykurilov/agent-chat-core@0.46.0
121
+
122
+ ## 0.45.0
123
+
124
+ ### Patch Changes
125
+
126
+ - @ykurilov/agent-chat-core@0.45.0
127
+
128
+ ## 0.44.0
129
+
130
+ ### Patch Changes
131
+
132
+ - Updated dependencies [6a2b5d9]
133
+ - @ykurilov/agent-chat-core@0.44.0
134
+
135
+ ## 0.43.0
136
+
137
+ ### Patch Changes
138
+
139
+ - Updated dependencies [763f4c5]
140
+ - @ykurilov/agent-chat-core@0.43.0
141
+
142
+ ## 0.42.0
143
+
144
+ ### Patch Changes
145
+
146
+ - Updated dependencies [a385276]
147
+ - @ykurilov/agent-chat-core@0.42.0
148
+
149
+ ## 0.41.0
150
+
151
+ ### Patch Changes
152
+
153
+ - Updated dependencies [9472fcb]
154
+ - @ykurilov/agent-chat-core@0.41.0
155
+
156
+ ## 0.40.0
157
+
158
+ ### Patch Changes
159
+
160
+ - Updated dependencies [0e7aab3]
161
+ - @ykurilov/agent-chat-core@0.40.0
162
+
163
+ ## 0.39.1
164
+
165
+ ### Patch Changes
166
+
167
+ - @ykurilov/agent-chat-core@0.39.1
168
+
169
+ ## 0.39.0
170
+
171
+ ### Patch Changes
172
+
173
+ - Updated dependencies [3b90429]
174
+ - @ykurilov/agent-chat-core@0.39.0
175
+
176
+ ## 0.38.0
177
+
178
+ ### Patch Changes
179
+
180
+ - Updated dependencies [1cae504]
181
+ - Updated dependencies [4065a4a]
182
+ - @ykurilov/agent-chat-core@0.38.0
183
+
184
+ ## 0.37.0
185
+
186
+ ### Patch Changes
187
+
188
+ - Updated dependencies [7f49d12]
189
+ - Updated dependencies [ea78982]
190
+ - @ykurilov/agent-chat-core@0.37.0
191
+
192
+ ## 0.36.0
193
+
194
+ ### Patch Changes
195
+
196
+ - Updated dependencies [3487fb4]
197
+ - Updated dependencies [8c43b86]
198
+ - Updated dependencies [6f42418]
199
+ - Updated dependencies [5f13c74]
200
+ - @ykurilov/agent-chat-core@0.36.0
201
+
202
+ ## 0.35.0
203
+
204
+ ### Patch Changes
205
+
206
+ - Updated dependencies [2382026]
207
+ - @ykurilov/agent-chat-core@0.35.0
208
+
209
+ ## 0.34.0
210
+
211
+ ### Patch Changes
212
+
213
+ - Updated dependencies [d039ce4]
214
+ - @ykurilov/agent-chat-core@0.34.0
215
+
216
+ ## 0.33.0
217
+
218
+ ### Patch Changes
219
+
220
+ - Updated dependencies [4a72339]
221
+ - @ykurilov/agent-chat-core@0.33.0
222
+
223
+ ## 0.32.0
224
+
225
+ ### Minor Changes
226
+
227
+ - 27f4b1e: Удаление чата стирает состояние его окон лимитов (окна области `chat`: начало скользящего окна, перенос перерасхода,
228
+ отправленные пороги). Раньше эти записи оставались в базе навсегда. Расход в журнале и окна человека и сервиса не трогаются.
229
+
230
+ Новое: необязательный метод хранилища `deleteSpendingLimitStates(keys)` (есть в памяти, SQLite и PostgreSQL) и
231
+ необязательный `chatDeleted({ userId, chatId })` у `SpendingLimits` — служба чата зовёт его после удаления чата;
232
+ ошибка уходит в журнал сервера и удаление не отменяет.
233
+
234
+ ### Как обновить приложение
235
+
236
+ Ничего, если хранилище встроенное. Своё хранилище может добавить `deleteSpendingLimitStates`; без него записи окон
237
+ удалённых чатов просто остаются, как раньше.
238
+
239
+ ### Patch Changes
240
+
241
+ - Updated dependencies [71c32b7]
242
+ - Updated dependencies [cae13bf]
243
+ - Updated dependencies [a94bda9]
244
+ - Updated dependencies [4e44492]
245
+ - Updated dependencies [ac9b7d3]
246
+ - Updated dependencies [27f4b1e]
247
+ - @ykurilov/agent-chat-core@0.32.0
248
+
249
+ ## 0.31.2
250
+
251
+ ### Patch Changes
252
+
253
+ - @ykurilov/agent-chat-core@0.31.2
254
+
255
+ ## 0.31.1
256
+
257
+ ### Patch Changes
258
+
259
+ - @ykurilov/agent-chat-core@0.31.1
260
+
261
+ ## 0.31.0
262
+
263
+ ### Patch Changes
264
+
265
+ - @ykurilov/agent-chat-core@0.31.0
266
+
267
+ ## 0.30.0
268
+
269
+ ### Patch Changes
270
+
271
+ - @ykurilov/agent-chat-core@0.30.0
272
+
273
+ ## 0.29.0
274
+
275
+ ### Patch Changes
276
+
277
+ - @ykurilov/agent-chat-core@0.29.0
278
+
279
+ ## 0.28.0
280
+
281
+ ### Minor Changes
282
+
283
+ - d63ccaa: Ломает приложения: закрытие чата теперь запись на ветке, а не флаг навсегда. Человек исправил сообщение до точки
284
+ закрытия, нажал «Переделать» или перешёл на другую ветку — на новой ветке чат открыт. Вернулся стрелкой назад — снова
285
+ закрыт. Отправка, «Повторить», подтверждения и виджеты в закрытом чате по-прежнему запрещены (409 `chat_closed`).
286
+
287
+ Плюс приложение узнаёт о смене состояния чата (`model.state`): на сервере — хук, на экране — проп и хук React.
288
+
289
+ - **Сервер:** `onStateChanged({ ownerId, chatId, reason, state, previousState, atMessageId })` у `createChatServer` и
290
+ `createChatService`. Зовётся после хода и смены ветки (`reason`: `turn`, `editMessage`, `regenerate`, `retry`,
291
+ `selectBranch`, `editAnswer`), только если состояние сменилось. Ход его не ждёт. Вызовы одного чата — по очереди.
292
+ Сбой — повторы (`onStateChangedAttempts`, 3) в сроке (`onStateChangedTimeoutMs`, 30 000), потом строка в `logError`.
293
+ Доставка только пока сервер жив: упал между ответом и хуком — хук не позовётся, сверяйте по `getChatState`.
294
+ - **События:** `state-changed` `{ chatId, reason, state, atMessageId }` (вкладка получает через `watch`) и
295
+ `state-changed-failed`. `chat-closed` и `chat-reopened` теперь приходят и при смене ветки.
296
+ - **Экран:** `<AgentChat onStateChange={(state) => …} />` — после загрузки и при каждой смене, в том числе по ходу
297
+ ответа и из другой вкладки (событие `state-changed`). Внутри ленты — `useChatState()`, у `useAgentChat` — `chatState`. Тип `ChatStateValues`.
298
+ - **Ответ сохраняется в базу сразу после итога каждого инструмента**, а не раз в полсекунды: запись состояния
299
+ переживает падение сервера. Промежуточные результаты (`preliminary`) — по-прежнему раз в полсекунды.
300
+ - **Служебные ключи** состояния (`$…`) больше не уходят в браузер: ни в истории, ни в потоке ответа.
301
+ - **`getChatState` дешёвый:** читает только связи сообщений и записи состояния. SQLite и Postgres получили метод
302
+ `listMessagePartsOfTypes(chatId, types)`. Новых таблиц и миграций нет.
303
+ - **Колонка `closed`** в записи чата — копия закрытия текущей ветки для `listChats`. `chatCapabilities` (`chat.closed`)
304
+ и история берут закрытие с ветки. Не записалась копия — её дописывает следующее открытие истории.
305
+ - **Чаты, закрытые в 0.27,** остаются закрытыми. Перенос в запись ленивый, без миграции: запись появится при первой
306
+ правке, «Переделать», выборе ветки или правке ответа в этом чате.
307
+ - Ядро экспортирует `CLOSED_STATE_KEY`, `isStateMessage`, `publicAppStateParts`, `DEFAULT_STATE_CHANGED_ATTEMPTS`,
308
+ `DEFAULT_STATE_CHANGED_TIMEOUT_MS`, типы `StateChangedHandler`, `StateChangedInput`, `StateChangeReason`,
309
+ `MessagePartsLink`.
310
+
311
+ ### Как обновить приложение
312
+
313
+ 1. **Закрытие должно быть навсегда** (нельзя уйти правкой или веткой): в настройках сервера (`createChatServer`)
314
+ добавьте права по закрытию.
315
+
316
+ Было: ничего.
317
+
318
+ Стало:
319
+
320
+ ```ts
321
+ chatCapabilities: ({ chat }) =>
322
+ chat.closed ? { edit: false, regenerate: false, branches: false, answerEditing: false } : {},
323
+ ```
324
+
325
+ Если `chatCapabilities` уже есть — допишите эту ветку в него. Если правка до закрытия вам подходит — шаг пропустите.
326
+ 2. **`switch` по событиям чата** (`ChatEvent`, `service.events`) с проверкой полноты (`never`): добавьте ветки
327
+ `state-changed` и `state-changed-failed`.
328
+ 3. **Своё хранилище** (не SQLite и не Postgres): ничего не обязательно. Для дешёвого чтения состояния добавьте
329
+ `listMessagePartsOfTypes(chatId, types)` → `[{ id, parentId, createdAt, parts }]` в порядке `listMessages`, только
330
+ части нужных типов. Общие тесты хранилища проверят его, флаг `partsOfTypes: "required"` делает его обязательным.
331
+ `setChatClosed` теперь пишет только копию для списка: без него закрытие работает, но список его не видит.
332
+ 4. **Своя реализация `ChatService`** (заглушки в тестах): полей не прибавилось. Если вы проверяли, что правка или
333
+ «Переделать» в закрытом чате дают `ChatClosedError`, — теперь они идут.
334
+ 5. **Таблица-копия состояния** (по желанию): `onStateChanged` с полной заменой записи —
335
+ `await db.upsertForm({ chatId, fields: state.fields ?? {} })`.
336
+ 6. Проверка: `pnpm typecheck`; закройте чат инструментом, исправьте вопрос до закрытия — без шага 1 чат открыт, с
337
+ шагом 1 правка недоступна.
338
+
339
+ ### Patch Changes
340
+
341
+ - Updated dependencies [11c3c97]
342
+ - Updated dependencies [d63ccaa]
343
+ - Updated dependencies [6a2f903]
344
+ - Updated dependencies [189cba9]
345
+ - @ykurilov/agent-chat-core@0.28.0
346
+
347
+ ## 0.27.0
348
+
349
+ ### Minor Changes
350
+
351
+ - d8341b2: Свои данные чата (`chat.data`) и защита таблиц SQLite. Приложения без новых опций работают как раньше: `subjectData` остался и равен `data`.
352
+
353
+ - **`chat.data`** — JSON-объект до 16 КБ у любого чата (раньше такие данные были только у чатов объектов). Лежит в том же столбце `subject_data`: миграции нет, старые данные чатов объектов уже читаются как `data`. В браузер и в запрос к модели данные не уходят: кэш модели не меняется.
354
+ - **Новый чат:** `service.createChat(userId, { data })`. Из браузера: `createChatServer({ newChatData: ({ user, request }) => data })` — данные ставит сервер, браузер их не присылает. Вернули не объект — 500. Хранилище без `updateChatData` с `newChatData` — ошибка при создании обработчика; `createChat({ data })` на таком хранилище — та же понятная ошибка, данные молча не теряются.
355
+ - **Правка:** `service.updateChatData(userId, chatId, patch)` — сливает верхние ключи (ключ со значением `undefined` удаляется, `null` остаётся значением). Итог больше 16 КБ — `TypeError`, данные не меняются. Возвращает новые данные. Правка атомарная: две одновременные правки разных ключей не затирают друг друга. Новое значение видно со следующего хода. Верхний ключ `__proto__` в данных, правке и фильтре не принимается (`TypeError`): иначе значение тихо пропало бы при слиянии.
356
+ - **Список:** `service.listChats(userId, { data: { formId: "f1", mode: "test" } })` — чаты, у которых все указанные верхние ключи равны значениям. Значения только строка, число, булево или `null` (иначе `TypeError`); `1` и `"1"` — разные значения. Можно вместе с `subjects`. SQLite ищет через `json_each`, PostgreSQL — через `jsonb @>`.
357
+ - **Кто видит данные:** `beforeTurn` и `chatInstructions` — `chat.data`; инструменты — `model.chat.data` (в том числе при сжатии и у фоновых помощников); `afterTurn` — `turn.chatData` (данные на момент итога: правка посреди хода видна, и итог после перезапуска даёт то же). `chatCapabilities`, проверка виджета и `stepContext` получают `Chat` и тоже видят `data`.
358
+ - **Событие `chat-data-changed { ownerId, chatId }`** (без данных) для `onChatEvent`. Вкладке браузера оно не пересылается.
359
+ - **Своё хранилище:** добавьте необязательный метод `updateChatData(chatId, patch, { maxBytes })` (одна атомарная операция, возвращает чат или `null`; больше предела — `ChatDataTooLargeError`), `listChats(ownerId, { subjects?, data? })` и запись/чтение `data` (у старого кода — `subjectData`). Без `updateChatData` служба отвечает понятной ошибкой на `updateChatData` и `listChats({ data })`. Проверка — `describeChatStorageContract(…, { chatData: "required" })`.
360
+ - **Песочница:** в копии чата человека инструменты и `afterTurn` видят данные исходного чата, а не служебную обёртку копии; это верно и для обычного чата без ключа объекта (`subject` у него по-прежнему нет).
361
+ - **SQLite: защита от чужих таблиц.** Таблицы SDK не переименованы: SDK должен жить в своём файле. При открытии, до миграций, SDK смотрит, нет ли в файле чужих `chats` или `messages` (журнал миграций SDK не знает их). Тогда `createSqliteChatStorage` бросает `ForeignChatTablesError` с текстом «дайте SDK отдельный файл», файл не тронут. Если у приложения свои `chats`/`messages` в том же файле — укажите SDK другой `filePath`. Раздел «Где лежат данные SDK» — в README хранилищ.
362
+ - 5e4ed28: **Что может сломаться:**
363
+
364
+ - исчерпывающий `switch` по виду ошибки сервера (`HandlerErrorKind`) и виду отказа на экране (`AppRefusalKind`): новые значения `action_forbidden`, `chat_closed`, `chat_creation_refused`;
365
+ - исчерпывающий `switch` по причине `turnNotStarted` (`TurnNotStartedReason`): новое значение `chat_closed`;
366
+ - свой словарь надписей `ChatLabels`, собранный целиком: новое обязательное поле `chatListClosed`.
367
+
368
+ Права человека в чате и закрытый чат. Приложение функцией `chatCapabilities` решает, что можно человеку: правка, «Переделать», «Повторить», ветки, выбор модели, правка ответа, список чатов, сжатие, файлы, голос. Чат можно закрыть из инструмента (`model.closeChat`) или из кода (`service.closeChat`, `reopenChat`): переписка видна, писать дальше нельзя. Хук `beforeCreateChat` решает, можно ли человеку завести новый чат. Без новых настроек всё работает как раньше. Подробно — [docs/in-product-agent.md](https://github.com/ykurilov/agent-chat-sdk/blob/main/docs/in-product-agent.md), разделы 9, 10 и 11.
369
+
370
+ - Права: опечатка в названии права (`edits` вместо `edit`) — ошибка приложения (`TypeError`), а не молчаливый доступ. За один вызов службы права спрашиваются не больше раза.
371
+ - Закрытие побеждает паузу: если чат закрыт, пока ответ ждёт подтверждения или виджета, ждущие запросы закрываются отказом, как при «Стопе», и ответ считается законченным.
372
+ - Закрытие из инструмента записывается сразу при вызове: после падения сервера чат уже закрыт. Чат, закрытый во время проверки перед ходом, хода не начинает: 409 `chat_closed`, резерв приложения закрывается `turnNotStarted` с причиной `chat_closed`.
373
+ - Новый чат: `beforeCreateChat({ userId, origin? })` вернул `{ code, message }` — `POST {base}` отвечает `403 { error: { kind: "chat_creation_refused", code, message } }`, мессенджер (`/new`, первый чат) — текстом. Чаты объектов и импорт хук не спрашивают.
374
+ - `listChats` отдаёт `closed`: на экране и в `/chats` мессенджера у закрытого чата пометка.
375
+ - Сервер: запрещённое действие — `403 { error: { kind: "action_forbidden", action, code, message } }`, закрытый чат — `409 { error: { kind: "chat_closed", code, message } }`. Мессенджеры отвечают человеку текстом.
376
+ - Мессенджеры: голосовое при `voice: false` и файлы при `attachments: false` не скачиваются и не распознаются, `/chats` смотрит на `chatList`. Своя служба может реализовать необязательный `assertCapability`.
377
+ - Песочница: закрытие работает как в бою, а кнопка «Открыть чат снова» (`POST {sandboxBase}/chats/:chatId/reopen`, только админ) позволяет продолжить тест. Скину песочницы — необязательный `SandboxSkin.ChatReopen`, надписи `SandboxLabels.chatReopen` (необязательные); в Тандеме кнопка своя.
378
+ - Экран: кнопки по правам прячутся сами, после ответа 403 `action_forbidden` права перечитываются, закрытый чат блокирует поле и показывает текст закрытия. Голосовое сообщение при `voice: false` не уходит. Новое:
379
+ - `AppRefusalKind` — `chat_creation_refused`; `useChatList` — `onCreateRefused` и `creationRefusal`, `ChatWorkspace` отдаёт отказ в `onTurnRefused`;
380
+ - `ChatListEntry.closed` и `ChatListItemView.closed` — пометка закрытого чата в списке, надпись `chatListClosed` («Закрыт», обязательное поле `ChatLabels`);
381
+ - `reloadKey` у `AgentChat`, `useAgentChat` и `ChatWorkspace` — перечитать историю, когда чат изменили не через экран;
382
+ - `onChatUnavailable` у `AgentChat` — служебный колбэк для `ChatWorkspace`, приложению не нужен;
383
+ - `ChatWorkspace` при `chatList: false` не мигает списком: он появляется после первой истории (и при `chatList: true` — тоже с этой короткой задержкой) и потом не запрашивается;
384
+ - `onTurnRefused` получает новые виды отказов, `onChatAccessChange` сообщает права и закрытие, `ChatWorkspace` при `chatList: false` показывает только чат.
385
+ - Скин: договор v20 — необязательный `ChatClosedNotice` и необязательный `list` у `ChatListSkin.ChatWorkspace` ([docs/skin-v20.md](https://github.com/ykurilov/agent-chat-sdk/blob/main/docs/skin-v20.md)). Скин v19 работает без правок. В Тандеме — `TandemChatClosedNotice`.
386
+ - Хранилища: колонка `closed` у чатов, миграция SQLite и PostgreSQL применяется сама. Своё хранилище может реализовать необязательный `setChatClosed(chatId, closed | null)` и отдавать `closed` в `getChat`; общий тест — `describeChatStorageContract(…, { chatClosure: "required" })`.
387
+
388
+ ### Как обновить приложение
389
+
390
+ 1. Найдите в коде приложения исчерпывающие `switch` по виду ошибки сервера чата, по `refusal.kind` из `onTurnRefused` и по `reason` в `turnNotStarted`. Искать: `kind: "turn_refused"`, `case "turn_refused"`, `case "access_refused"`, `HandlerErrorKind`, `AppRefusalKind`, `TurnNotStartedReason`, `case "busy"`.
391
+ 2. Добавьте ветки для новых видов:
392
+
393
+ Было:
394
+
395
+ ```ts
396
+ switch (refusal.kind) {
397
+ case "turn_refused": return showLessonClosed(refusal.message);
398
+ case "access_refused": return showNoAccess(refusal.message);
399
+ default: { const never: never = refusal.kind; return never; }
400
+ }
401
+ ```
402
+
403
+ Стало:
404
+
405
+ ```ts
406
+ switch (refusal.kind) {
407
+ case "turn_refused": return showLessonClosed(refusal.message);
408
+ case "access_refused": return showNoAccess(refusal.message);
409
+ case "chat_closed":
410
+ case "action_forbidden": return; // чат уже показал текст сам
411
+ case "chat_creation_refused": return showToast(refusal.message);
412
+ default: { const never: never = refusal.kind; return never; }
413
+ }
414
+ ```
415
+
416
+ В `switch` по `reason` в `turnNotStarted` добавьте `case "chat_closed":` рядом с `case "busy":`: резерв снимается так же.
417
+
418
+ 3. Свой словарь `ChatLabels`, собранный целиком (не поверх `russianChatLabels`): добавьте `chatListClosed: "Закрыт"`.
419
+ 4. Своё хранилище чата (не SQLite и не PostgreSQL из SDK): если нужен закрытый чат, реализуйте `setChatClosed` и отдавайте `closed` в `getChat`. Без этого `closeChat` бросает ошибку, а инструменты не получают `model.closeChat`.
420
+ 5. Проверка: `pnpm typecheck` проходит, чат открывается и отвечает.
421
+
422
+ Заметка к 0.28: закрытие станет записью хода, и правка, «Переделать» и ветки в закрытом чате станут возможны. Чтобы сохранить нынешнее поведение, задайте права: `chatCapabilities: ({ chat }) => chat.closed ? { edit: false, regenerate: false, branches: false } : {}`.
423
+
424
+ ### Patch Changes
425
+
426
+ - Updated dependencies [d8341b2]
427
+ - Updated dependencies [5e4ed28]
428
+ - Updated dependencies [e0bb519]
429
+ - @ykurilov/agent-chat-core@0.27.0
430
+
431
+ ## 0.26.2
432
+
433
+ ### Patch Changes
434
+
435
+ - Updated dependencies [f24858a]
436
+ - @ykurilov/agent-chat-core@0.26.2
437
+
438
+ ## 0.26.1
439
+
440
+ ### Patch Changes
441
+
442
+ - @ykurilov/agent-chat-core@0.26.1
443
+
444
+ ## 0.26.0
445
+
446
+ ### Patch Changes
447
+
448
+ - Updated dependencies [eb43e47]
449
+ - @ykurilov/agent-chat-core@0.26.0
450
+
451
+ ## 0.25.0
452
+
453
+ ### Patch Changes
454
+
455
+ - Updated dependencies [99d426e]
456
+ - Updated dependencies [d8201f6]
457
+ - @ykurilov/agent-chat-core@0.25.0
458
+
459
+ ## 0.24.0
460
+
461
+ ### Minor Changes
462
+
463
+ - 9b895cd: Ломает приложения: только исчерпывающие `switch` по видам ошибок сервера и событиям чата (новые значения) и код, который
464
+ сам читает ответ `resolveUser`. Без новых настроек поведение прежнее.
465
+
466
+ Агент внутри продукта: агент работает с объектами приложения — уроком, заказом, документом. Всё по желанию; без новых
467
+ настроек запрос к модели не меняется ни на байт. Подробно — `docs/in-product-agent.md`.
468
+
469
+ - **Проверка перед ходом.** `beforeTurn({ userId, chat, action, text? })` отказывает своими словами: 403 `turn_refused`
470
+ до любых записей и лимитов, на отправке, правке, повторе, переделке, подтверждении, виджете, служебных сообщениях, фоновой
471
+ задаче и в мессенджерах. `resolveUser` может вернуть `{ refused: { code, message, status? } }` → `access_refused` на всех
472
+ адресах чата, голоса, голосового режима (WebSocket 4401) и мессенджеров. React: `onTurnRefused`, текст отказа вместо «нет
473
+ связи», сообщение возвращается в поле. Ручная проверка — `service.assertTurnAllowed`.
474
+ - **Итог хода.** `afterTurn(turn)` после каждого хода с повторами и сроком (`afterTurnAttempts`, `afterTurnTimeoutMs`),
475
+ переживает перезапуск сервера. `TurnResult`: владелец и автор хода, текст, части, расход, действие, канал, объект,
476
+ сотрудник. События приложения `{ type, data? }` — из хука, из инструмента (`model.emitAppEvent`) и вне хода
477
+ (`service.publishAppEvent`); в React `appEvents` и `onAppEvent`, события переживают перезагрузку. Модели не уходят.
478
+ - **Чат объекта.** Ключ и данные объекта у чата, один чат на ключ у человека: `service.openSubjectChat`, на сервере
479
+ `subjectChats` (`POST …/subject`), в React `<AgentChat subject="lesson:42" />`. Данные объекта видят `beforeTurn`,
480
+ инструменты (`model.subject`) и `afterTurn`; в браузер и модели не уходят.
481
+ - **Приветствие.** `greeting: { text }` — первое сообщение от агента без вызова модели. Проверено на всех моделях каталога.
482
+ - **Лимит сообщений.** Окно лимитов с `limitMessages` вместо `limitUsd`, `exemptUsers` — люди без лимитов,
483
+ `onMessageThreshold` — пороги. Отказ — 429 с `unit: "messages"`.
484
+ - **Импорт старой переписки.** `service.importChat({ ownerId, chat, messages })` с датами и картинками
485
+ (`attachments.importFile`), без хода, памяти, расхода и лимитов.
486
+ - **Сотрудник проверяет чат человека.** Песочница с опцией `people`: админ видит чаты человека только на чтение и
487
+ проверяет ответы в копии у себя. Память человека в копии только читается, расход — на админа, человек ничего не видит.
488
+ Инструменты приложения получают `model.staff` и в копии в данные человека не пишут.
489
+ - **Свои кнопки у поля ввода.** `composerActions` с помощником `{ value, setValue, send, addFiles, busy, blocked, canSend }`.
490
+ Договор скина v19: `Composer.actions`; скин v18 работает без кнопок, Тандем готов.
491
+ - **Хранилища.** Миграции применяются сами: SQLite `0015`, PostgreSQL `0014` (ключ объекта, состояние хука итога).
492
+
493
+ ### Что может сломаться
494
+
495
+ - **Исчерпывающие `switch` по закрытым спискам.** Новые значения: вид ошибки сервера `turn_refused` и `access_refused`
496
+ (`HandlerErrorKind`), события чата `after-turn-failed` и `app-event` (`ChatEvent`), `unit` у отказа лимита. Добавьте ветки
497
+ и надписи.
498
+ - **Список чатов без чатов объектов.** `listChats` и `GET {basePath}` по умолчанию не показывают чаты с ключом объекта.
499
+ Касается только приложений, которые начнут ими пользоваться; показать — `chatList: { subjectChats: true }` или
500
+ `listChats(userId, { subjects: "include" })`.
501
+ - **Свой `resolveUser` в обёртках.** Тип ответа — `ResolvedUser` (`{ id } | { refused } | null`). Код, который сам вызывает
502
+ `resolveUser` и читает `.id`, сначала проверяет `"refused" in user`.
503
+ - **Своё хранилище.** Для новых возможностей нужны необязательные методы (`findChatBySubject`, `importChat`,
504
+ `setAfterTurnState`, `listPendingAfterTurn`); без них эти возможности при включении дают ошибку при старте, остальное
505
+ работает. `listChats(ownerId, options?)` получил второй параметр — чаты с ключом скрывать по умолчанию.
506
+ - **Свой `ChatService`** (заглушки в тестах) добавляет новые методы: `openSubjectChat`, `importChat`, `assertTurnAllowed`, `peekHistory`,
507
+ `publishAppEvent`.
508
+ - **Своя реализация `SpendingLimits`** может добавить `takeMessage` — без него сообщения не считаются.
509
+ - **`AgentChat`: `chatId` стал необязательным** — нужен ровно один из `chatId` и `subject`; ни одного или оба — ошибка при
510
+ рендере. При смене `chatId` чат пересоздаётся: черновик и состояние прошлого чата не переносятся.
511
+ - **Окна лимитов: `limitUsd` стал необязательным** (у окна сообщений его нет). Код, который читает свои окна, проверяет
512
+ `limitMessages`.
513
+ - **Отказ сервера в React** (`ServerRefusal`) получил `kind` и `code`. 401 со словами приложения (`access_refused`) — это
514
+ отказ, а не «вход истёк».
515
+ - **«Пустой чат»** для «Нового чата»: чат объекта пустым не считается, обычный чат с одним приветствием — считается.
516
+ - Новый подпуть `@ykurilov/agent-chat-core/access` — помощники отказа в доступе для своих обработчиков.
517
+
518
+ ### Как обновить приложение
519
+
520
+ 1. `pnpm up "@ykurilov/agent-chat-*@^0.24.0"`. Миграции базы применяются сами при открытии хранилища.
521
+ 2. Исчерпывающие `switch` по `HandlerErrorKind` (сервер) и `ChatEvent` (`chatEvents` ядра): найдите их поиском и добавьте
522
+ ветки `turn_refused`, `access_refused`, `after-turn-failed`, `app-event`. Обязательных надписей не прибавилось: текст
523
+ отказа приходит с сервера. Свой словарь для лимита сообщений — необязательные `spendingMessagesLeft`,
524
+ `spendingMessagesExhausted`, `editMessageLimit`.
525
+
526
+ Было: `switch (event.type) { case "tasks-changed": … case "notice-added": … default: assertNever(event) }`
527
+ Стало: те же ветки плюс `case "after-turn-failed": case "app-event": break;`
528
+ 3. Свой код, который сам зовёт `resolveUser` и читает `.id`: сначала `if (user && "refused" in user) …`.
529
+ 4. Ничего больше не нужно, если новые возможности не включать. Чтобы включить — `docs/in-product-agent.md`.
530
+ 5. Свой скин: кнопки у поля появятся, когда `Composer` начнёт рисовать `actions` (договор v19, `docs/skin-v19.md`).
531
+ 6. Проверка: `pnpm typecheck` без ошибок, чат открывается и отвечает как раньше.
532
+
533
+ ### Patch Changes
534
+
535
+ - Updated dependencies [9b895cd]
536
+ - @ykurilov/agent-chat-core@0.24.0
537
+
538
+ ## 0.23.0
539
+
540
+ ### Patch Changes
541
+
542
+ - @ykurilov/agent-chat-core@0.23.0
543
+
544
+ ## 0.22.0
545
+
546
+ ### Patch Changes
547
+
548
+ - @ykurilov/agent-chat-core@0.22.0
549
+
550
+ ## 0.21.0
551
+
552
+ ### Minor Changes
553
+
554
+ - 7c3bcc4: Картинки в файлы приложения и ход, который не ждёт картинку. Всё новое включается опциями: без них `createImageGeneration` работает как раньше.
555
+
556
+ - `createImageQueue` — своя очередь картинок для инструментов приложения: заказ отвечает сразу, готовая картинка приходит в `deliver(image, job, ctx)`; `ctx.isCurrent()` и `ctx.signal` — картинка ещё нужна (запись файла делать условной). Задачи лежат в базе (`sqliteImageJobStore` из `@ykurilov/agent-chat-images/sqlite`, `postgresImageJobStore` из `/postgres`), переживают перезапуск сервера, расход пишется ровно один раз. Есть «Повторить» (`retry`), замена прежнего заказа на то же место (`key`), предел за ход, `stop()`.
557
+ - `createImageGeneration`: `deliver` — картинка приложению, а не во вложения; `mode: "background"` — инструмент отвечает сразу (с `jobStore`, `pollIntervalMs`, `attempts`, `onQueued`); `maxImagesPerTurn` — предел картинок за ход; `start()` и `stop()`.
558
+ - Ядро: `agent.tools(model)` получает `model.turnId` — номер хода, один на все попытки и запасного провайдера.
559
+ - Ядро: необязательный `ChatStorage.hasUsageRecord(id)` — есть ли запись расхода (память, SQLite, PostgreSQL). Своё хранилище может его не делать.
560
+ - Хранилища задач картинок: одна открытая задача на место (`ownerId` + `key`) — уникальный индекс; своё хранилище бросает `ImageJobKeyTakenError`.
561
+ - Экспорт `DEFAULT_REMOVE_BACKGROUND_PROMPT` — подсказка разделения фона.
562
+ - Защита денег: срок у каждого обращения к провайдеру, повторное скачивание того же адреса вместо нового заказа, второй шаг не оплачивает первый заново.
563
+
564
+ Фоновый режим и очередь с базой: при остановке сервера до закрытия базы вызвать `await images.stop()` (или `imageQueue.stop()`).
565
+
566
+ ### Patch Changes
567
+
568
+ - Updated dependencies [7c3bcc4]
569
+ - @ykurilov/agent-chat-core@0.21.0
570
+
571
+ ## 0.20.0
572
+
573
+ ### Minor Changes
574
+
575
+ - fd16b01: В таблицу чатов добавлено поле происхождения чата (какой мессенджер его создал). Миграция применяется сама при открытии базы (SQLite — `0014_chat_origin`, PostgreSQL — `0013_chat_origin`), старые чаты остаются как есть. Своё хранилище чатов (не из этих пакетов) может сохранять `origin` из `createChat`, но не обязано: без него чат просто не получит значок мессенджера.
576
+
577
+ Что сделать в приложении: ничего.
578
+
579
+ ### Patch Changes
580
+
581
+ - Updated dependencies [fd16b01]
582
+ - @ykurilov/agent-chat-core@0.20.0
583
+
584
+ ## 0.19.0
585
+
586
+ ### Minor Changes
587
+
588
+ - aa0fd51: Хранилища держат состояние лимитов расходов: начало скользящего окна, перенос перерасхода, ручной сброс, личные лимиты и
589
+ блокировку человека, отметки о порогах. Новая таблица `spending_limit_states` (миграция SQLite `0013`, PostgreSQL `0012`
590
+ применяется сама при открытии хранилища). Запись по версии: из двух одновременных записей с одной версией проходит одна,
591
+ поэтому две копии сервера не затрут друг друга. В `ChatStorage` — необязательные `getSpendingLimitState(key)` и
592
+ `saveSpendingLimitState(key, value, expectedVersion)`, тип `SpendingLimitStateRecord`.
593
+
594
+ Обновление ничего не требует. Своё хранилище работает как раньше. Если с ним нужны окна «N часов», личные настройки, сброс
595
+ или пороги из `@ykurilov/agent-chat-limits`, реализуйте эти два метода и проверьте общими тестами
596
+ `describeChatStorageContract(createStorage, { spendingLimitState: "required" })`.
597
+
598
+ ### Patch Changes
599
+
600
+ - Updated dependencies [fde2297]
601
+ - Updated dependencies [aa0fd51]
602
+ - Updated dependencies [9f94dbe]
603
+ - Updated dependencies [2013f15]
604
+ - Updated dependencies [aa0fd51]
605
+ - Updated dependencies [05e258b]
606
+ - Updated dependencies [aa0fd51]
607
+ - @ykurilov/agent-chat-core@0.19.0
608
+
609
+ ## 0.18.6
610
+
611
+ ### Patch Changes
612
+
613
+ - @ykurilov/agent-chat-core@0.18.6
614
+
615
+ ## 0.18.5
616
+
617
+ ### Patch Changes
618
+
619
+ - @ykurilov/agent-chat-core@0.18.5
620
+
621
+ ## 0.18.4
622
+
623
+ ### Patch Changes
624
+
625
+ - @ykurilov/agent-chat-core@0.18.4
626
+
627
+ ## 0.18.3
628
+
629
+ ### Patch Changes
630
+
631
+ - @ykurilov/agent-chat-core@0.18.3
632
+
633
+ ## 0.18.2
634
+
635
+ ### Patch Changes
636
+
637
+ - @ykurilov/agent-chat-core@0.18.2
638
+
639
+ ## 0.18.1
640
+
641
+ ### Patch Changes
642
+
643
+ - @ykurilov/agent-chat-core@0.18.1
644
+
645
+ ## 0.18.0
646
+
647
+ ### Patch Changes
648
+
649
+ - Updated dependencies [43d7ebe]
650
+ - @ykurilov/agent-chat-core@0.18.0
651
+
652
+ ## 0.17.0
653
+
654
+ ### Patch Changes
655
+
656
+ - @ykurilov/agent-chat-core@0.17.0
657
+
658
+ ## 0.16.0
659
+
660
+ ### Minor Changes
661
+
662
+ - cf52f10: **Ломает только исчерпывающие проверки:** у `UsageSource` появилось значение `images`, у видов ошибок сервера — `widget_not_pending` и `invalid_answer`, у `ChatService` — метод `answerWidget`. Своё хранилище должно поддержать условную запись `AwaitingMessagePatch.expectedParts` (при несовпадении частей — `MessageNotAwaitingError`; помощник `sameMessageParts` из core), иначе две копии сервера могут затереть ответы друг друга; общие тесты хранилищ это проверяют. Остальное необязательно, приложение без правок работает как раньше.
663
+
664
+ Что нового: агент может спросить кнопками, звёздами или формой (виджеты), нарисовать схему блоком ` ```mermaid ` и создать картинки через Runware. Всё включается отдельно:
665
+
666
+ - виджеты: `agent: { widgets: { basic: true, custom: [свойВиджет] } }`, свой виджет — `defineWidget` из core и компонент в `widgets={{ … }}` у `AgentChat`; ответ человека идёт на `POST /:chatId/widgets/:toolCallId`, ход ждёт со статусом `awaiting_approval`;
667
+ - схемы: новый пакет `@ykurilov/agent-chat-diagrams`, `agent: { diagrams: true }` на сервере и `diagrams={diagramsClient}` на странице;
668
+ - картинки: новый пакет `@ykurilov/agent-chat-images` (нужен пакет вложений и `RUNWARE_API_KEY`), `images.tools(model)` в `agent.tools`, `images={imagesClient}` на странице; свой срок ожидания (210 с) инструмент сообщает ядру сам (`declareToolTimeout`), расход бронируется до заказа (`assertWithinLimit({ pendingUsd })`);
669
+ - окно просмотра картинок и схем: Тандем готов, своему скину — необязательные части договора версии 13 (`Widget`, `SuggestedReplies`, `GeneratedImages`, `MediaViewer`, `diagramColors`, `renderDiagram`). `splitMermaidBlocks` экспортируется из react;
670
+ - вложения: `Attachments.storage` и `detectFileKind` экспортируются из пакета вложений;
671
+ - голосовой режим произносит вопрос и варианты ждущего виджета и говорит «схема на экране» вместо блока схемы;
672
+ - «Стоп» на экране закрывает висящие вызовы так же, как сервер, разрешённые без результата — «результат неизвестен» (`stoppedToolCallReasons.resultUnknown`);
673
+ - упавший свой виджет страницы заменяется запасным видом скина, лента цела.
674
+
675
+ Подробно: `docs/widgets.md`, `docs/diagrams.md`, `docs/images.md`, `docs/media-viewer.md`.
676
+
677
+ ### Как обновить приложение
678
+
679
+ Обычному приложению на SQLite или PostgreSQL из набора делать ничего не нужно: миграций нет, всё новое включается отдельно.
680
+
681
+ 1. Исчерпывающий `switch` по `UsageSource` — добавьте ветку `"images"`; по видам ошибок сервера — `"widget_not_pending"` и `"invalid_answer"`.
682
+ 2. Своя реализация `ChatService` — добавьте метод `answerWidget({ chatId, ownerId, toolCallId, answer, page }): Promise<{ resumed: boolean }>` (тип `WidgetAnswerRequest` из core, образец — служба из core).
683
+ 3. Своё хранилище: `updateAwaitingMessage` получает необязательное поле `expectedParts`. Если оно есть и части сообщения в базе с ним не совпадают (`sameMessageParts` из core), бросайте `MessageNotAwaitingError`; проверку и запись делайте в одной транзакции. Прогоните общие тесты хранилищ из core.
684
+ 4. Своя обёртка `assertWithinLimit` из limits — пробрасывайте новое необязательное поле `pendingUsd`.
685
+ 5. Проверка: `pnpm typecheck`, затем в чате отправить сообщение и получить ответ.
686
+
687
+ ### Patch Changes
688
+
689
+ - Updated dependencies [cf52f10]
690
+ - @ykurilov/agent-chat-core@0.16.0
691
+
692
+ ## 0.15.0
693
+
694
+ ### Patch Changes
695
+
696
+ - Updated dependencies [f05ad23]
697
+ - Updated dependencies [f05ad23]
698
+ - @ykurilov/agent-chat-core@0.15.0
699
+
700
+ ## 0.14.0
701
+
702
+ ### Minor Changes
703
+
704
+ - 656233a: Долгая память агента. Агент сам запоминает устойчивое о человеке (предпочтения, правила общения, факты о работе) и помнит это в новых чатах. В новом пакете `@ykurilov/agent-chat-memory` лежат инструмент `manage_memory`, правила записей и блок памяти для запроса. Человек видит записи на экране памяти: может добавить, исправить, удалить запись или стереть всю память. Записи у каждого человека свои, удаление чата их не трогает. Раз в сутки после ответа та же модель предлагает слить повторные записи (опция `optimizer`); код проверяет группы и применяет годные, на экране появляется раздел «Объединено» с кнопкой «Вернуть как было». Отрицание не теряется («не любит кофе» не склеивается с «любит кофе»). После сбоев проверка ждёт дольше: час, 6 часов, сутки. Пределы записей и дубли держатся при одновременных действиях (замок на человека, а в SQLite и PostgreSQL ещё и проверка внутри транзакции), в том числе при возврате склейки. Память не меняет начало запроса к модели, поэтому кэш модели сохраняется.
705
+
706
+ **Ломает приложения** только с полным словарём надписей `ChatLabels`, написанным вручную (шаг 5): в него добавились надписи `memory…`. Остальным обновление ничего не ломает: без опции `memory` чат работает как раньше. Чтобы включить память, выполните шаги ниже. Новые адреса: `/api/chat/memory/merges` и `/api/chat/memory/merges/:runId/revert`. Миграции SQLite `0009` и `0010` и PostgreSQL `0008` и `0009` применяются сами при открытии базы (`0010` и `0009` — задел под склейку повторов записей).
707
+
708
+ ### Как обновить приложение
709
+
710
+ 1. **Включить память на сервере** (по желанию). Пакет ставится отдельно: `pnpm add @ykurilov/agent-chat-memory`. В файле, где создаётся `createChatHandler`:
711
+
712
+ Было:
713
+
714
+ ```ts
715
+ createChatHandler({ basePath: "/api/chat", storage, resolveUser /* … */ });
716
+ ```
717
+
718
+ Стало:
719
+
720
+ ```ts
721
+ import { createLongTermMemory } from "@ykurilov/agent-chat-memory";
722
+
723
+ createChatHandler({ basePath: "/api/chat", storage, resolveUser, memory: createLongTermMemory({ storage }) /* … */ });
724
+ ```
725
+
726
+ Инструкция агента и инструмент `manage_memory` добавляются сами: в `agent.instructions` и `agent.tools` ничего дописывать не нужно. Имя `manage_memory` занято: своего инструмента с таким именем быть не должно (иначе ошибка при создании). Адреса `/api/chat/memory` появляются сами. Без `memory` они отвечают `404`.
727
+ 2. **Подключить экран памяти** (по желанию). Найдите страницу, куда его поставить, и добавьте компонент; `createMemoryApi` вызывайте один раз (например, в `useMemo`):
728
+
729
+ ```tsx
730
+ import { AgentMemory, createMemoryApi } from "@ykurilov/agent-chat-react";
731
+ import { tandemMemorySkin } from "@ykurilov/agent-chat-skin-tandem";
732
+
733
+ const memoryApi = useMemo(() => createMemoryApi({ api: "/api/chat" }), []);
734
+ <AgentMemory api={memoryApi} skin={tandemMemorySkin} />
735
+ ```
736
+ 3. **Подпись вызова в ленте.** Было: `labels={russianLabels}`. Стало: `labels={withMemoryToolTitles(russianLabels)}`, где `withMemoryToolTitles` — из `@ykurilov/agent-chat-memory/labels`. Вместо `manage_memory` в ленте будет «Работа с памятью».
737
+ 4. **Свой скин (договор скина версии 11).** `ChatSkin` и `ChatListSkin` не менялись. Экрану памяти нужен отдельный `MemorySkin` из пяти компонентов (`MemoryScreen`, `MemoryGroup`, `MemoryNoteRow`, `MemoryNoteEditor`, `MemoryConfirm`). Скин Тандема даёт его готовым: `tandemMemorySkin`. Описание — `specs/017-long-term-memory/contracts/memory.md`, раздел 5.
738
+ 5. **Полный словарь надписей.** Если приложение задаёт объект `ChatLabels` целиком, а не `{ ...russianLabels, … }`, добавьте новые надписи: все ключи, которые начинаются с `memory` (`memoryHeading`, `memoryAdd`, `memoryClearAll` и другие, полный список — тип `ChatLabels` в `@ykurilov/agent-chat-react`). Без этого `pnpm typecheck` не пройдёт.
739
+ 6. **Своё хранилище.** Нужно только при включённой памяти. В своей реализации `ChatStorage` добавьте пять необязательных методов: `listMemoryNotes`, `insertMemoryNotes`, `updateMemoryNote`, `deleteMemoryNote`, `clearMemoryNotes`. Сигнатуры — в `ChatStorage` пакета core и в `packages/storage-sqlite/src/sqlite-storage.ts`. Без них `createLongTermMemory` остановится с ошибкой `MemoryStorageNotSupportedError` и списком недостающих методов. Для склейки повторов нужны ещё шесть методов (`claimMemoryOptimizationRun`, `finishMemoryOptimizationRun`, `applyMemoryMerge`, `revertMemoryMerge`, `listMemoryOptimizationRuns`, `listMergedMemoryNotes`); без них склейка выключена. Чтобы пределы держались при нескольких серверах, добавьте необязательные `insertMemoryNotesChecked` и `updateMemoryNoteChecked`. Общие тесты `describeChatStorageContract` проверят их сами.
740
+ 7. **Проверка.** `pnpm typecheck` и сборка проходят. Напишите агенту «Запомни, что я пишу на TypeScript», откройте экран памяти: там запись в разделе, который выбрал агент. В новом чате спросите, что он про вас знает.
741
+ - 0cdf102: Несколько копий сервера на одной базе (022, этап 8в). Идущий ответ раз в 5 с отмечает «жив» (новая колонка `alive_at`: миграция SQLite `0012`, PostgreSQL `0011`, применяются сами). `recoverInterruptedAnswers` прерывает только брошенные ответы — без отметки дольше 15 с — и дальше сам проверяет их раз в 5 с до `flush`. Штатная остановка помечает свои ответы брошенными, и следующий старт прерывает их сразу. Новая копия при старте и обновление с перекрытием больше не обрывают живые ответы соседа; две копии, прервавшие один ответ, не задваивают расход. Цена: после падения сервера ответ ещё до ~20 с выглядит идущим (раньше перезапуск прерывал его сразу). Экран человека в это время сам перечитывает историю раз в 3 с и показывает «прерван» с «Повторить», как только сервер прервёт ответ; обновлять страницу не нужно (история отдаёт признак `detachedAnswer`, чтение истории сразу прерывает брошенный дольше порога ответ этого чата). Ход, чей ответ прервала другая копия, останавливается сам: человек видит «прерван», расход не задваивается. Время знака «жив» ставит хранилище: PostgreSQL — по часам базы, часы машин с копиями могут расходиться. Запросы одного чата держите на одной копии. Свои хранилища: два новых необязательных метода — `listSilentAnswerIds({ silentForMs, chatId? })` и лёгкая отметка `markAnswerAlive(messageId)` — и поле записи `alive` (`"now"` или `"released"`, время ставит само хранилище); общий набор контракта их проверяет. Хранилище без `listSilentAnswerIds` продолжает работать только с одной копией сервера: при старте `recoverInterruptedAnswers` прерывает все идущие ответы, проверок брошенных ответов дальше нет, а в журнал сервера пишется предупреждение. Без `markAnswerAlive` знак в тишине идёт полной записью ответа. Поле `alive` приходит в `insertMessage`, `updateStreamingMessage` и `updateAwaitingMessage`: если хранилище не ведёт знак, уберите поле перед записью, а не передавайте его в базу целиком. Переход на эту версию — один раз без перекрытия копий: старые копии не отмечают «жив», и новая прервала бы их ответы старше 15 с; сначала остановите все старые копии, потом запускайте новые. Дальше перекрытие безопасно. Если запрос истории попал не на ту копию, экран через 30 с сам покажет «прерван», хотя ответ идёт: держите запросы одного чата на одной копии. Сбой базы при конечной записи ответа повторяется через 1 и 3 с, а если не вышло — ответ остаётся для восстановления вместо вечного «идёт»; если запись прошла, а потерялся только ответ базы, повтор узнаёт свой итог и считает его удачным. Инструмент «по очереди», не слушающий отмену, после обрыва попытки держит очередь не дольше своего срока; очередь одна на чат, так что «Стоп» и сразу «Повторить» не запустят его второй раз параллельно. Восстановление пропускает ответ, на котором упало, с записью в журнал сервера, и прерывает остальные: битая запись не мешает серверу стартовать. Мелочи: адрес ответа вкладки на браузерный инструмент отвечает 413 `too_large` на слишком большое тело; неотвеченный запрос подтверждения закрывается только после сохранения нового сообщения, а если закрыть не вышло — повтор того же сообщения или «Повторить» доводят дело до конца; вызов веб-инструмента, оборванный «Стопом» или сбоем после отправки запроса, пишется в расход `web_tools`; картинки при загрузке ужимаются через очередь — разом не больше половины ядер (`limits.maxParallelImageFits`).
742
+ - aba59fe: Цикл агента и учёт расхода крепче (022, этап 8). Когда шаги хода кончаются, последний запрос идёт без инструментов (счёт по шагам всего хода, ход из одного шага не ограничивается) и с пометкой «подведи итог» в самом конце: ход кончается сводкой для человека, а не обрывом на вызове. Новая настройка `agent.sequentialTools` (`true` или список имён) пускает инструменты с последствиями по одному: «спиши деньги» не пойдёт одновременно с другим таким вызовом; очередь одна на чат (во всех ходах и повторах), ожидание очереди не съедает срок инструмента, «Стоп» отменяет ждущий вызов, и модель видит, что он не запускался. Оборванный вызов инструмента теперь говорит модели, что действие могло выполниться частично, и показывает последний промежуточный вывод. Ответ, упёршийся в предел длины, получает отметку: человек видит надпись «Ответ оборван по длине» (`labels.answerCutByLength`, в скине — `AgentMessage.cutByLength`), модель в следующих ходах видит пометку в конце своего ответа. Ответ из одних рассуждений (например, «Стоп» на рассуждении) больше не идёт в запрос, а сообщения человека, оказавшиеся подряд, склеиваются в одно (строгие провайдеры не принимают два подряд). Сегодняшняя дата словами идёт в конце последнего сообщения человека, только в запросе — начало запроса не меняется, кэш цел. Дата берётся по часовому поясу человека: браузер шлёт свой пояс (`timeZone`) с каждым запросом хода, сервер берёт только настоящий пояс IANA, иначе — новая настройка `agent.timeZone`, без неё — пояс сервера. Ни один ответ больше не бесплатен в журнале расхода: шаг, оборванный «Стопом» или сбоем после первых слов, и шаг, по которому провайдер не прислал числа, прикидываются по длине текста с учётом письма (кириллица, JSON и иероглифы плотнее латиницы; прикидка скорее завышает), а ответ, оборванный перезапуском сервера, записывается при следующем запуске (`recoverInterruptedAnswers`): только то, чего ещё нет в журнале, поэтому сбой после «Разрешить» не задваивает расход. Такие записи и итог ответа помечены «примерно» (`estimated`). В SQLite и PostgreSQL у записей расхода новая колонка `estimated`: миграция применяется сама при открытии хранилища. Своим хранилищам менять ничего не нужно: без колонки теряется только пометка. Фильтр `sumUsage` получил поля `messageId` и `source`: свои хранилища должны их учитывать (общие тесты `describeChatStorageContract` это проверяют).
743
+ - 6b20d83: Голосовой режим («разговор»): человек говорит, слова сами уходят сообщением в чат, ответ агента звучит голосом, человека можно перебить. Новый пакет `@ykurilov/agent-chat-voice-dialog`: распознавание Soniox, озвучка ElevenLabs (голос по умолчанию «Профессиональный диктор для аудиокниг», модель `eleven_v4_turbo`), ключи только на сервере. Перебитый ответ остаётся в ленте с отметкой «Вы перебили: озвучено до «…»», она хранится и видна после перезагрузки. Расход на озвучку идёт в учёт и лимиты (`source: "speech"`). Человек выбирает голос в меню режима; выбор хранится на сервере.
744
+
745
+ Модель узнаёт о режиме двумя служебными пометками: одна при входе («ответы озвучиваются, отвечай коротко и разговорно»), одна при возврате к тексту («разговор снова текстом»). Они стоят в конце сообщения человека, где режим сменился, а не в каждом запросе. Сообщение из режима запоминается голосовым отметкой в самом сообщении (часть `data-voice-mode` в `parts`), поэтому миграции для этого нет, пометки выводятся из сохранённого и кэш модели цел. Тексты задают настройки агента `voiceModeHint` и `textModeNote` (есть умолчания).
746
+
747
+ Обновление ничего не ломает: без нового пакета чат работает как раньше. Диктовка в поле ввода (`@ykurilov/agent-chat-voice`) не меняется.
748
+
749
+ ### Как обновить приложение
750
+
751
+ Нужно, только если вы хотите голосовой режим.
752
+
753
+ 1. Поставьте пакет `@ykurilov/agent-chat-voice-dialog` той же версии, что и остальные пакеты SDK. Скин Тандем обновите вместе с SDK: договор скина теперь версии 10 (необязательный `dialog` у поля ввода, панель режима, меню голосов, отметка обрыва; свой скин может их не рисовать, тогда кнопки режима не будет).
754
+ 2. Сервер. Рядом с чатом подключите режим (нужен WebSocket; с Hono и `@hono/node-ws`):
755
+
756
+ ```ts
757
+ import { mountDialogRelay } from "@ykurilov/agent-chat-voice-dialog/hono";
758
+
759
+ mountDialogRelay(app, {
760
+ path: "/api/voice-dialog",
761
+ upgradeWebSocket,
762
+ storage,
763
+ resolveUser, // тот же вход, что у чата
764
+ spendingLimits, // по желанию
765
+ logError: (message) => console.error(message),
766
+ speech: {
767
+ voices: [{ id: "oE0n0S3wK5Mj9C62iehn", name: "Профессиональный диктор для аудиокниг" }],
768
+ defaultVoiceId: "oE0n0S3wK5Mj9C62iehn",
769
+ pricePerThousandChars: { eleven_v4_turbo: 0.11, eleven_v4: 0.22 }, // ваша цена, $ за 1000 знаков
770
+ },
771
+ });
772
+ ```
773
+
774
+ Ключи `ELEVENLABS_API_KEY` и `SONIOX_API_KEY` — в окружении сервера. Next.js: WebSocket нужен свой Node-сервер, см. README пакета (`createDialogRelay`).
775
+ 3. Браузер:
776
+
777
+ ```tsx
778
+ const voiceDialog = createVoiceDialogEngine({ basePath: "/api/voice-dialog" });
779
+ <AgentChat api="/api/chat" skin={tandemSkin} voiceDialog={voiceDialog} />
780
+ ```
781
+
782
+ 4. Настройки. Слова остановки и междометия по умолчанию только русские; для другого языка задайте `bargeIn.stopWords` и `bargeIn.ignoreWords` у движка и `language` на сервере. Строгая политика содержимого страницы: добавьте `worklet-src blob:`.
783
+ 5. Хранилище. SQLite получает миграцию 0008, PostgreSQL — 0007 (схема `agent_chat`) сами при открытии базы. Свой `ChatStorage`: добавьте необязательные методы `setVoiceCut`, `getVoiceChoice`, `setVoiceChoice` (без них отметка обрыва и выбор голоса живут только до перезагрузки) и значение `"speech"` в `UsageSource`, если учёт расхода у вас свой.
784
+ 6. Свой экран (не наш скин): части сообщения с типом `data-voice-mode` не рисуйте, показывайте только текстовые части.
785
+ 7. Проверка: `pnpm typecheck` проходит; в чате появилась кнопка режима, после нажатия виден статус «Слушаю», фраза уходит сообщением, ответ звучит.
786
+
787
+ ### Patch Changes
788
+
789
+ - 96098be: Мелкие исправления (022). Символ с кодом ноль в первом сообщении больше не роняет отправку на Postgres: он выбрасывается из названия чата, из записей памяти и из сводки сжатия истории (иначе запись сжатия падала бы при каждой попытке); в самом тексте сообщения он лежит как раньше. Вложения отмечаются прикреплёнными до записи сообщения, а не после: если сервер упал между двумя шагами, чистка брошенных файлов не удалит их через сутки (файл, отмеченный зря, безвреден и уходит вместе с чатом). Во всех пакетах появилось поле `engines: { node: ">=22" }`; голосовой сервер (`createVoiceRelay`) и голосовой диалог (`createDialogRelay`) на Node 20 сразу при создании говорят понятной ошибкой, что нужен Node 22 (раньше падали при первом соединении; со своим `connectUpstream` проверки нет). В пакете вложений `react` и `@ykurilov/agent-chat-react` стали необязательными peer-зависимостями: серверу без экрана они больше не ставятся. Приложению, которое берёт `@ykurilov/agent-chat-attachments/react`, достаточно иметь их в проекте — для чата на экране они нужны и так; если они подтягивались только через вложения, добавьте их в зависимости приложения.
790
+ - Updated dependencies [4f33e85]
791
+ - Updated dependencies [15fbbcc]
792
+ - Updated dependencies [c09c1e2]
793
+ - Updated dependencies [062ce36]
794
+ - Updated dependencies [656233a]
795
+ - Updated dependencies [96098be]
796
+ - Updated dependencies [b6f4855]
797
+ - Updated dependencies [278a718]
798
+ - Updated dependencies [c1581f4]
799
+ - Updated dependencies [213bcb3]
800
+ - Updated dependencies [927c02c]
801
+ - Updated dependencies [be77ee2]
802
+ - Updated dependencies [8b59096]
803
+ - Updated dependencies [7fbc105]
804
+ - Updated dependencies [84ad64f]
805
+ - Updated dependencies [5027963]
806
+ - Updated dependencies [c2769dd]
807
+ - Updated dependencies [0cdf102]
808
+ - Updated dependencies [ba16989]
809
+ - Updated dependencies [aba59fe]
810
+ - Updated dependencies [47a56ac]
811
+ - Updated dependencies [f9b6162]
812
+ - Updated dependencies [f9b6162]
813
+ - Updated dependencies [f9b6162]
814
+ - Updated dependencies [1b2f9f1]
815
+ - Updated dependencies [0e8e0ae]
816
+ - Updated dependencies [6b20d83]
817
+ - Updated dependencies [d12dcf3]
818
+ - Updated dependencies [366d785]
819
+ - Updated dependencies [946e5d8]
820
+ - @ykurilov/agent-chat-core@0.14.0
821
+
822
+ ## 0.13.0
823
+
824
+ ### Minor Changes
825
+
826
+ - 4793e4d: Чат запоминает последний выбор человека: провайдера, модель и уровень рассуждений. Новый чат открывается на них, а не на модели по умолчанию. Если модели больше нет в каталоге или у её провайдера нет ключа, новый чат открывается на выборе по умолчанию из настроек; уровень, которого у модели нет, заменяется её уровнем по умолчанию. У каждого человека свой выбор, старые чаты с сообщениями остаются на своём, а пустой чат, который открывается кнопкой «Новый чат», переходит на последний выбор. Запоминается последний выбор, в том числе сделанный в старом чате. При двух открытых вкладках побеждает позже пришедший запрос. Выбор хранится по человеку и общий для всех обработчиков на одной базе; недоступная модель заменяется моделью по умолчанию. Новый чат сразу показывает запомненный выбор, без мигания моделью по умолчанию.
827
+
828
+ Обновление ничего не требует: SQLite и PostgreSQL получают новую таблицу сами при открытии базы (SQLite — миграция 0007, PostgreSQL — миграция 0006 в схеме `agent_chat`), встроенное хранилище в памяти тоже умеет запоминать.
829
+
830
+ ### Как обновить приложение со своим хранилищем
831
+
832
+ Нужно только если приложение передаёт в `createChatHandler` собственное хранилище (свою реализацию `ChatStorage`). Без этого шага всё продолжит работать как раньше: новые чаты будут открываться на `defaultChoice`.
833
+
834
+ 1. В своём хранилище добавьте два необязательных метода из `ChatStorage`:
835
+
836
+ ```ts
837
+ getUserModelChoice(ownerId: string): Promise<{ modelId: string; providerId: string; reasoning?: string } | null>;
838
+ saveUserModelChoice(ownerId: string, choice: { modelId: string; providerId: string; reasoning?: string }): Promise<void>;
839
+ ```
840
+
841
+ Один выбор на человека (`ownerId`): повторная запись заменяет прежний, запись без `reasoning` стирает сохранённый уровень. С чатами выбор не связан: удаление чата его не трогает. Пример запросов — в `packages/storage-sqlite/src/sqlite-storage.ts`.
842
+ 2. Добавьте таблицу или коллекцию под выбор (SQLite: `owner_id` — первичный ключ, `model_id`, `provider_id`, `reasoning` — может быть пустым, `updated_at`).
843
+ 3. В тестах хранилища вызов `describeChatStorageContract(createStorage)` сам проверит новые методы; если они есть, проверки пройдут, если нет — будут пропущены.
844
+ 4. Проверка: `pnpm typecheck` проходит; сменить модель в чате, открыть новый чат — на нём та же модель.
845
+
846
+ ### Patch Changes
847
+
848
+ - Updated dependencies [4793e4d]
849
+ - @ykurilov/agent-chat-core@0.13.0
850
+
851
+ ## 0.12.0
852
+
853
+ ### Patch Changes
854
+
855
+ - @ykurilov/agent-chat-core@0.12.0
856
+
857
+ ## 0.11.0
858
+
859
+ ### Patch Changes
860
+
861
+ - @ykurilov/agent-chat-core@0.11.0
862
+
863
+ ## 0.10.0
864
+
865
+ ### Minor Changes
866
+
867
+ - 4a76ff3: **Ломает приложения:** у `ChatStorage` появились обязательные операции `updateAwaitingMessage` и `getMessage`, у словаря `ChatLabels` — новые ключи `approval*`; касается приложений со своим хранилищем и с полным словарём надписей.
868
+
869
+ Подтверждение перед опасными действиями агента. Инструмент, помеченный `needsApproval: true` (или функцией от входных данных), не выполняется без разрешения человека: ход останавливается, в ленте появляется карточка с названием действия, входными данными и кнопками «Разрешить», «Отклонить» и «Всегда в этом чате». Кнопка «Всегда» есть только у инструмента с `needsApproval: true`; у инструмента с функцией её нет, каждый вызов спрашивает заново. Ждать можно сколько угодно: состояние лежит на сервере, переживает перезагрузку страницы и перезапуск сервера, ответить можно с любого устройства.
870
+
871
+ Что изменилось:
872
+
873
+ - `@ykurilov/agent-chat-core`: статус ответа `awaiting_approval`; `answerApproval` у сервиса чата; ответ после решения дописывается в то же сообщение, инструмент выполняется ровно раз. «Отклонить» — модель получает «пользователь отклонил» и продолжает. Новое сообщение вместо ответа на карточку считается отказом. Сбой или «Стоп» после «Разрешить» не оставляют вызов без итога: он закрывается как прервавшийся («результат неизвестен», если инструмент уже мог сработать). Ответ на карточку проверяет лимит расходов (429), а расход паузы и продолжения не считается дважды. После смены модели во время ожидания продолжение идёт на модели ждущего сообщения. Кэш модели не страдает: инструкция и набор инструментов не меняются.
874
+ - `@ykurilov/agent-chat-server`: адрес `POST /:chatId/approvals/:approvalId` с телом `{ "decision": "allow" | "deny" | "always" }`. Ошибки: 409 `busy` (в чате идёт другой ход), 409 `approval_not_pending` (на этот запрос отвечать уже нечем), 429 `spending_limit_reached`.
875
+ - `@ykurilov/agent-chat-storage-sqlite`, `@ykurilov/agent-chat-storage-postgres`: реализуют `updateAwaitingMessage` и `getMessage`. Схема базы не меняется, миграции не нужны: статус ответа хранится обычным текстом.
876
+ - `@ykurilov/agent-chat-react`: договор скина версии 9 — у `ToolCallRow` необязательные `title` и `approval`; хук `useAgentChat` отдаёт `respondToApproval`, `approvalPendingId`, `approvalFailure` (вид сбоя: нет связи, чат занят, лимит расходов, запрос уже не ждёт); при неудачной отправке карточка возвращается к кнопкам, а не остаётся «отклонённой». У `ToolApprovalView` в состоянии `requested` есть `canAlways` (показывать ли «Всегда») и необязательный `failureText`. Словарь получил ключи `approvalRequested`, `approvalWaiting`, `approvalAllow`, `approvalDeny`, `approvalAlways`, `approvalFailed`, `approvalSpendingLimit`, `approvalBusy`, `approvalAllowedMark`, `approvalAlwaysMark`, `approvalDeniedMark`, `approvalAnnouncement`.
877
+ - `@ykurilov/agent-chat-skin-tandem`: карточка подтверждения (группа с названием для читалки, «Ждёт вашего решения» и итог озвучиваются, фокус не теряется, на телефоне длинные поля не выпирают) и пометка решения в строке вызова.
878
+
879
+ ### Как обновить приложение
880
+
881
+ Если опасных инструментов нет, а хранилище и скин стандартные, обновление ничего не требует.
882
+
883
+ 1. Своё хранилище (`ChatStorage`): добавьте `getMessage(messageId)` — вернуть сообщение по номеру или `null`. И добавьте `updateAwaitingMessage(messageId, patch)` — перезаписать `parts` ответа со статусом `awaiting_approval` и, если в `patch.status` указано, сменить статус на `"streaming"` или `"complete"`; ответ не в этом статусе — бросить `MessageNotAwaitingError`. Готовый образец — `packages/storage-sqlite/src/sqlite-storage.ts`. Проверка: `describeChatStorageContract` из `@ykurilov/agent-chat-core/storage-contract` проходит.
884
+ 2. Полный свой словарь надписей (`ChatLabels`, объект без `...russianLabels`): добавьте двенадцать ключей `approval*` из списка выше. Словарь `{ ...russianLabels, … }` менять не нужно.
885
+ 3. Свой скин: если `ToolCallRow` свой, добавьте разбор `approval` — `{ state: "requested", onDecide, pending, failed, failureText?, canAlways }` (нарисовать кнопки, `onDecide("allow" | "deny" | "always")`; «Всегда» показывать только при `canAlways`; при `failed` показать `failureText`, если он есть) и `{ state: "allowed" | "always" | "denied" }` (итог решения). Нет `approval` — рисуйте строку как раньше. Название действия — в `title`.
886
+ 4. Чтобы включить подтверждение: у опасного инструмента задайте `needsApproval: true` (можно функцией от входных данных; с функцией кнопки «Всегда» не будет) и `title` — название для карточки. Больше ничего подключать не надо.
887
+ 5. Проверка: `pnpm typecheck`; попросите агента вызвать опасный инструмент — в ленте карточка, после «Разрешить» действие выполнено, ответ дописан в то же сообщение.
888
+
889
+ ### Patch Changes
890
+
891
+ - Updated dependencies [4a76ff3]
892
+ - @ykurilov/agent-chat-core@0.10.0
893
+
894
+ ## 0.9.0
895
+
896
+ ### Minor Changes
897
+
898
+ - d07a1c3: **Ломает приложения:** только если вы сами реализовали `ChatStorage` или собрали словарь надписей целиком (без `{ ...russianLabels, … }`); остальным ничего делать не нужно.
899
+
900
+ Новый пакет `@ykurilov/agent-chat-limits`: лимиты расходов. Бюджет в долларах на человека, на чат или на то и другое; период — календарный месяц (по умолчанию), сутки или без обнуления. Исчерпанный лимит останавливает ход до его начала; при 80% чат предупреждает.
901
+
902
+ Что изменилось:
903
+
904
+ - Ядро: цены в профиле модели (`ModelProfile.pricing` по провайдерам; вход, выход, чтение и запись кэша, рассуждения считаются как выход). Цены есть у всех стандартных моделей. Журнал расхода `usage_records` — токены и стоимость в долларах (`costUsd`, по ценам модели на момент хода) каждого ответа и фонового сжатия, в том числе сжатия, которое не удалось, но было оплачено; запись датируется началом хода. Журнал только дописывается и переживает удаление чата. Сумма лимита берётся из записей, поэтому смена цен и удаление модели из каталога прошлый расход не меняют. Новые методы хранилища `insertUsageRecord` и `sumUsage`.
905
+ - Ядро: `createChatService({ limits })` и `ChatService.getSpendingStatus`. Проверка идёт перед отправкой, повтором, переделкой и правкой, до любых записей; исчерпание — `SpendingLimitReachedError`.
906
+ - Сервер: `createChatHandler({ limits })`. Исчерпанный лимит — `429 {"error":{"kind":"spending_limit_reached","scope","period","resetsAt"}}`; новый адрес `GET /api/chat/:id/spending` (без сумм в долларах).
907
+ - `@ykurilov/agent-chat-limits`: `createSpendingLimits({ storage, catalog, budget, period?, warnAt?, unpricedModels?, now?, logError? })` и `getSpending({ userId, chatId })` для страницы «тариф» (расход чата считается только среди записей этого человека). Своя модель без цены — понятная ошибка при создании; явно `unpricedModels: "ignore"` — такие ходы пишутся с нулевой стоимостью. `compactNow` лимит не проверяет: ручное сжатие остаётся доступным.
908
+ - Хранилища: SQLite (миграция `0006`) и PostgreSQL (миграция `0005`) добавляют таблицу `usage_records`; миграции применяются при открытии хранилища сами, существующие данные не трогаются. Проверено на копии базы песочницы.
909
+ - Браузер: `AgentChat` принимает `spendingLimits`. Договор скина версии 8: необязательный компонент `SpendingNotice({ id, state, text })` (`state` бывает `none`: область для диктора всегда на странице) и необязательные свойства `Composer.sendingBlocked` и `Composer.sendingBlockedReasonId` (для `aria-describedby`). Скин Тандема их рисует; при остановке фокус переходит на надпись и возвращается в поле, когда лимит снят. Отказ 429 не запускает переподключения: текст возвращается в поле (при правке сообщения редактор остаётся открытым с текстом и надписью `editErrors.spendingLimit`), показывается «Лимит на месяц исчерпан, обновится 1 ноября»; исчерпание снимается само в момент обновления.
910
+ - Словарь надписей: `spendingWarning` и `spendingExhausted` (функции от сведений о лимите) и `editErrors.spendingLimit`.
911
+
912
+ Не входит в расход: веб-инструменты (`web-tools`, другой сервис), незаконченный шаг остановленного хода и ход, оборванный перезапуском сервера. Ход, начатый в пределах бюджета, доводится до конца; одновременные ходы могут вместе выйти за бюджет (проверка не резервирует деньги).
913
+
914
+ ### Как обновить приложение
915
+
916
+ Если лимиты не нужны, делать ничего не нужно, кроме двух случаев:
917
+
918
+ 1. **Только если вы реализовали `ChatStorage` сами:** добавьте `insertUsageRecord(record)` и `sumUsage(filter)` (запись расхода несёт `costUsd`, сумма возвращает `costUsd`) и прогоните `describeChatStorageContract` (в нём появился блок «записи расхода»). Хранилища SQLite и PostgreSQL из этого репозитория обновляются сами.
919
+ 2. **Только если словарь надписей написан целиком** (не через `{ ...russianLabels, … }`): добавьте функции `spendingWarning` и `spendingExhausted` и надпись `editErrors.spendingLimit`. Значения — в `russianLabels`.
920
+
921
+ Чтобы включить лимиты:
922
+
923
+ 1. `pnpm add @ykurilov/agent-chat-limits`.
924
+ 2. На сервере: `const limits = createSpendingLimits({ storage, catalog, budget: async ({ userId }) => ({ user: 5 }) })` и `createChatHandler({ …, limits })`. `storage` и `catalog` — те же, что у чата.
925
+ 3. В браузере: `<AgentChat … spendingLimits />`. Скин свой — добавьте `SpendingNotice` (с `id`, состоянием `none` и `role="status"`) и учтите `Composer.sendingBlocked` и `sendingBlockedReasonId`; нет их — надписи не будет, но отправка остановится.
926
+ 4. Проверка: с маленьким бюджетом чат сначала предупреждает, потом останавливает отправку и пишет дату обновления.
927
+
928
+ ### Patch Changes
929
+
930
+ - Updated dependencies [d07a1c3]
931
+ - @ykurilov/agent-chat-core@0.9.0
932
+
933
+ ## 0.8.0
934
+
935
+ ### Minor Changes
936
+
937
+ - a24c79d: **Ломает приложения:** у `ChatStorage` три новых обязательных метода (`listChats`, `renameChat`, `deleteChat`), у `AttachmentStorage` — один (`removeAllOfChat`), а у `ChatLabels` — 16 новых обязательных надписей `chatList*`. Приложения на готовых хранилищах и готовом словаре надписей ничего менять не должны; поломка только у своих реализаций.
938
+
939
+ Список чатов в боковой панели: создать чат, переименовать, удалить. Чат сам получает название по первому сообщению. Это расширение по желанию: приложение без `chatList` и без `ChatWorkspace` работает как раньше.
940
+
941
+ Что изменилось:
942
+
943
+ - `@ykurilov/agent-chat-core`: `createChatService` умеет `listChats(userId)`, `renameChat(userId, chatId, title)`, `deleteChat(userId, chatId)`. Название ставится по первому сообщению (до 60 знаков), вручную заданное не перезаписывается; длина названия до 100 знаков. Удаление настоящее: идущий ответ и сжатие останавливаются, затем убираются сообщения, записи сжатия, выбор веток, файлы вложений и сам чат. Чужой и несуществующий чат — одинаково `ChatNotFoundError`. Экспортируются `deriveChatTitle`, `normalizeChatTitle`, `InvalidChatTitleError`, тип `ChatListItem`. `Chat.title` необязательное. Набор проверок `describeChatStorageContract` расширен.
944
+ - `@ykurilov/agent-chat-server`: `createChatHandler({ chatList: true })` включает три адреса: `GET {basePath}` (список), `PATCH {basePath}/:id` с телом `{ title }`, `DELETE {basePath}/:id`. Без `chatList` эти адреса отвечают 404. Чужой чат — 404, неверное название — 400. Идущий ответ удалению не мешает: он останавливается. `409` при удалении — только если чат уже удаляется или в этот миг запускается ход; пока чат удаляется, сообщение, сжатие и вложения в него тоже получают `409`. Переименование `409` не даёт.
945
+ - `@ykurilov/agent-chat-storage-sqlite` и `@ykurilov/agent-chat-storage-postgres`: колонка `title` у чатов, методы `listChats`, `renameChat`, `deleteChat` (удаление в одной транзакции). Миграция сама заполняет названия старых чатов по их первому сообщению.
946
+ - `@ykurilov/agent-chat-attachments`: `AttachmentStorage` получил `removeAllOfChat(chatId)`; `createAttachments` отдаёт ядру `removeChatFiles`, и файлы удаляются вместе с чатом. Набор `describeAttachmentStorageContract` расширен.
947
+ - `@ykurilov/agent-chat-react`: компонент `ChatWorkspace` (список и открытый чат вместе), хук `useChatList`, у `AgentChat` необязательное `onBusyChange`. Договор скина версии 7: скин, который хочет работать с `ChatWorkspace`, дополнительно даёт `ChatWorkspace` и `ChatList` (тип `ChatListSkin`); договор самого чата не менялся. 16 новых надписей `chatList*` в словаре (`ChatLabels`).
948
+ - `@ykurilov/agent-chat-skin-tandem`: `TandemChatWorkspace` и `TandemChatList`. На широком экране список слева, на узком — панель по кнопке «Чаты».
949
+
950
+ ### Как обновить приложение
951
+
952
+ Если список чатов не нужен, а хранилища и словарь надписей готовые, обновление ничего не требует.
953
+
954
+ Если у приложения своё `ChatStorage`:
955
+
956
+ 1. Добавьте методы `listChats(ownerId)`, `renameChat(chatId, title | null)` и `deleteChat(chatId)`. Список — только чаты владельца, самые недавно менявшиеся первыми; `deleteChat` убирает сообщения, записи сжатия, выбор веток и чат одной транзакцией. Точные правила — в общем наборе проверок.
957
+ 2. Храните название чата (`Chat.title`, необязательное) и проведите свою миграцию.
958
+ 3. Проверка: `describeChatStorageContract` из `@ykurilov/agent-chat-core/storage-contract` проходит.
959
+
960
+ Если у приложения своё `AttachmentStorage`: добавьте `removeAllOfChat(chatId): Promise<number>` (удалить все файлы чата, вернуть сколько) и прогоните `describeAttachmentStorageContract`.
961
+
962
+ Если у приложения свой полный словарь надписей (`ChatLabels` целиком, а не `{ ...russianLabels, … }`): добавьте 16 новых ключей, иначе `pnpm typecheck` упадёт. Простые строки: `chatListHeading` («Чаты»), `chatListNewChat` («Новый чат»), `chatListUntitled` («Новый чат»), `chatListLoading`, `chatListLoadFailed`, `chatListCreateFailed`, `chatListRenameFailed`, `chatListDeleteFailed`, `chatListRename`, `chatListRenameField`, `chatListDelete`, `chatListDeleteTitle`, `chatListDeleteConfirm`, `chatListDeleteCancel`. Функции от названия чата: `chatListRowMenu(title)` («Действия с чатом …») и `chatListDeleteBody(title)`.
963
+ Было: `const labels: ChatLabels = { …прежние надписи }`. Стало: те же надписи плюс 16 новых; проще всего `{ ...russianLabels, …свои }`.
964
+
965
+ Чтобы включить список чатов:
966
+
967
+ 1. Сервер: `createChatHandler({ …, chatList: true })`. Базы SQLite и PostgreSQL обновятся сами при открытии.
968
+ 2. Браузер: вместо `<AgentChat api chatId … />` — `<ChatWorkspace api="/api/chat" skin={tandemSkin} … />` (те же остальные свойства, кроме `chatId`). Открытый чат приложение может показывать в адресе: `initialChatId` и `onChatChange`.
969
+ 3. Если скин свой: добавьте `ChatWorkspace` и `ChatList` по договору версии 7 (`ChatListSkin`). Скин Тандема обновляется вместе с пакетом.
970
+ 4. Если приложение само создавало чат кнопкой «Новый чат», уберите её: «Новый чат» теперь в списке.
971
+ 5. Проверка: `pnpm typecheck` (ловит недостающие методы хранилищ и надписи); в списке видны чаты пользователя, «Новый чат» открывает пустой чат, после первого сообщения чат получает название, переименование и удаление (с подтверждением) работают.
972
+
973
+ ### Patch Changes
974
+
975
+ - Updated dependencies [a24c79d]
976
+ - @ykurilov/agent-chat-core@0.8.0
977
+
978
+ ## 0.7.0
979
+
980
+ ### Minor Changes
981
+
982
+ - b8f9e9f: **Ломает приложения со своими хранилищами и своими скинами:** ответ агента можно переделать, а отправленное сообщение исправить; все варианты сохраняются, между ними переключает «‹ 2/3 ›».
983
+
984
+ Что изменилось:
985
+
986
+ - Под ответом агента появилась кнопка «Переделать ответ»: агент отвечает на тот же вопрос заново, прежний ответ остаётся вариантом. Под сообщением собеседника появился карандаш «Изменить сообщение»: правка идёт на месте пузыря, отправляется новой веткой с новым ответом агента.
987
+ - Когда у сообщения больше одного варианта, рядом стоит переключатель «‹ 2/3 ›». Выбранный вариант запоминается на сервере: после обновления страницы или на другом устройстве открывается он же.
988
+ - Ничего не удаляется и не переписывается: журнал переписки только дописывается, выбор варианта — отдельная запись в новой таблице `branch_selections`.
989
+ - Модель видит только текущую ветку: после правки она не знает прежнего текста, после переключения на прежний вариант — знает. Инструкция и инструменты в запросе не меняются, кэш модели ветки не задевают. Сжатие истории и поиск по старой переписке работают по текущей ветке (сводка действует на ветке, где она сделана).
990
+ - Правка сообщения с файлами (пакет вложений): файлы остаются как были и переходят в новый вариант, меняется только текст; убрать или добавить файл при правке нельзя. Сообщение из одних файлов можно отправить и с пустым текстом. Файлы проходят ту же проверку сервера, что и при обычной отправке.
991
+ - Пока идёт ответ, переключать, переделывать и править нельзя: сервер отвечает 409 `busy`, кнопки в экране неактивны. Если отказ всё же пришёл (ответ пошёл с другой вкладки), поле правки остаётся открытым с текстом собеседника и понятной ошибкой: текст не пропадает.
992
+ - Новые адреса в `createChatHandler`: `GET /api/chat/:id/branches`, `POST /api/chat/:id/branch`, `POST /api/chat/:id/regenerate`, `POST /api/chat/:id/edit`; развилки приходят и в `GET /api/chat/:id` (поле `forks`). Новые методы службы: `selectBranch`, `regenerate`, `editMessage`, `getBranchForks`. Хук `useAgentChat` отдаёт `forks`, `selectBranch`, `canRegenerate`, `regenerate`, `canEdit`, `edit` (возвращает `Promise<EditOutcome>`: правка началась, не началась или сервер отказал).
993
+ - Базы SQLite (миграция 0004) и PostgreSQL (миграция 0003) получают новую таблицу сами при первом запуске. Ничего запускать вручную не нужно.
994
+ - В скине Тандема строка с карандашом и переключателем занимает прежний промежуток между сообщениями: высота сообщений не меняется.
995
+
996
+ ### Как обновить приложение
997
+
998
+ 1. Обновите все пакеты SDK одной командой: `pnpm update "@ykurilov/agent-chat-*" --latest`. Базы обновятся сами при первом запуске сервера.
999
+ 2. **Если у приложения своё хранилище** (свой класс, реализующий `ChatStorage`): добавьте два метода и таблицу под них.
1000
+
1001
+ ```ts
1002
+ insertBranchSelection(record: StoredBranchSelection): Promise<void>; // дописать запись «выбран этот вариант»
1003
+ listBranchSelections(chatId: string): Promise<StoredBranchSelection[]>; // записи чата по порядку добавления
1004
+ ```
1005
+
1006
+ `StoredBranchSelection` — `{ id, chatId, messageId, selectedAt }` (оба времени — строка ISO). Записи только добавляются, не меняются и не удаляются. Тип и общий тест `describeChatStorageContract` берутся из `@ykurilov/agent-chat-core`; тест теперь проверяет и эти методы. Приложения на памяти, SQLite и PostgreSQL из наших пакетов ничего не меняют.
1007
+ 3. **Если у приложения свой скин** (объект `ChatSkin` не из `@ykurilov/agent-chat-skin-tandem`): договор скина теперь версии 6 (версия 5 — голосовой ввод и вложения, необязательные `Composer.voice`, `Composer.attachments`, `UserMessage.attachments`). Добавьте два компонента и новые свойства сообщений.
1008
+
1009
+ ```tsx
1010
+ const BranchSwitcher: ChatSkin["BranchSwitcher"] = ({ position, count, onPrevious, onNext }) => {
1011
+ const labels = useChatLabels();
1012
+ return (
1013
+ <div role="group" aria-label={labels.branchPosition(position, count)}>
1014
+ <button type="button" aria-label={labels.previousBranch} disabled={!onPrevious} onClick={onPrevious}>‹</button>
1015
+ {position}/{count}
1016
+ <button type="button" aria-label={labels.nextBranch} disabled={!onNext} onClick={onNext}>›</button>
1017
+ </div>
1018
+ );
1019
+ };
1020
+ const MessageEditor: ChatSkin["MessageEditor"] = ({ value, onValueChange, onSave, onCancel, canSave, error, attachments }) => {
1021
+ const labels = useChatLabels();
1022
+ return (
1023
+ <div>
1024
+ {attachments && <ul aria-label={attachments.label}>{attachments.items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>}
1025
+ <textarea aria-label={labels.editMessage} value={value} onChange={(event) => onValueChange(event.target.value)} />
1026
+ {error && <p role="alert">{error}</p>}
1027
+ <button type="button" onClick={onCancel}>{labels.cancelEdit}</button>
1028
+ <button type="button" disabled={!canSave} onClick={onSave}>{labels.saveEdit}</button>
1029
+ </div>
1030
+ );
1031
+ };
1032
+ const skin = { ...mySkin, BranchSwitcher, MessageEditor };
1033
+ ```
1034
+
1035
+ `UserMessage` получает свойства `branchSwitcher?: ReactNode` (положить рядом с сообщением) и `onEdit?: () => void` (кнопка «Изменить сообщение»; когда правка недоступна, свойства нет). `AgentMessage` получает `branchSwitcher?: ReactNode` и `onRegenerate?: () => void` (кнопка «Переделать ответ», только у последнего законченного ответа). Свойства необязательны: скин без них не ломается, только не показывает кнопок. Пример — `skins/tandem/src` в репозитории SDK.
1036
+ 4. **Если словарь надписей написан целиком**, а не через `{ ...russianLabels, … }`: добавьте ключи `previousBranch` («Предыдущий вариант»), `nextBranch` («Следующий вариант»), `branchPosition(position, count)` («Вариант 2 из 3»), `regenerate` («Переделать ответ»), `editMessage` («Изменить сообщение»), `saveEdit` («Отправить»), `cancelEdit` («Отмена»), `editErrors` — `{ busy: "Ответ ещё идёт. Дождитесь его конца и отправьте снова", offline: "Нет связи с сервером. Текст сохранён в поле", rejected: "Сервер не принял правку. Текст сохранён в поле" }`.
1037
+ 5. **Если приложение само собирает ленту через `useAgentChat`**: подключите `forks` и `selectBranch`, как в `packages/react/src/agent-chat.tsx`. Готовый `AgentChat` ничего менять не просит.
1038
+ 6. Проверка: `pnpm typecheck` проходит; отправьте сообщение, нажмите «Переделать ответ» — под ответом появится «2/2», стрелка «‹» вернёт первый вариант, после обновления страницы выбор сохранится. Подробности — раздел «Ветки ответов и правка сообщений» в `docs/getting-started.md`.
1039
+
1040
+ ### Patch Changes
1041
+
1042
+ - Updated dependencies [b8f9e9f]
1043
+ - Updated dependencies [7c205c4]
1044
+ - @ykurilov/agent-chat-core@0.7.0
1045
+
1046
+ ## 0.6.0
1047
+
1048
+ ### Minor Changes
1049
+
1050
+ - 5252793: **Ломает приложения со своими хранилищами и своими скинами:** длинная переписка теперь сжимается сама, а у агента появился инструмент «поиск по старой переписке».
1051
+
1052
+ Что изменилось:
1053
+
1054
+ - Когда модель видит больше 200 тысяч токенов (у моделей с маленьким окном — 80% окна), ядро в фоне после ответа заменяет старую часть переписки короткой сводкой и оставляет свежие ~20 тысяч токенов как есть. Сообщения из базы не удаляются. В ленте на месте сжатия появляется строка «История сжата», по клику открывается сводка. Сжатие включено по умолчанию; `compaction: false` в `createChatHandler` (или `createChatService`) его выключает, `compaction: { thresholdTokens, tailTokens, summaryCeilingTokens, model, instruction }` настраивает.
1055
+ - Новый пакет `@ykurilov/agent-chat-history-search`: инструмент `search_earlier_conversation`, который ищет точные детали в оригиналах старой переписки. Подключается по желанию.
1056
+ - Новый адрес `GET /api/chat/:id/compactions` (в `createChatHandler`), новый метод службы `compactNow`, набор инструментов агента получает `chatId` хода.
1057
+ - «Стоп» обрывает ожидание сжатия сразу (само сжатие доделывается в фоне). `chat.flush()` при остановке сервера ждёт идущие сжатия до `compaction.flushWaitMs` (30 секунд по умолчанию) и отменяет остальные без записи полусводки.
1058
+ - Разделитель появляется без перезагрузки надёжно: сервер ставит признак «идёт сжатие» до конца потока ответа, а экран опрашивает `/compactions`, пока признак стоит (до 12 минут). `/compactions` стал лёгким: читает связи сообщений, а не тексты с выводами инструментов.
1059
+ - Сводку пишет самый малый уровень рассуждений модели (или без них, где можно), а потолок вывода имеет запас, так что рассуждения не обрывают сводку по длине. Пауза после сбоя теперь действует и на исход «нечего сжимать».
1060
+ - Поиск нормализует текст каждого сообщения один раз за вызов.
1061
+ - Дедупликация сжатий живёт в памяти процесса, как и реестр идущих ответов: при нескольких копиях сервера возможно двойное сжатие и двойной расход токенов.
1062
+ - Базы SQLite и PostgreSQL получают новую таблицу и новые поля сами при первом запуске. Ничего запускать вручную не нужно.
1063
+
1064
+ ### Как обновить приложение
1065
+
1066
+ 1. Обновите все пакеты SDK одной командой: `pnpm update "@ykurilov/agent-chat-*" --latest`. Базы обновятся сами при первом запуске сервера.
1067
+ 2. **Если у приложения своё хранилище** (свой класс, реализующий `ChatStorage`): добавьте три метода.
1068
+
1069
+ ```ts
1070
+ insertCompaction(record: StoredCompaction): Promise<void>; // записать отметку сжатия
1071
+ listCompactions(chatId: string): Promise<StoredCompaction[]>; // отметки чата по порядку добавления
1072
+ listMessageLinks(chatId: string): Promise<MessageLink[]>; // { id, parentId, createdAt } сообщений чата, в порядке listMessages
1073
+ ```
1074
+
1075
+ Типы `StoredCompaction`, `MessageLink` и общий тест `describeChatStorageContract` берутся из `@ykurilov/agent-chat-core`; тест теперь проверяет и эти методы.
1076
+ Приложения на памяти, SQLite и PostgreSQL из наших пакетов ничего не меняют.
1077
+ 3. **Если у приложения свой скин** (объект `ChatSkin` не из `@ykurilov/agent-chat-skin-tandem`): добавьте компонент `CompactionDivider`, договор скина теперь версии 4.
1078
+
1079
+ ```tsx
1080
+ const CompactionDivider: ChatSkin["CompactionDivider"] = ({ summary, open, onOpenChange }) => {
1081
+ const labels = useChatLabels();
1082
+ return (
1083
+ <div>
1084
+ <button type="button" aria-expanded={open} onClick={() => onOpenChange(!open)}>
1085
+ {labels.historyCompacted}
1086
+ </button>
1087
+ {open && (
1088
+ <div>
1089
+ <p>{labels.historyCompactedHint}</p>
1090
+ <pre>{summary}</pre>
1091
+ </div>
1092
+ )}
1093
+ </div>
1094
+ );
1095
+ };
1096
+ const skin = { ...mySkin, CompactionDivider };
1097
+ ```
1098
+
1099
+ `summary` — сводка в Markdown; свёрнутый разделитель — строка «История сжата». Скин Тандема обновляется вместе с пакетом.
1100
+ 4. **Если словарь надписей написан целиком**, а не через `{ ...russianLabels, … }`: добавьте ключи `historyCompacted` («История сжата») и `historyCompactedHint`.
1101
+ 5. Чтобы агент мог искать в сжатой переписке (по желанию): `pnpm add @ykurilov/agent-chat-history-search`, затем в `agent` обработчика добавьте `historySearchInstructions` в `instructions` и `...historySearch(model)` в `tools`; шаги и код — в разделе «Сжатие истории и поиск по старой переписке» файла `docs/getting-started.md`.
1102
+ 6. Проверка: `pnpm typecheck` проходит, чат открывается и отвечает. Кэш модели после первого сжатия обнуляется один раз (запрос после него дороже обычного), это ожидаемо.
1103
+
1104
+ ### Patch Changes
1105
+
1106
+ - Updated dependencies [5252793]
1107
+ - @ykurilov/agent-chat-core@0.6.0
1108
+
1109
+ ## 0.5.0
1110
+
1111
+ ### Minor Changes
1112
+
1113
+ - 62027c6: Ломает приложения: в поле ввода появился выбор модели и уровня рассуждений, поэтому «какой моделью отвечать» теперь решает не сервер приложения на каждый ход, а пользователь в чате. Настройка `resolveModel` заменена на `defaultChoice` (модель для нового чата).
1114
+
1115
+ Что нового простыми словами:
1116
+
1117
+ - Стандартный набор моделей: DeepSeek 4.1 Flash, GLM 5.3 Flash, GPT 6 Luna, Gemini 3.8 Flash. Провайдеры: Fireworks, DeepSeek, Together и новый OpenRouter (ключ `OPENROUTER_API_KEY`, среди хозяев модели выбирается самый быстрый).
1118
+ - В поле ввода одно меню моделей (провайдер → модель) и рядом выбор уровня рассуждений. Уровни свои у каждой модели: у DeepSeek «Выкл/Вкл», у GLM от «Низкий», у GPT и Gemini несколько уровней. У GPT рассуждения приходят зашифрованными: человеку виден только пересказ. На телефоне меню плоское («модель · провайдер»).
1119
+ - Выбор запоминается в чате. Модель можно сменить посреди чата: рассуждения прежней модели в запрос новой не уходят, но в базе и на экране остаются. Если сохранённой модели больше нет в каталоге, чат открывается на выборе по умолчанию, сообщения не меняются.
1120
+ - `extendCatalog` умеет больше: `removeProviders` (провайдер уходит, модель без провайдеров уходит вместе с ним) и `pinModelVersions` (закрепить версию модели у провайдера: `{ "gpt-6-luna": { openrouter: "openai/gpt-6-luna-2026-09-30" } }`). Ошибочная правка — ошибка при создании каталога.
1121
+ - Новые адреса сервера: `GET /api/chat/models` (список для меню, без ключей) и `PUT /api/chat/:id/model` (записать выбор чата).
1122
+ - Скин Тандема рисует `Composer` варианта `extended`. Договор скина стал версии 3: `Composer` получает необязательное `modelChoice`; свой скин без него работает как раньше. В `@ykurilov/agent-chat-react` есть хук `useModelChoice`.
1123
+ - Кэш: у Gemini через OpenRouter метка кэша теперь стоит и на последнем сообщении истории (скользящая точка, на следующем ходе переезжает на новое), поэтому кэшируется вся переписка, а не только инструкция. Для своего провайдера то же задаётся полем `lastMessageProviderOptions` в `requestOptions`.
1124
+ - Профиль модели получил отметку `toolResultImageProviders`: провайдеры, через которых модель видит картинку в результате инструмента (по живым замерам). У GPT 6 Luna и Gemini 3.8 Flash это `["openrouter"]`, у GLM 5.3 Flash и DeepSeek 4.1 Flash отметки нет. Ядро отметку не читает; по ней веб-инструменты решают, выдавать ли `page_screenshot`.
1125
+ - Если ключа выбранного провайдера временно нет, ответ идёт на запасной модели, но выбор пользователя в чате не перезаписывается: когда ключ вернётся, чат снова отвечает выбранной моделью.
1126
+ - Хранилища: у чата новое поле `reasoning` (уровень рассуждений). SQLite: колонка `chats.reasoning`, PostgreSQL: колонка `agent_chat.chats.reasoning`; миграции применяются сами при открытии базы, данные не теряются.
1127
+
1128
+ ### Как обновить приложение
1129
+
1130
+ 1. Сервер (там, где `createChatHandler` или `createChatService`). Ищите `resolveModel`.
1131
+
1132
+ Было:
1133
+
1134
+ ```ts
1135
+ createChatHandler({
1136
+ // …
1137
+ resolveModel: async () => ({ modelId: "deepseek-4.1-flash", providerId: "deepseek" }),
1138
+ catalog: standardCatalog,
1139
+ });
1140
+ ```
1141
+
1142
+ Стало:
1143
+
1144
+ ```ts
1145
+ createChatHandler({
1146
+ // …
1147
+ // Только для нового чата; дальше модель выбирает пользователь. Можно не задавать:
1148
+ // тогда берётся первая модель каталога у первого провайдера с ключом.
1149
+ defaultChoice: { modelId: "deepseek-4.1-flash", providerId: "deepseek" },
1150
+ catalog: standardCatalog,
1151
+ });
1152
+ ```
1153
+
1154
+ Раньше `resolveModel` мог выбирать модель по пользователю или чату. Теперь это делает пользователь в меню; ограничить выбор можно каталогом (`extendCatalog(standardCatalog, { removeProviders, removeModels, addModels, addProviders, pinModelVersions })`) и ключами на сервере: в меню попадают только провайдеры, чей ключ есть в окружении.
1155
+ 2. Ключи. Хотите GPT 6 Luna, Gemini 3.8 Flash или GLM через OpenRouter — добавьте `OPENROUTER_API_KEY` в окружение сервера. Без него этих моделей в меню не будет; остальное работает как раньше.
1156
+ 3. Свой словарь надписей. Если вы задали `ChatLabelsProvider labels={…}` не через `{ ...russianLabels, … }`, добавьте новые ключи: `reasoningLevels` (названия уровней: `off`, `on`, `minimal`, `low`, `medium`, `high`, `max`), `modelChoiceFailed` («Не удалось сменить модель») и `modelMenuFailed` («Список моделей не загрузился»). Через `{ ...russianLabels, … }` ничего делать не нужно.
1157
+ 4. Свой скин. Договор версии 3: `Composer` получает необязательное `modelChoice` (`providers`, `current`, `onChange`, `reasoningLevels`, `reasoning`, `onReasoningChange`, `error`). Не рисуете его — выбора модели в чате не будет, всё остальное работает. Подробно: `specs/003-models-and-picker/contracts/skin.md`. Скин Тандема (`tandemSkin`) обновляется вместе с пакетом, править нечего.
1158
+ 5. Своё хранилище (только если вы реализовали `ChatStorage` сами). Добавьте необязательное поле `reasoning?: string` в чат: `createChat` принимает и сохраняет его, `getChat` отдаёт (без значения — поля нет), `updateChatModel(chatId, { modelId, providerId, reasoning? })` записывает его целиком и без `reasoning` стирает сохранённый уровень. Наборы проверок `describeChatStorageContract` из `@ykurilov/agent-chat-core/storage-contract` проверяют это. Хранилища SQLite и PostgreSQL из SDK обновляются сами.
1159
+ 6. Проверка: `pnpm typecheck` проходит; в браузере в поле ввода есть меню моделей и выбор рассуждений; выбор остаётся после обновления страницы; сообщение отправляется и ответ приходит по частям.
1160
+ 7. Только если вы уже ставили `@ykurilov/agent-chat-web-tools` из main до этого выпуска: опция `screenshotProviders` убрана. Снимок страницы выдаётся по отметке `toolResultImageProviders` в профиле модели.
1161
+
1162
+ Было:
1163
+
1164
+ ```ts
1165
+ const webTools = createWebTools({ ...firecrawl, screenshotProviders: ["openrouter"] });
1166
+ ```
1167
+
1168
+ Стало:
1169
+
1170
+ ```ts
1171
+ const webTools = createWebTools(firecrawl);
1172
+ // Своя модель, у которой картинка из инструмента доходит через провайдера: добавьте отметку в профиль.
1173
+ // extendCatalog(standardCatalog, { addModels: [{ ...профиль, toolResultImageProviders: ["openrouter"] }] })
1174
+ ```
1175
+ - b059fcc: Переписку теперь можно хранить в PostgreSQL, а чат подключается к Next.js. Хранилища проверяются одним и тем же набором тестов.
1176
+
1177
+ - Новый пакет `@ykurilov/agent-chat-storage-postgres`: `createPostgresChatStorage({ connectionString })`. Таблицы лежат в отдельной схеме `agent_chat`, миграции применяются сами при создании, два сервера на старте друг другу не мешают.
1178
+ - В `@ykurilov/agent-chat-server` новая точка входа `@ykurilov/agent-chat-server/next`: `createNextRouteHandlers(chatHandler)` для файла маршрута App Router.
1179
+ - В `@ykurilov/agent-chat-core` набор проверок `describeChatStorageContract` дополнен: порядок сообщений при равном времени, изоляция чатов, очень длинный ответ, пустые поля. Хранилище в памяти для тестов остаётся в ядре, отдельного пакета нет.
1180
+ - В `@ykurilov/agent-chat-storage-sqlite` сообщения с одинаковым временем создания теперь читаются в порядке записи (раньше порядок между ними был не определён). Порядок держится на новой колонке `seq` в таблице `messages`, а не на внутреннем номере строки SQLite, который `VACUUM` вправе поменять. Миграция применяется сама при открытии базы: старые сообщения получают `seq` в том порядке, в каком лежали, данные не теряются.
1181
+
1182
+ ### Как обновить приложение
1183
+
1184
+ Только если вы берёте PostgreSQL или Next.js. Остальным обновление ничего не требует: SQLite и Hono работают как раньше, миграция SQLite (колонка `seq`) применится сама при первом запуске, делать с базой ничего не нужно.
1185
+
1186
+ 1. PostgreSQL вместо SQLite. В `server/index.ts` замените создание хранилища:
1187
+
1188
+ Было:
1189
+
1190
+ ```ts
1191
+ const storage = createSqliteChatStorage({ filePath: "chat.sqlite" });
1192
+ // при остановке: storage.close()
1193
+ ```
1194
+
1195
+ Стало:
1196
+
1197
+ ```ts
1198
+ import { createPostgresChatStorage } from "@ykurilov/agent-chat-storage-postgres";
1199
+
1200
+ const storage = await createPostgresChatStorage({ connectionString: process.env.DATABASE_URL! });
1201
+ // при остановке: await chat.flush(); await storage.close()
1202
+ ```
1203
+
1204
+ Адрес базы положите в `DATABASE_URL`. Пользователю базы нужно право создавать схему и таблицы. Переписку из SQLite пакет сам не переносит.
1205
+ 2. Next.js. Создайте `app/api/chat/[[...path]]/route.ts` с `createNextRouteHandlers` и строками `export const dynamic = "force-dynamic"` и `export const runtime = "nodejs"`; `basePath` обработчика чата должен совпадать с путём маршрута. Нужен долгоживущий сервер, не serverless. Пример и подробности: `docs/getting-started.md`, раздел «Next.js».
1206
+ 3. Проверка: `pnpm typecheck` проходит, чат отвечает по частям, «Остановить» работает.
1207
+
1208
+ ### Patch Changes
1209
+
1210
+ - Updated dependencies [62027c6]
1211
+ - Updated dependencies [b059fcc]
1212
+ - Updated dependencies [61717c6]
1213
+ - @ykurilov/agent-chat-core@0.5.0