@ykurilov/agent-chat-react 0.0.0-stage → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1688 -0
- package/LICENSE +21 -0
- package/README.md +132 -2
- package/dist/chunk-7DVTSVKQ.js +6967 -0
- package/dist/chunk-7DVTSVKQ.js.map +1 -0
- package/dist/index.js +501 -0
- package/dist/index.js.map +1 -0
- package/dist/panel.js +399 -0
- package/dist/panel.js.map +1 -0
- package/dist/types/agent-chat-extensions.d.ts +80 -0
- package/dist/types/agent-chat.d.ts +103 -0
- package/dist/types/agent-panel/agent-panel-hooks.d.ts +14 -0
- package/dist/types/agent-panel/agent-panel-provider.d.ts +24 -0
- package/dist/types/agent-panel/agent-panel-skin.d.ts +41 -0
- package/dist/types/agent-panel/agent-panel.d.ts +13 -0
- package/dist/types/agent-panel/index.d.ts +8 -0
- package/dist/types/agent-panel/panel-context.d.ts +46 -0
- package/dist/types/agent-panel/panel-state.d.ts +12 -0
- package/dist/types/answer-editing/answer-editing.d.ts +25 -0
- package/dist/types/app-bridge/app-bridge.d.ts +50 -0
- package/dist/types/app-bridge/browser-calls.d.ts +24 -0
- package/dist/types/app-bridge/create-app-bridge.d.ts +35 -0
- package/dist/types/app-bridge/remembered-ids.d.ts +6 -0
- package/dist/types/app-bridge/tab-id.d.ts +10 -0
- package/dist/types/app-events.d.ts +39 -0
- package/dist/types/app-refusal.d.ts +21 -0
- package/dist/types/attachments-contract.d.ts +82 -0
- package/dist/types/background-tasks/use-background-tasks.d.ts +53 -0
- package/dist/types/chat-access.d.ts +33 -0
- package/dist/types/chat-list/active-chat-memory.d.ts +5 -0
- package/dist/types/chat-list/chat-list-client.d.ts +35 -0
- package/dist/types/chat-list/chat-list-skin.d.ts +63 -0
- package/dist/types/chat-list/chat-workspace.d.ts +17 -0
- package/dist/types/chat-list/use-chat-list-view.d.ts +12 -0
- package/dist/types/chat-list/use-chat-list.d.ts +47 -0
- package/dist/types/chat-list/use-workspace-list-sync.d.ts +7 -0
- package/dist/types/chat-state/chat-state.d.ts +27 -0
- package/dist/types/chat-types.d.ts +85 -0
- package/dist/types/chat-watch/chat-watch.d.ts +57 -0
- package/dist/types/close-stopped-tool-calls.d.ts +8 -0
- package/dist/types/composer-actions.d.ts +29 -0
- package/dist/types/diagrams/diagram-renders.d.ts +16 -0
- package/dist/types/diagrams/mermaid-blocks.d.ts +18 -0
- package/dist/types/diagrams/unique-svg-ids.d.ts +6 -0
- package/dist/types/diagrams/use-diagram-blocks.d.ts +25 -0
- package/dist/types/diagrams/use-diagram-colors.d.ts +8 -0
- package/dist/types/dictation-field.d.ts +19 -0
- package/dist/types/expanded-parts.d.ts +11 -0
- package/dist/types/feed-announcement.d.ts +7 -0
- package/dist/types/helpers/helper-call.d.ts +57 -0
- package/dist/types/index.d.ts +53 -0
- package/dist/types/labels.d.ts +468 -0
- package/dist/types/live-demos/answer-fences.d.ts +25 -0
- package/dist/types/live-demos/live-demo-document.d.ts +42 -0
- package/dist/types/live-demos/live-demo-frame.d.ts +14 -0
- package/dist/types/live-demos/use-live-demo-blocks.d.ts +18 -0
- package/dist/types/liveness/connection-errors.d.ts +15 -0
- package/dist/types/liveness/reconnect-attempts.d.ts +14 -0
- package/dist/types/liveness/request-deadline.d.ts +16 -0
- package/dist/types/liveness/silence-watchdog.d.ts +21 -0
- package/dist/types/media-viewer/answer-file-images.d.ts +18 -0
- package/dist/types/media-viewer/collect-viewer-items.d.ts +16 -0
- package/dist/types/media-viewer/generated-images.d.ts +20 -0
- package/dist/types/media-viewer/media-viewer-types.d.ts +17 -0
- package/dist/types/media-viewer/open-attachments.d.ts +9 -0
- package/dist/types/media-viewer/use-media-viewer.d.ts +23 -0
- package/dist/types/memory/agent-memory.d.ts +13 -0
- package/dist/types/memory/memory-client.d.ts +19 -0
- package/dist/types/memory/memory-skin.d.ts +103 -0
- package/dist/types/memory/memory-tree.d.ts +17 -0
- package/dist/types/memory/memory-types.d.ts +77 -0
- package/dist/types/memory/use-memory-notes.d.ts +23 -0
- package/dist/types/message-actions.d.ts +45 -0
- package/dist/types/message-feed.d.ts +27 -0
- package/dist/types/notices/agent-notices.d.ts +40 -0
- package/dist/types/pause.d.ts +2 -0
- package/dist/types/server-refusal.d.ts +16 -0
- package/dist/types/skin-contract.d.ts +687 -0
- package/dist/types/spending-status.d.ts +76 -0
- package/dist/types/steering/corrections.d.ts +19 -0
- package/dist/types/step-reset.d.ts +15 -0
- package/dist/types/subject-chat/subject-chat-loading.d.ts +8 -0
- package/dist/types/subject-chat/use-subject-chat-id.d.ts +21 -0
- package/dist/types/thinking-status/thinking-status.d.ts +33 -0
- package/dist/types/turn-finished.d.ts +14 -0
- package/dist/types/use-agent-chat.d.ts +228 -0
- package/dist/types/use-model-choice.d.ts +31 -0
- package/dist/types/use-voice-dialog.d.ts +18 -0
- package/dist/types/use-voice-dictation.d.ts +17 -0
- package/dist/types/voice-dialog.d.ts +103 -0
- package/dist/types/voice-input.d.ts +47 -0
- package/dist/types/voice-tags.d.ts +18 -0
- package/dist/types/voice-turn.d.ts +11 -0
- package/dist/types/widgets/custom-widget-guard.d.ts +21 -0
- package/dist/types/widgets/form-check.d.ts +29 -0
- package/dist/types/widgets/suggested-replies.d.ts +6 -0
- package/dist/types/widgets/use-widget-answer.d.ts +54 -0
- package/dist/types/widgets/widget-answer-keys.d.ts +14 -0
- package/dist/types/widgets/widget-messages.d.ts +14 -0
- package/dist/types/widgets/widget-part.d.ts +17 -0
- package/dist/types/widgets/widget-view.d.ts +50 -0
- package/dist/types/without-markdown.d.ts +1 -0
- package/dist/types/work-view/step-phrases.d.ts +37 -0
- package/dist/types/work-view/work-layout.d.ts +57 -0
- package/dist/types/work-view/work-steps-part.d.ts +15 -0
- package/package.json +53 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,1688 @@
|
|
|
1
|
+
# @ykurilov/agent-chat-react
|
|
2
|
+
|
|
3
|
+
## 0.56.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 1eadba9: Библиотека готовых картинок: новый пакет `@ykurilov/agent-chat-image-library` с инструментами `find_images` и
|
|
8
|
+
`show_image` поверх каталога приложения. Картинка показывается по ширине ответа с подписью, по нажатию — окно просмотра.
|
|
9
|
+
|
|
10
|
+
- `images` у `AgentChat`, `ChatWorkspace`, `AgentPanel` принимает и список клиентов: `images={[imagesClient, imageLibraryClient]}`.
|
|
11
|
+
Одиночный клиент работает как раньше.
|
|
12
|
+
- Договор скина версии 26: у `GeneratedImages` необязательные `wide` и `caption`. Тандем их рисует. Скин версии 25
|
|
13
|
+
работает без правок: картинка будет плиткой ближайшей обычной пропорции (1:1, 3:2, 2:3, 16:9, 9:16) без подписи.
|
|
14
|
+
|
|
15
|
+
Подробно и как подключить — `docs/image-library.md`.
|
|
16
|
+
|
|
17
|
+
Отказ `show_image`, `show_video` и недоступный каталог в `find_images`, `find_videos` теперь ошибка инструмента (`ImageLibraryError`, `VideoLibraryError` с полем `kind`).
|
|
18
|
+
В ленте строка «ошибка» вместо зелёной галочки, модель получает тот же текст. Код, который читал `{ отказ }` из
|
|
19
|
+
результата `show_video` или `find_videos`, должен смотреть на `state: "output-error"` у части сообщения.
|
|
20
|
+
|
|
21
|
+
## 0.55.0
|
|
22
|
+
|
|
23
|
+
### Minor Changes
|
|
24
|
+
|
|
25
|
+
- cdc7dd7: Голос: расход в лимитах, причина отказа, Next.js, язык записи, предел из настройки.
|
|
26
|
+
|
|
27
|
+
- `UsageSource` открыт для своих видов приложения (`"font_download"` без приведения типа), штатные — в
|
|
28
|
+
`BUILT_IN_USAGE_SOURCES`, тип `BuiltInUsageSource`. Новые штатные виды `voice_input` (секунды распознавания,
|
|
29
|
+
`inputTokens` — секунды) и `voice_polish` (чистка текста). Миграций нет.
|
|
30
|
+
- `createVoiceRelay({ spending: { service, recognitionUsdPerHour, chatId? } })`: лимит до диктовки, секунды звука в журнал
|
|
31
|
+
расхода. Чистка: `polish: { askModel, model }` через службу чата; у Together — `pricing`; своя `clean` может вернуть
|
|
32
|
+
`{ text, usage }`. Расход чистки — вид `voice_polish`.
|
|
33
|
+
- Причина отказа доходит до браузера кодом закрытия (`RELAY_CLOSE`): 4401 не вошёл, 4402 лимит, 4403 нет доступа,
|
|
34
|
+
4413 слишком длинно, 4429 часто, 4502 недоступен, 1009 кусок больше предела. Отказ приложения `{ refused: { code } }`
|
|
35
|
+
переводится по `code`. Новые виды ошибок `limit`, `forbidden`, `too_long`; 4401 теперь `forbidden` (был
|
|
36
|
+
`service_unavailable`).
|
|
37
|
+
- `@ykurilov/agent-chat-voice/node`: `createVoiceUpgrade({ path, relay, maxFrameBytes })` — голос на своём HTTP-сервере
|
|
38
|
+
Node.js (Next.js custom server), `shareVoiceRelay` / `sharedVoiceRelay` — мостик внутри процесса. Нужен `ws`.
|
|
39
|
+
Кривой путь — 400, обрыв во время проверки входа не роняет процесс, `authorizeTimeoutMs` (10 с) — 503.
|
|
40
|
+
- Ретранслятор: `helloTimeoutMs` (10 с без приветствия — 4400, место свободно), `maxFrameBytes` (1 МиБ, 1009) — единый
|
|
41
|
+
для Hono и node. Чат расхода из `?chat=` — только свой (`service.assertOwnChat`), иначе `"voice"`.
|
|
42
|
+
`allowedLanguageHints` с регионом (`pt-BR`) разрешает язык `pt`. Относительный `relayUrl` снова работает.
|
|
43
|
+
- Язык каждой записи: `voice.start(listener, { chatId, language })`, `createVoiceInput({ languageHints })`; сервер
|
|
44
|
+
проверяет значение сам (`allowedLanguageHints`). `AgentChat` передаёт текущий чат и язык словаря.
|
|
45
|
+
- react: подпись `time_limit` — строка или функция от предела сервера («предел 3 минуты»); `onError(kind, details)`;
|
|
46
|
+
подписи `limit`, `forbidden`, `too_long` необязательные в своём словаре; `voiceErrorText`.
|
|
47
|
+
|
|
48
|
+
## 0.54.0
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- c8fb0dd: Лента во время ответа: пока она прилипла к низу и едет за растущим ответом, запас над экраном держится. Раньше окно ленты
|
|
53
|
+
(virtua) убирало его на время прокрутки вниз и возвращало через 150 мс тишины, и готовые сообщения рисовались заново.
|
|
54
|
+
- 73d3aaa: Лента больше не теряет сообщение, которое сервер записал сам (утреннее по таймеру, касание, ход из мессенджера). Раньше
|
|
55
|
+
два перечитывания истории подряд (открытие подписки после своего ответа и `turn-started`) мешали друг другу: старое
|
|
56
|
+
приходило первым, а новое с сообщением выбрасывалось как «лента изменилась» — экран показывал его только после
|
|
57
|
+
перезагрузки. Теперь ленту, сменённую другим перечитыванием, более новое чтение всё равно показывает, а запоздавшее
|
|
58
|
+
старое новое не стирает. Действий от приложения не нужно.
|
|
59
|
+
|
|
60
|
+
## 0.53.0
|
|
61
|
+
|
|
62
|
+
### Minor Changes
|
|
63
|
+
|
|
64
|
+
- b6a7786: Сценарии: конец разговора без сбоев, «не сейчас», пояс по умолчанию, реплика сотрудника, тихое утро, касания с
|
|
65
|
+
карточками; расход по списку чатов.
|
|
66
|
+
|
|
67
|
+
- Конец разговора («срок вышел», «не завершил», прощание) больше не зависит от того, начался ли ход касания. Касания
|
|
68
|
+
сценария не режет потолок ядра «5 ходов сервера подряд»; у `deliverNotices` — свой потолок `maxServerTurnsInRow`
|
|
69
|
+
(число или `false`, по умолчанию 5, `MAX_SERVER_TURNS_IN_ROW`).
|
|
70
|
+
- Мягкий потолок сценария: 50 ходов сервера подряд без человека (`timers.maxServerTurnsInRow`), дальше касания ждут
|
|
71
|
+
его сообщения, конец разговора — в свой срок. Срок без причин писать не повторяется каждый час.
|
|
72
|
+
- `canWriteNow` → `false` — «не сейчас»: касание через час; `"never"` — молчать до сообщения человека; `{ retryAt: Date }`
|
|
73
|
+
— в свой момент (негодная дата — как `false`, прошлая — через минуту). Сроки конца разговора не снимаются.
|
|
74
|
+
- Человек писал до начала дня меньше 3 часов назад (`skipMorningIfHumanWithinMin`) — касания в начале дня ждут, не только
|
|
75
|
+
утреннее. Переход ноды по сроку и ответ на ночные сообщения не ждут.
|
|
76
|
+
- Сообщения после жёсткой рамки дня ставят срок на утро сами: ответ на накопленное приходит в `dayStart` при любых
|
|
77
|
+
касаниях. Их время — в данных чата (`flowHeldHuman`); для правила 3 часов, «не завершил» и перехода «человек молчит»
|
|
78
|
+
они — сообщения человека.
|
|
79
|
+
- `attach.deliver` получает `parts` — все части ответа для человека (карточка ролика, картинка, файл, виджет), `text`
|
|
80
|
+
прежний; `deliverNotices({ awaitAnswer }).answer.parts`; `deliverableParts` из ядра.
|
|
81
|
+
- `storage.sumUsageByChat({ chatIds, ownerId?, source?, since? })` — расход по списку чатов одним запросом (память,
|
|
82
|
+
SQLite, PostgreSQL; общие тесты хранилищ).
|
|
83
|
+
- `createAgentFlow({ defaultTimeZone })` — пояс, пока пояс человека неизвестен. Порядок: человек → страница →
|
|
84
|
+
`defaultTimeZone` → пояс службы → сервер. `prepareTurn` получает `pageTimeZone` — пояс, присланный страницей.
|
|
85
|
+
- `service.appendStaffMessage({ ownerId, chatId, text, staffName?, wakeAgent? })` — реплика живого сотрудника: модель
|
|
86
|
+
видит её словами сотрудника, `person_link_view` словами человека её не считает, проверки ответа агента её текст знают,
|
|
87
|
+
ход сама не начинает.
|
|
88
|
+
`flow.noteStaffReply` пишет тем же путём и принимает `staffName`. Экран: необязательный `StaffMessage` скина (договор 25),
|
|
89
|
+
без него — ответ агента с подписью `labels.notices.staffSaid`.
|
|
90
|
+
- `flow.tick()` в публичном типе, `factId` из пакета, перемотка до утра у чата, закреплённого через `adopt` без хода.
|
|
91
|
+
|
|
92
|
+
Как обновить приложение: снимите `it.fails` у теста «молчун все три дня»; ответ эксперта кладите
|
|
93
|
+
`appendStaffMessage` / `noteStaffReply` вместо сообщения собеседника и уберите обёртку `withoutNonPersonWords`; по желанию
|
|
94
|
+
`defaultTimeZone` и `StaffMessage` в своём скине; снимите `it.fails` у теста «касание, отложенное ночью до утра»;
|
|
95
|
+
доставляйте `message.parts` в своей доставке касаний; расход по чатам — `sumUsageByChat`.
|
|
96
|
+
|
|
97
|
+
## 0.52.0
|
|
98
|
+
|
|
99
|
+
No changes in this release.
|
|
100
|
+
|
|
101
|
+
## 0.51.0
|
|
102
|
+
|
|
103
|
+
No changes in this release.
|
|
104
|
+
|
|
105
|
+
## 0.50.0
|
|
106
|
+
|
|
107
|
+
No changes in this release.
|
|
108
|
+
|
|
109
|
+
## 0.49.0
|
|
110
|
+
|
|
111
|
+
### Minor Changes
|
|
112
|
+
|
|
113
|
+
- 084cef7: **Ломает приложения (типы и своё хранилище сценариев):** у типа поля сценария `FlowFieldType` появилось значение `"boolean"`, у источника
|
|
114
|
+
расхода `UsageSource` — `"first_message"`, у `SteerResult.status` — `"held"`. Исчерпывающий `switch` по ним перестанет
|
|
115
|
+
собираться. Своё хранилище сценариев (`FlowStore`) должно реализовать новый `renewLease()` и третий аргумент `setDue`.
|
|
116
|
+
|
|
117
|
+
Сценарии для приложений: программа по дням (ночь, касания, рамки дня, «срок вышел»), своя служба чата и доставка с
|
|
118
|
+
исходами, пауза сотрудника, конец разговора с итогом, карточка и пояс человека в базе приложения, попутные задачи.
|
|
119
|
+
Проверка готового ответа: «чужие слова» и «выдуманные ссылки». Подробно — `docs/flows.md`, `docs/reply-check.md`,
|
|
120
|
+
раздел 0.49.0 в `CHANGES.md`.
|
|
121
|
+
|
|
122
|
+
- **flows**: формат сценария версии 2 (`schedule`, `nudges`, таймер `state_days`, поле `boolean`, `sideTasks`,
|
|
123
|
+
`afterEnd`; версия 1 читается сама); `attach({ portFor, deliver })`, `admit`, `pause`/`resume`/`noteStaffReply`,
|
|
124
|
+
`reportDelivery`, `onFinish`, `clientCard`, `people`, `settled`, `now`; причина перехода `reason`; перенос чата
|
|
125
|
+
через `loadFlow` (`stateId`, `startedAt`, `enteredAt`); заголовки судьи `judge.openrouter.headers`; встроенная задача
|
|
126
|
+
«узнать город»; `checkFlow(flow, catalog, { checks })` и `FlowAppChecks` из `/format`; в редакторе панели дней,
|
|
127
|
+
касаний, рамок и попутных задач, `catalog.fields`, кнопка «К началу следующего дня» (`advanceToNextDay`). Новые
|
|
128
|
+
таблицы `flow_people`, `flow_side_tasks` и колонки `flow_chats` создаются сами.
|
|
129
|
+
- **core**: `replyChecks` (список или `{ checks, web: "replace" | "hold" }`), `now`, `markUndelivered`, ответы
|
|
130
|
+
`beforeTurn` `{ hold: true }` и `{ reply: { text } }`, `deliverNotices({ timeZone, awaitAnswer, usageSource })`,
|
|
131
|
+
источник расхода `first_message`.
|
|
132
|
+
- **flows, надёжность**: обход таймеров до 5 ходов одновременно; копия продлевает аренду чата перед ходом и во время
|
|
133
|
+
него. В `FlowStore` новые `renewLease()` и `setDue(…, { unlessDelivery })`. Недоставленное касание (`failed`,
|
|
134
|
+
`window_closed`) не засчитывается — тот же шаг через свой срок от отказа; в `FlowRun` новое поле `touch`. «Окно
|
|
135
|
+
закрыто» снимает любое сообщение человека; «не завершил» считает тишину и от конца паузы сотрудника; пока ждёт
|
|
136
|
+
повтор доставки, срок не затирается и новых ходов нет.
|
|
137
|
+
- **server**: `createChatServer` передаёт службе `replyChecks` и `now`.
|
|
138
|
+
- **messengers**: `onAnswerDelivery` — исход доставки каждого ответа; закрытое окно ВК (950) —
|
|
139
|
+
`MessengerWindowClosedError` и исход `window_closed`; с `replyChecks` уходит только проверенный текст.
|
|
140
|
+
- **web-tools**: `createPersonLinkTool` — инструмент `person_link_view`, агент смотрит ссылку, которую прислал человек
|
|
141
|
+
(адрес из почты ссылкой не считается).
|
|
142
|
+
- **reply-check** (новый пакет): `foreignWords` и `unknownLinks`; ответ из одной выдуманной ссылки заменяется запасной
|
|
143
|
+
фразой `labels.linkOnlyReply` (`russianReplyCheckLabels`, `englishReplyCheckLabels`, `ReplyCheckLabels`).
|
|
144
|
+
- **react**: договор скина версии 24 — необязательное `undelivered` у ответа агента и надпись `answerUndelivered`
|
|
145
|
+
(«Не доставлено»). Скин версии 23 работает без правок.
|
|
146
|
+
|
|
147
|
+
### Как обновить приложение
|
|
148
|
+
|
|
149
|
+
1. Поднимите все пакеты `@ykurilov/agent-chat-*` до этой версии вместе.
|
|
150
|
+
2. Найдите в коде приложения исчерпывающие `switch` и словари по этим типам и добавьте ветки:
|
|
151
|
+
- по `FlowFieldType` (поля карточки сценария) — `"boolean"`, «да/нет», значения `true`/`false`;
|
|
152
|
+
- по `UsageSource` (отчёты расходов) — `"first_message"`, «агент написал первым»;
|
|
153
|
+
- по `SteerResult.status` — `"held"`, «сообщение записано, хода нет».
|
|
154
|
+
3. Своё хранилище сценариев (свой `FlowStore`) — добавьте `renewLease({ chatId, owner, now, leaseMs }): Promise<boolean>`
|
|
155
|
+
(продлить аренду до `now + leaseMs`; `false` — чат уже не ваш) и третий аргумент `setDue(chatId, due, { unlessDelivery })`
|
|
156
|
+
(при `unlessDelivery` не писать срок, пока чат ждёт повтора доставки). Готовые хранилища SDK это уже умеют.
|
|
157
|
+
4. Сценарии сохраняются версией 2, а 0.48 её не откроет. Перед откатом на 0.48 держите копии сценариев версии 1.
|
|
158
|
+
5. Новое по желанию: каналы SDK и сценарий — `createMessengers({ onAnswerDelivery: (report) => flow.reportDelivery(report) })`;
|
|
159
|
+
проверка ответа — `pnpm add @ykurilov/agent-chat-reply-check` и `replyChecks` в `createChatServer`.
|
|
160
|
+
6. Проверка: `pnpm typecheck` проходит, чат отвечает, сценарий версии 1 открывается в редакторе.
|
|
161
|
+
|
|
162
|
+
## 0.48.0
|
|
163
|
+
|
|
164
|
+
No changes in this release.
|
|
165
|
+
|
|
166
|
+
## 0.47.0
|
|
167
|
+
|
|
168
|
+
No changes in this release.
|
|
169
|
+
|
|
170
|
+
## 0.46.2
|
|
171
|
+
|
|
172
|
+
No changes in this release.
|
|
173
|
+
|
|
174
|
+
## 0.46.1
|
|
175
|
+
|
|
176
|
+
No changes in this release.
|
|
177
|
+
|
|
178
|
+
## 0.46.0
|
|
179
|
+
|
|
180
|
+
### Patch Changes
|
|
181
|
+
|
|
182
|
+
- 56d7528: Наблюдения «Умных форм», без ломающих изменений.
|
|
183
|
+
|
|
184
|
+
- **Начатый вызов в базе до выполнения.** Вызов инструмента пишется в хранилище сразу, как модель его прислала, а
|
|
185
|
+
инструмент ждёт этой записи (не дольше 5 с). Упади сервер посреди инструмента — восстановленный ответ показывает
|
|
186
|
+
вызов прерванным, «Повторить» продолжает с него.
|
|
187
|
+
- **Последний шаг без действий.** На шаге без инструментов (последний шаг хода и помощника, итог по потолку хода)
|
|
188
|
+
вызов вопреки запрету не выполняется: у него закрывающий результат-отказ с постоянным текстом, подтверждение не
|
|
189
|
+
спрашивается.
|
|
190
|
+
- **`afterTurn` видит `stepLimit: true`**, когда ход упёрся в `agent.maxSteps`; в журнале строка `[предел шагов]`, в
|
|
191
|
+
ответе часть `data-step-limit` (модель её не видит). Человеку на экране ничего не показывается.
|
|
192
|
+
- **«Повторить» передаёт `previousAnswerId` и `abandonedAnswerIds`** в `beforeTurn` и `turnNotStarted`: оборванный
|
|
193
|
+
ответ, который заменит новый ответ-сосед.
|
|
194
|
+
- **React: «ответа уже нет» — лента сразу перечитывает историю.** Виджет, подтверждение и браузерный вызов, на которые
|
|
195
|
+
сервер ответил «не ждёт», перечитывают историю; сорвавшееся под нагрузкой чтение повторяется, лента не застывает.
|
|
196
|
+
- **Тандем: поле ввода не пересоздаётся**, когда приходят меню моделей, вложения, голос и другие поздние части
|
|
197
|
+
панели. Текст, набранный до загрузки меню, и фокус остаются.
|
|
198
|
+
|
|
199
|
+
### Как обновить приложение
|
|
200
|
+
|
|
201
|
+
Обновление само по себе ничего не требует. Обходы, которые теперь можно убрать:
|
|
202
|
+
|
|
203
|
+
1. Тесты, которые перед вводом текста ждут кнопку «Модель» (например, `tests/support/test-chat-ready.ts`), — ждать её
|
|
204
|
+
больше не нужно: текст, набранный раньше меню, не пропадает.
|
|
205
|
+
2. Тесты восстановления, которые перед остановкой сервера ждут `status: complete` в базе, могут обрывать сервер
|
|
206
|
+
посреди инструмента: вызов уже в базе и после восстановления виден прерванным.
|
|
207
|
+
3. Свои проверки «последний ли это шаг» внутри инструментов с последствиями (бронь, оплата) — SDK сам не выполняет
|
|
208
|
+
вызов на последнем шаге.
|
|
209
|
+
4. Свой подсчёт шагов в `afterTurn`, чтобы понять, что ход упёрся в предел, — заменить на `turn.stepLimit`.
|
|
210
|
+
5. Своё вычисление прежнего ответа для «Повторить» — брать `previousAnswerId` из `beforeTurn` / `turnNotStarted`.
|
|
211
|
+
|
|
212
|
+
## 0.45.0
|
|
213
|
+
|
|
214
|
+
### Minor Changes
|
|
215
|
+
|
|
216
|
+
- eb74c58: Простой вид работы агента: свойство экрана `workView="simple"` — пока ход идёт, одна живая строка с текущим шагом
|
|
217
|
+
(«Рисую календарь…»); после хода шаги свёрнуты в «Сделано N шагов» со списком понятных фраз, без кода и «Думал N сек».
|
|
218
|
+
По умолчанию `workView="detailed"` — всё как раньше. Фразы задаёт приложение (`stepPhrases`: имя инструмента →
|
|
219
|
+
`{ running, done, failed? }` или функция от вызова); встроенные пакеты дают свои во входе `./labels`
|
|
220
|
+
(`webToolStepPhrases`, `memoryStepPhrases`, `historySearchStepPhrases`, `backgroundStepPhrases`, и английские).
|
|
221
|
+
Договор скина 23: необязательный `ChatSkin.WorkSteps`; скин 22 работает без правок. Подробно — docs/work-view.md.
|
|
222
|
+
|
|
223
|
+
### Как обновить приложение
|
|
224
|
+
|
|
225
|
+
Ничего не требуется. Чтобы включить простой вид:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import { webToolStepPhrases } from "@ykurilov/agent-chat-web-tools/labels";
|
|
229
|
+
|
|
230
|
+
<ChatWorkspace
|
|
231
|
+
workView="simple"
|
|
232
|
+
stepPhrases={{
|
|
233
|
+
...webToolStepPhrases,
|
|
234
|
+
write_file: ({ input }) => ((input as { path?: string } | undefined)?.path ? { running: "Пишу файл", done: "Написал файл" } : undefined),
|
|
235
|
+
check_app: { running: "Проверяю приложение", done: "Проверил приложение" },
|
|
236
|
+
}}
|
|
237
|
+
…
|
|
238
|
+
/>;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Свой скин может нарисовать простой вид сам — `WorkSteps` по docs/work-view.md (`WorkStepsView`: `live` с фразой или
|
|
242
|
+
`done` со `steps`, `count`, `open`). Без него шаги рисуются строкой `ToolCallRow`: «Сделано N шагов» в `title`, входа нет,
|
|
243
|
+
а список понятных фраз приходит в `output` строкой — скин покажет его как обычный результат вызова.
|
|
244
|
+
- ac9aede: Скин Тандема ставит надписи дизайн-системы Тандем (`@ykurilov/tandem-ui` 0.7.0) на языке словаря чата: с `englishLabels`
|
|
245
|
+
в чате нет русских надписей. В словаре — поле `locale`.
|
|
246
|
+
|
|
247
|
+
## 0.44.0
|
|
248
|
+
|
|
249
|
+
### Minor Changes
|
|
250
|
+
|
|
251
|
+
- 6a2b5d9: Живые демонстрации: блок ```html-demo в ответе запускается в изолированной рамке (`sandbox="allow-scripts"`, строгий CSP,
|
|
252
|
+
наружу только высота). `agent.liveDemos` на сервере — одна постоянная строка инструкции; `liveDemos` у `AgentChat`,
|
|
253
|
+
`ChatWorkspace` и `AgentPanel`. Договор скина v22: необязательные `AgentText.renderDemo` и `ChatSkin.demoColors`, Тандем
|
|
254
|
+
умеет. Мессенджеры вместо блока пишут `liveDemoOnSite`, если у службы чата `liveDemos` (новое необязательное поле `ChatService.liveDemos`).
|
|
255
|
+
Код живёт в рамке внутри рамки: уйти на чужой адрес он не может, WebRTC убран. Выключатель: в чате с чужим текстом (веб-инструменты, файл человека, инструмент с `metadata: { externalContent: true }`)
|
|
256
|
+
демонстрации не запускаются — признак `ChatHistory.externalContent` и кусок потока `data-external-content`. Вложенные рамки
|
|
257
|
+
и `<link rel=preconnect>` в документе демонстрации запрещены. Всё необязательное, ломающих изменений нет. Как обновить
|
|
258
|
+
приложение — в CHANGES.md, раздел 0.44.0.
|
|
259
|
+
|
|
260
|
+
## 0.43.0
|
|
261
|
+
|
|
262
|
+
No changes in this release.
|
|
263
|
+
|
|
264
|
+
## 0.42.0
|
|
265
|
+
|
|
266
|
+
### Minor Changes
|
|
267
|
+
|
|
268
|
+
- a385276: Публичный чат больше не выдаёт посетителю служебное: модель, провайдера, расход, текст сводки сжатия и закрытые
|
|
269
|
+
ключи состояния. Ничего не ломает: по умолчанию всё как раньше.
|
|
270
|
+
|
|
271
|
+
- Новое право `modelDetails` в `chatCapabilities` (по умолчанию `true`). `false` убирает из истории и потока ответа
|
|
272
|
+
модель, провайдера и уровень рассуждений чата, `ownerId`, `metadata.modelId`/`providerId`/`usage` у сообщений,
|
|
273
|
+
`providerMetadata`/`callProviderMetadata`/`resultProviderMetadata` у частей и кусков, провайдера у отметки
|
|
274
|
+
`data-step-reset`, текст сводок сжатия (`summary: ""`, `summaryTokens: 0` в истории и `GET …/compactions`) и
|
|
275
|
+
выключает выбор модели (`modelChoice`). То же при переподключении и смене ветки. Хранилище, модель, хуки и лимиты
|
|
276
|
+
получают всё.
|
|
277
|
+
- Опция сервера и службы `modelMenu: ({ userId }) => boolean` закрывает `GET …/models` (403 `action_forbidden`,
|
|
278
|
+
`action: "modelDetails"`): меню чата не знает, право его не закрывает.
|
|
279
|
+
- Тандем рисует разделитель сжатия без текста сводки строкой без кнопки.
|
|
280
|
+
- Опция `privateStateKeys: ["booking"]` — закрытые верхние ключи состояния чата: не уходят ни в историю, ни в поток,
|
|
281
|
+
ни в `state-changed` наблюдения (`useChatState` их не видит). `getChatState`, `onStateChanged`, события и модель
|
|
282
|
+
видят их как обычно. Имя без точки и не на `$`, иначе `TypeError` при создании. Закрытое значение не возвращайте в
|
|
283
|
+
выходе инструмента (его прячет лишь `toolDetails: false`) и в тексте ответа (не прячется никак).
|
|
284
|
+
- Экран запрашивает меню моделей после истории и только если оно нужно: разрешён выбор модели (или вложениям старого
|
|
285
|
+
сервера). История всегда несёт `modelAcceptsImages` — видит ли модель чата картинки.
|
|
286
|
+
|
|
287
|
+
### Как обновить приложение
|
|
288
|
+
|
|
289
|
+
Публичному чату (посетитель без входа) в `server/chat.ts` или где создаётся обработчик. Посетитель — человек со своим
|
|
290
|
+
`userId` от `resolveUser` (например, `visitor:<id сессии из cookie>`); `isVisitor = (userId) => userId.startsWith("visitor:")`.
|
|
291
|
+
`modelDetails: false` прячет и модель, и текст сводок сжатия; `toolDetails: false` сводку не трогает.
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
createChatHandler({
|
|
295
|
+
// …
|
|
296
|
+
chatCapabilities: ({ userId }) => (isVisitor(userId) ? { modelDetails: false, toolDetails: false, reasoning: false /* , … */ } : {}),
|
|
297
|
+
modelMenu: ({ userId }) => !isVisitor(userId),
|
|
298
|
+
privateStateKeys: ["booking"], // верхние ключи состояния со служебными id
|
|
299
|
+
});
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Свой скин: `CompactionDivider` может получить `summary: ""` (при `modelDetails: false`) — раскрывать нечего, покажите
|
|
303
|
+
только строку.
|
|
304
|
+
|
|
305
|
+
## 0.41.0
|
|
306
|
+
|
|
307
|
+
### Minor Changes
|
|
308
|
+
|
|
309
|
+
- 9472fcb: Поправка на лету и английский интерфейс.
|
|
310
|
+
|
|
311
|
+
- `onTurnFinished` в `AgentChat` приходит и для ответа, закрытого ради поправки, — перед ответом на поправку. Раньше
|
|
312
|
+
событие получал только последний ответ, и действия под первым (например, «Вернуть эту версию») не появлялись.
|
|
313
|
+
Событие есть, даже если продолжение по поправке не стартовало (лимит расходов, чужие ходы, чат закрыт).
|
|
314
|
+
**Внимание:** при поправке на ходу `onTurnFinished` теперь приходит на каждый ответ — обработчик, который сохраняет
|
|
315
|
+
снимок или считает ходы, увидит 2 события и больше.
|
|
316
|
+
- Продолжение после поправки получает `place` самой свежей поправки, где он есть, иначе начала хода: служебная заметка
|
|
317
|
+
приложения уже новая, а поправка только с `tabId` место не стирает. Вкладка и пояс — от последней поправки: браузерные
|
|
318
|
+
инструменты продолжения идут на её вкладку. Кэш цел — блок места ложится в хвост поправки.
|
|
319
|
+
- `englishLabels` — полный английский словарь чата. `englishAttachmentsLabels`, `englishWebToolTitles`,
|
|
320
|
+
`englishHistorySearchTitles`, `englishMemoryToolTitles` (два последних — и из корня пакета); `withWebToolTitles` и
|
|
321
|
+
соседи берут словарь названий вторым аргументом (по умолчанию русский).
|
|
322
|
+
|
|
323
|
+
Как обновить: поднять версию. Английский интерфейс — `<ChatLabelsProvider labels={englishLabels}>` и
|
|
324
|
+
`createAttachmentsClient(englishAttachmentsLabels)` вместо своих словарей.
|
|
325
|
+
|
|
326
|
+
## 0.40.0
|
|
327
|
+
|
|
328
|
+
### Minor Changes
|
|
329
|
+
|
|
330
|
+
- 0e7aab3: Наблюдения «Умных форм», без ломающих изменений:
|
|
331
|
+
|
|
332
|
+
- `useChatState(chatId)` — состояние чата вне `AgentChat` (панель рядом с чатом): те же значения и живые обновления,
|
|
333
|
+
без своих запросов.
|
|
334
|
+
- `onModelChosen` — человек выбрал модель в меню: колбэк `AgentChat` и хук службы (`createChatService`, `createChatServer`).
|
|
335
|
+
- `{ by: "app" }` у `service.deleteChat` и `service.setModel` — сервер приложения действует без прав экрана; адреса HTTP
|
|
336
|
+
проверяют права, как раньше.
|
|
337
|
+
- «Переделать» у приветствия скрыто, а сервер такой запрос отклоняет (`InvalidMessageError`).
|
|
338
|
+
- `recoverInterruptedAnswers({ soleServer: true })` — у единственной копии сервера оборванные ответы прерываются сразу,
|
|
339
|
+
а не через 15–20 с. Только при старте процесса; не с несколькими копиями на одной базе и не в dev-режиме Next.js.
|
|
340
|
+
|
|
341
|
+
Как обновить: поднять версию, правки не обязательны. Обходные пути можно заменить — см. CHANGES.md «0.40.0».
|
|
342
|
+
|
|
343
|
+
## 0.39.1
|
|
344
|
+
|
|
345
|
+
### Patch Changes
|
|
346
|
+
|
|
347
|
+
- 5c0f3af: `useAgentChat` больше не грузит историю заново, когда приложение при перерисовке передаёт новые функции (`fetch`,
|
|
348
|
+
`onTurnRefused`, `onAppEvent`, `onExpectLiveAppEvents`). Раньше каждая новая функция `fetch` слала второй GET истории
|
|
349
|
+
и сбрасывала `loaded`: поле ввода на миг пропадало, набранный текст мог потеряться. Теперь историю грузят заново
|
|
350
|
+
только другой чат (`api`, `chatId`) и «Повторить» после сбоя; перечитывание того же чата (`reloadKey`) поле не трогает.
|
|
351
|
+
То же в `useModelChoice`: новый `fetch` больше не перечитывает меню моделей.
|
|
352
|
+
|
|
353
|
+
Как обновить: поднять версию, правки в коде приложения не нужны. Обходной `useCallback`/`useMemo` вокруг `fetch`
|
|
354
|
+
можно оставить или убрать.
|
|
355
|
+
|
|
356
|
+
## 0.39.0
|
|
357
|
+
|
|
358
|
+
### Minor Changes
|
|
359
|
+
|
|
360
|
+
- 3b90429: Виджеты: `onStateChanged` с причиной `widgetAnswer` после записей `checkAnswer`; `ctx.emitAppEvent` в `checkAnswer`
|
|
361
|
+
(событие вместе с ответом виджета, на странице — живым один раз); `chatId` и `toolCallId` в `CustomWidgetProps`;
|
|
362
|
+
`agent.widgets.enabledFor` — набор виджетов по чату, выключенных модель не видит, их прошлые вызовы идут модели текстом.
|
|
363
|
+
Всё необязательное, ломающих изменений нет. Как обновить приложение — в CHANGES.md, раздел 0.39.0.
|
|
364
|
+
|
|
365
|
+
## 0.38.0
|
|
366
|
+
|
|
367
|
+
No changes in this release.
|
|
368
|
+
|
|
369
|
+
## 0.37.0
|
|
370
|
+
|
|
371
|
+
No changes in this release.
|
|
372
|
+
|
|
373
|
+
## 0.36.0
|
|
374
|
+
|
|
375
|
+
### Minor Changes
|
|
376
|
+
|
|
377
|
+
- 3487fb4: Публичному посетителю можно не показывать кухню агента. Новые права в `chatCapabilities`: `reasoning` (мысли модели) и
|
|
378
|
+
`toolDetails` (служебные инструменты). По умолчанию оба `true` — всё как раньше.
|
|
379
|
+
|
|
380
|
+
- При запрете мысли и вызовы служебных инструментов (вход, выход, запрос подтверждения) не уходят в браузер: ни в историю
|
|
381
|
+
(`GET`, `getHistory`, `peekHistory`, `selectBranch`), ни в поток ответа, ни в повтор кусков после переподключения
|
|
382
|
+
(`GET …/stream`). Из метаданных ответа пропадает `reasoningDurationsMs`, у текста и вызовов — детали рассуждений
|
|
383
|
+
OpenRouter (`providerMetadata.openrouter.reasoning_details`).
|
|
384
|
+
- Модель, хранилище, `afterTurn`, сжатие и поиск по истории получают ответ целиком.
|
|
385
|
+
- Виджеты (`agent.widgets`) и браузерные инструменты видны всегда. Свой инструмент с карточкой на экране пометьте в его
|
|
386
|
+
описании — `metadata: { showOnScreen: true }` (поле AI SDK, модели не уходит). Метка записывается в каждый вызов
|
|
387
|
+
(`toolMetadata`), поэтому работает и для набора инструментов функцией от модели. Вызовы, записанные до пометки, метки
|
|
388
|
+
не имеют и при запрете скрыты.
|
|
389
|
+
|
|
390
|
+
```ts
|
|
391
|
+
const showCard = tool({ description: "Карточка тарифа", inputSchema, metadata: { showOnScreen: true }, execute });
|
|
392
|
+
createChatServer({
|
|
393
|
+
agent: { instructions, tools: { show_card: showCard, lookup } },
|
|
394
|
+
chatCapabilities: ({ userId }) => (isVisitor(userId) ? { reasoning: false, toolDetails: false } : {}),
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
- Права считаются на того, кто открыл поток, в момент открытия.
|
|
399
|
+
- Вызов, который спросил подтверждения (`needsApproval`), виден сам, без пометки: иначе человек не увидит вопроса и ход
|
|
400
|
+
зависнет. Вызов того же инструмента без вопроса скрыт.
|
|
401
|
+
- Свои тесты, которые сравнивают `capabilities` истории целиком, получат два новых поля `reasoning` и `toolDetails`.
|
|
402
|
+
|
|
403
|
+
Ломает типы: тип ChatCapabilities получил новые обязательные поля, литералы надо дополнить.
|
|
404
|
+
- 5f13c74: Поправку к идущему ответу можно запретить правом `steer` в `chatCapabilities`. Раньше `POST …/steer` принимался, даже
|
|
405
|
+
если экран прятал поправку (`steering={false}`). Теперь при `steer: false` сервер отвечает 403
|
|
406
|
+
`{ error: { kind: "action_forbidden", action: "steer", … } }` до любой записи, `service.steer` бросает
|
|
407
|
+
`ActionForbiddenError`, а `<AgentChat>` не предлагает поправку и ждёт конца ответа. По умолчанию право `true`.
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
chatCapabilities: ({ userId }) => (isVisitor(userId) ? { steer: false, edit: false, regenerate: false, branches: false } : {}),
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
Свои тесты, которые сравнивают `capabilities` истории целиком, получат новое поле `steer`.
|
|
414
|
+
|
|
415
|
+
Ломает типы: тип ChatCapabilities получил новые обязательные поля, литералы надо дополнить.
|
|
416
|
+
|
|
417
|
+
## 0.35.0
|
|
418
|
+
|
|
419
|
+
### Patch Changes
|
|
420
|
+
|
|
421
|
+
- 6ceee78: Две правки клиента.
|
|
422
|
+
|
|
423
|
+
- Чат больше не просит `GET {chat}/tasks` у сервера без фоновых задач (раньше — 404 и красная строка в консоли при каждом открытии). История теперь несёт `backgroundTasks: true | false` (сервер знает, подключён ли `background`), клиент при `false` задач не запрашивает. Сервер до этой правки поля не присылает: клиент спрашивает адрес задач, как раньше, поэтому приложения со старым сервером и фоном продолжают работать.
|
|
424
|
+
- Скрытая строка для экранного диктора получает текст ответа без разметки Markdown: заголовки, жирный, курсив, зачёркнутое, код, списки и цитаты без значков, у ссылки остаётся её текст. Звёздочки больше не зачитываются вслух. Обычный текст не трогается: выделение снимается только целыми словами, поэтому «5\*6\*7», «src/\*.ts» и «\_\_init\_\_» остаются; «> 100 000 ₽» — не цитата; решётка в конце снимается только у заголовка.
|
|
425
|
+
|
|
426
|
+
## 0.34.0
|
|
427
|
+
|
|
428
|
+
### Minor Changes
|
|
429
|
+
|
|
430
|
+
- d039ce4: Поправка на лету: человек пишет агенту, пока тот отвечает, и не ждёт конца ответа и не жмёт «Стоп».
|
|
431
|
+
|
|
432
|
+
Что это для человека: поле ввода во время ответа свободно, «Стоп» рядом с «Отправить». Поправка сразу видна в ленте с пометкой
|
|
433
|
+
«Агент учтёт на следующем шаге». Агент берёт её после очередных вызовов инструментов и продолжает с её учётом; сделанное до
|
|
434
|
+
поправки остаётся. Ответ кончился раньше — поправка запускает новый ход. Несколько поправок — все, по порядку. «Стоп» поправку
|
|
435
|
+
не теряет. Поправка хранится в базе и видна после обновления страницы.
|
|
436
|
+
|
|
437
|
+
Новое:
|
|
438
|
+
|
|
439
|
+
- ядро: `service.steer(userId, chatId, message, { page? })` → `{ status: "queued" | "started", messageId }`; в истории `pendingMessages`
|
|
440
|
+
(ждущие поправки); `isCorrection`, `STEER_PART_TYPE`, тип `SteerResult`; повтор с тем же `message.id` — тот же итог без второй копии;
|
|
441
|
+
- сервер: `POST …/:id/steer` `{ message, place?, tabId? }` → `202`;
|
|
442
|
+
- react: проп `steering` у `AgentChat` (включено, если скин рисует поправки; `false` — выключить), опция `steering` у
|
|
443
|
+
`useAgentChat` (по умолчанию выключено), у него же `canSteer`, `corrections`; надпись `steeringPending`
|
|
444
|
+
(русская по умолчанию, английская в `englishWorkLabels`);
|
|
445
|
+
- договор скина 21: `ChatSkin.steering?`, `Composer.canSteer?`, `UserMessage.pending?` ([docs/skin-v21.md](https://github.com/ykurilov/agent-chat-sdk/blob/main/docs/skin-v21.md));
|
|
446
|
+
- Тандем: во время ответа «Остановить» рядом с «Отправить» в рамке поля, пометка под ждущей поправкой. На узком экране
|
|
447
|
+
кнопка голосового режима на время ответа прячется, чтобы ряд не вылез за край.
|
|
448
|
+
|
|
449
|
+
### Как обновить приложение
|
|
450
|
+
|
|
451
|
+
Обязательных шагов нет: со скином Тандема поправки включаются сами, свой скин и своя лента работают как раньше.
|
|
452
|
+
|
|
453
|
+
1. Проверьте, что `@ykurilov/agent-chat-server` обновлён вместе с `@ykurilov/agent-chat-react`: старый сервер не знает `POST …/steer`,
|
|
454
|
+
и поправка вернётся в поле с ошибкой.
|
|
455
|
+
2. Свой словарь надписей (полный объект, не поверх `russianLabels`) — добавьте `steeringPending`. Без неё будет русская надпись по умолчанию.
|
|
456
|
+
3. Свой скин: без `steering: true` поле во время ответа занято, как раньше, — поправок у людей не будет. Чтобы были, поддержите
|
|
457
|
+
`Composer.canSteer` и `UserMessage.pending` и поставьте в скин `steering: true` по [docs/skin-v21.md](https://github.com/ykurilov/agent-chat-sdk/blob/main/docs/skin-v21.md).
|
|
458
|
+
4. Своя лента на `useAgentChat`: включите `useAgentChat({ steering: true })`, рисуйте `corrections` после `messages`, пометкой — `steeringPending`.
|
|
459
|
+
5. Несколько копий сервера: поправка принимается копией, на которой идёт ответ (как «Стоп»), иначе `409 busy`. Держите запросы
|
|
460
|
+
одного чата на одной копии.
|
|
461
|
+
6. Не нужно со скином Тандема — `<AgentChat steering={false} />`.
|
|
462
|
+
7. Проверка: `pnpm typecheck`; в чате во время долгого ответа отправьте поправку — она видна с пометкой, затем агент отвечает с её учётом.
|
|
463
|
+
|
|
464
|
+
## 0.33.0
|
|
465
|
+
|
|
466
|
+
No changes in this release.
|
|
467
|
+
|
|
468
|
+
## 0.32.0
|
|
469
|
+
|
|
470
|
+
### Patch Changes
|
|
471
|
+
|
|
472
|
+
- 4087a22: Чат: сообщения с развилками (стрелки «‹ 2/3 ›») больше не перерисовываются на каждый кусок нового ответа. Отправка, нажатая до загрузки истории, сбрасывается, если человек перешёл в другой чат, и не уходит туда неожиданно. «Повторить» загрузку того же чата её по-прежнему сохраняет.
|
|
473
|
+
|
|
474
|
+
## 0.31.2
|
|
475
|
+
|
|
476
|
+
No changes in this release.
|
|
477
|
+
|
|
478
|
+
## 0.31.1
|
|
479
|
+
|
|
480
|
+
No changes in this release.
|
|
481
|
+
|
|
482
|
+
## 0.31.0
|
|
483
|
+
|
|
484
|
+
No changes in this release.
|
|
485
|
+
|
|
486
|
+
## 0.30.0
|
|
487
|
+
|
|
488
|
+
No changes in this release.
|
|
489
|
+
|
|
490
|
+
## 0.29.0
|
|
491
|
+
|
|
492
|
+
No changes in this release.
|
|
493
|
+
|
|
494
|
+
## 0.28.0
|
|
495
|
+
|
|
496
|
+
### Minor Changes
|
|
497
|
+
|
|
498
|
+
- d63ccaa: Ломает приложения: закрытие чата теперь запись на ветке, а не флаг навсегда. Человек исправил сообщение до точки
|
|
499
|
+
закрытия, нажал «Переделать» или перешёл на другую ветку — на новой ветке чат открыт. Вернулся стрелкой назад — снова
|
|
500
|
+
закрыт. Отправка, «Повторить», подтверждения и виджеты в закрытом чате по-прежнему запрещены (409 `chat_closed`).
|
|
501
|
+
|
|
502
|
+
Плюс приложение узнаёт о смене состояния чата (`model.state`): на сервере — хук, на экране — проп и хук React.
|
|
503
|
+
|
|
504
|
+
- **Сервер:** `onStateChanged({ ownerId, chatId, reason, state, previousState, atMessageId })` у `createChatServer` и
|
|
505
|
+
`createChatService`. Зовётся после хода и смены ветки (`reason`: `turn`, `editMessage`, `regenerate`, `retry`,
|
|
506
|
+
`selectBranch`, `editAnswer`), только если состояние сменилось. Ход его не ждёт. Вызовы одного чата — по очереди.
|
|
507
|
+
Сбой — повторы (`onStateChangedAttempts`, 3) в сроке (`onStateChangedTimeoutMs`, 30 000), потом строка в `logError`.
|
|
508
|
+
Доставка только пока сервер жив: упал между ответом и хуком — хук не позовётся, сверяйте по `getChatState`.
|
|
509
|
+
- **События:** `state-changed` `{ chatId, reason, state, atMessageId }` (вкладка получает через `watch`) и
|
|
510
|
+
`state-changed-failed`. `chat-closed` и `chat-reopened` теперь приходят и при смене ветки.
|
|
511
|
+
- **Экран:** `<AgentChat onStateChange={(state) => …} />` — после загрузки и при каждой смене, в том числе по ходу
|
|
512
|
+
ответа и из другой вкладки (событие `state-changed`). Внутри ленты — `useChatState()`, у `useAgentChat` — `chatState`. Тип `ChatStateValues`.
|
|
513
|
+
- **Ответ сохраняется в базу сразу после итога каждого инструмента**, а не раз в полсекунды: запись состояния
|
|
514
|
+
переживает падение сервера. Промежуточные результаты (`preliminary`) — по-прежнему раз в полсекунды.
|
|
515
|
+
- **Служебные ключи** состояния (`$…`) больше не уходят в браузер: ни в истории, ни в потоке ответа.
|
|
516
|
+
- **`getChatState` дешёвый:** читает только связи сообщений и записи состояния. SQLite и Postgres получили метод
|
|
517
|
+
`listMessagePartsOfTypes(chatId, types)`. Новых таблиц и миграций нет.
|
|
518
|
+
- **Колонка `closed`** в записи чата — копия закрытия текущей ветки для `listChats`. `chatCapabilities` (`chat.closed`)
|
|
519
|
+
и история берут закрытие с ветки. Не записалась копия — её дописывает следующее открытие истории.
|
|
520
|
+
- **Чаты, закрытые в 0.27,** остаются закрытыми. Перенос в запись ленивый, без миграции: запись появится при первой
|
|
521
|
+
правке, «Переделать», выборе ветки или правке ответа в этом чате.
|
|
522
|
+
- Ядро экспортирует `CLOSED_STATE_KEY`, `isStateMessage`, `publicAppStateParts`, `DEFAULT_STATE_CHANGED_ATTEMPTS`,
|
|
523
|
+
`DEFAULT_STATE_CHANGED_TIMEOUT_MS`, типы `StateChangedHandler`, `StateChangedInput`, `StateChangeReason`,
|
|
524
|
+
`MessagePartsLink`.
|
|
525
|
+
|
|
526
|
+
### Как обновить приложение
|
|
527
|
+
|
|
528
|
+
1. **Закрытие должно быть навсегда** (нельзя уйти правкой или веткой): в настройках сервера (`createChatServer`)
|
|
529
|
+
добавьте права по закрытию.
|
|
530
|
+
|
|
531
|
+
Было: ничего.
|
|
532
|
+
|
|
533
|
+
Стало:
|
|
534
|
+
|
|
535
|
+
```ts
|
|
536
|
+
chatCapabilities: ({ chat }) =>
|
|
537
|
+
chat.closed ? { edit: false, regenerate: false, branches: false, answerEditing: false } : {},
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Если `chatCapabilities` уже есть — допишите эту ветку в него. Если правка до закрытия вам подходит — шаг пропустите.
|
|
541
|
+
2. **`switch` по событиям чата** (`ChatEvent`, `service.events`) с проверкой полноты (`never`): добавьте ветки
|
|
542
|
+
`state-changed` и `state-changed-failed`.
|
|
543
|
+
3. **Своё хранилище** (не SQLite и не Postgres): ничего не обязательно. Для дешёвого чтения состояния добавьте
|
|
544
|
+
`listMessagePartsOfTypes(chatId, types)` → `[{ id, parentId, createdAt, parts }]` в порядке `listMessages`, только
|
|
545
|
+
части нужных типов. Общие тесты хранилища проверят его, флаг `partsOfTypes: "required"` делает его обязательным.
|
|
546
|
+
`setChatClosed` теперь пишет только копию для списка: без него закрытие работает, но список его не видит.
|
|
547
|
+
4. **Своя реализация `ChatService`** (заглушки в тестах): полей не прибавилось. Если вы проверяли, что правка или
|
|
548
|
+
«Переделать» в закрытом чате дают `ChatClosedError`, — теперь они идут.
|
|
549
|
+
5. **Таблица-копия состояния** (по желанию): `onStateChanged` с полной заменой записи —
|
|
550
|
+
`await db.upsertForm({ chatId, fields: state.fields ?? {} })`.
|
|
551
|
+
6. Проверка: `pnpm typecheck`; закройте чат инструментом, исправьте вопрос до закрытия — без шага 1 чат открыт, с
|
|
552
|
+
шагом 1 правка недоступна.
|
|
553
|
+
- 189cba9: Проверка ответа виджета на сервере. Раньше ответ проверялся только схемой. Теперь приложение может проверить его с
|
|
554
|
+
доступом к своей базе («это время уже заняли»), отказать человеку своими словами, подменить ответ и записать состояние чата.
|
|
555
|
+
|
|
556
|
+
```ts
|
|
557
|
+
agent: {
|
|
558
|
+
widgets: {
|
|
559
|
+
custom: [
|
|
560
|
+
{
|
|
561
|
+
...pickSlot, // виджет из общего файла; проверка — только на сервере
|
|
562
|
+
checkAnswer: async (answer, ctx) => {
|
|
563
|
+
const hold = await bookings.tryHold({ key: ctx.toolCallId, chatId: ctx.chatId, start: answer.slot });
|
|
564
|
+
if (!hold) return { refuse: "Это время уже заняли. Выберите другое." }; // виджет остаётся открытым
|
|
565
|
+
ctx.state.set("booking.holdId", hold.id); // ляжет в ответ вместе с результатом виджета
|
|
566
|
+
return { answer: { ...answer, holdId: hold.id } }; // подмена ответа; можно вернуть и undefined — принять как есть
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
],
|
|
570
|
+
},
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
- Проверка зовётся после схемы, пока чат удержан за этим ответом. `ctx`: `ownerId`, `chatId`, `messageId`, `toolName`,
|
|
575
|
+
`toolCallId`, `input`, `subject?`, `chatData?`, `state` (запись, как `model.state`), `signal` (`AbortSignal`). Исходы: `undefined` — принять,
|
|
576
|
+
`{ answer }` — принять с заменой (её видят модель и экран; до 16 КБ, схемой не перепроверяется), `{ refuse }` — отказать
|
|
577
|
+
текстом до 300 знаков.
|
|
578
|
+
- Общая проверка для всех виджетов без своей, в том числе базовых (`ask_choice` и др.):
|
|
579
|
+
`agent.widgets.checkAnswer(call, answer, ctx)`, где `call` — `{ toolName, toolCallId, input }`.
|
|
580
|
+
- Отказ: служба бросает `WidgetAnswerRefusedError`, сервер отвечает `409 { error: { kind: "widget_answer_refused", code,
|
|
581
|
+
message } }` (новый вид в `HandlerErrorKind`), экран показывает текст под виджетом и оставляет виджет открытым; поле ввода и
|
|
582
|
+
`onTurnRefused` не трогает. Ничего не записано, повтор ответа возможен.
|
|
583
|
+
- Записи `ctx.state` принятого ответа ложатся частями `data-app-state` вместе с результатом виджета; отказ и исключение их
|
|
584
|
+
отбрасывают. `getChatState` и следующий ход видят их.
|
|
585
|
+
- Исключение проверки — ответ 500, ничего не записано, виджет открыт, в `logError` строка
|
|
586
|
+
`[виджет] ошибка проверки чат=… вызов=…` с именем ошибки, без её текста.
|
|
587
|
+
- Срок проверки (`agent.widgets.checkAnswerTimeoutMs`, по умолчанию 5 000 мс) — не сбой: служба бросает
|
|
588
|
+
`WidgetAnswerCheckTimeoutError`, сервер отвечает `504 { error: { kind: "widget_check_timeout", code, message } }` (новый вид
|
|
589
|
+
в `HandlerErrorKind`), ничего не записано, виджет открыт, в журнале строка `[виджет] отказ answer_check_timeout …`, а не
|
|
590
|
+
ошибка. Страница показывает под виджетом новую подпись `widgets.answerCheckTimeout` («Не удалось проверить ответ,
|
|
591
|
+
попробуйте ещё раз»; её можно переопределить в `labels`), у `WidgetFailureKind` новый вид `check_timeout`. Зависшая
|
|
592
|
+
проверка не держит чат занятым. Срок проверки держите заметно меньше `requestTimeoutMs` страницы (10 000 мс).
|
|
593
|
+
- `ctx.signal` (`AbortSignal`) срабатывает по сроку проверки и по «Стопу» в чате. После отмены служба ничего не пишет,
|
|
594
|
+
виджет остаётся открытым, но остановить работу приложения она не может: слушайте сигнал сами
|
|
595
|
+
(`fetch(url, { signal: ctx.signal })`). «Стоп» во время проверки — не сбой: служба бросает новую
|
|
596
|
+
`WidgetAnswerCheckAbortedError`, сервер отвечает `409 { error: { kind: "widget_answer_cancelled", code, message } }`
|
|
597
|
+
(новый вид в `HandlerErrorKind`), в журнале строка `[виджет] отказ answer_cancelled …`, страница тихо оставляет виджет
|
|
598
|
+
открытым, без надписи под ним.
|
|
599
|
+
- Чат закрыли (`closeChat`) во время проверки: ответ не пишется, служба бросает `ChatClosedError`, ждущий виджет закрывается
|
|
600
|
+
отказом, как при обычном закрытии.
|
|
601
|
+
- Для React: у `WidgetFailureKind` новый вид `answer_refused` (текст отказа в `text`). Скины, которые перебирают виды отказа
|
|
602
|
+
полным `switch`, добавляют ветку; готовый скин и `AgentChat` ничего не требуют.
|
|
603
|
+
- Без `checkAnswer` запрос к модели, хранилище и ответы сервера прежние.
|
|
604
|
+
|
|
605
|
+
Обновление ничего не требует. Если приложение разбирает ответы сервера полным `switch` по `HandlerErrorKind`, добавьте
|
|
606
|
+
`widget_answer_refused`, `widget_answer_cancelled` и `widget_check_timeout`. Подробности и пример — `docs/widgets.md`, раздел «Проверка ответа на сервере».
|
|
607
|
+
|
|
608
|
+
## 0.27.0
|
|
609
|
+
|
|
610
|
+
### Minor Changes
|
|
611
|
+
|
|
612
|
+
- 5e4ed28: **Что может сломаться:**
|
|
613
|
+
|
|
614
|
+
- исчерпывающий `switch` по виду ошибки сервера (`HandlerErrorKind`) и виду отказа на экране (`AppRefusalKind`): новые значения `action_forbidden`, `chat_closed`, `chat_creation_refused`;
|
|
615
|
+
- исчерпывающий `switch` по причине `turnNotStarted` (`TurnNotStartedReason`): новое значение `chat_closed`;
|
|
616
|
+
- свой словарь надписей `ChatLabels`, собранный целиком: новое обязательное поле `chatListClosed`.
|
|
617
|
+
|
|
618
|
+
Права человека в чате и закрытый чат. Приложение функцией `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.
|
|
619
|
+
|
|
620
|
+
- Права: опечатка в названии права (`edits` вместо `edit`) — ошибка приложения (`TypeError`), а не молчаливый доступ. За один вызов службы права спрашиваются не больше раза.
|
|
621
|
+
- Закрытие побеждает паузу: если чат закрыт, пока ответ ждёт подтверждения или виджета, ждущие запросы закрываются отказом, как при «Стопе», и ответ считается законченным.
|
|
622
|
+
- Закрытие из инструмента записывается сразу при вызове: после падения сервера чат уже закрыт. Чат, закрытый во время проверки перед ходом, хода не начинает: 409 `chat_closed`, резерв приложения закрывается `turnNotStarted` с причиной `chat_closed`.
|
|
623
|
+
- Новый чат: `beforeCreateChat({ userId, origin? })` вернул `{ code, message }` — `POST {base}` отвечает `403 { error: { kind: "chat_creation_refused", code, message } }`, мессенджер (`/new`, первый чат) — текстом. Чаты объектов и импорт хук не спрашивают.
|
|
624
|
+
- `listChats` отдаёт `closed`: на экране и в `/chats` мессенджера у закрытого чата пометка.
|
|
625
|
+
- Сервер: запрещённое действие — `403 { error: { kind: "action_forbidden", action, code, message } }`, закрытый чат — `409 { error: { kind: "chat_closed", code, message } }`. Мессенджеры отвечают человеку текстом.
|
|
626
|
+
- Мессенджеры: голосовое при `voice: false` и файлы при `attachments: false` не скачиваются и не распознаются, `/chats` смотрит на `chatList`. Своя служба может реализовать необязательный `assertCapability`.
|
|
627
|
+
- Песочница: закрытие работает как в бою, а кнопка «Открыть чат снова» (`POST {sandboxBase}/chats/:chatId/reopen`, только админ) позволяет продолжить тест. Скину песочницы — необязательный `SandboxSkin.ChatReopen`, надписи `SandboxLabels.chatReopen` (необязательные); в Тандеме кнопка своя.
|
|
628
|
+
- Экран: кнопки по правам прячутся сами, после ответа 403 `action_forbidden` права перечитываются, закрытый чат блокирует поле и показывает текст закрытия. Голосовое сообщение при `voice: false` не уходит. Новое:
|
|
629
|
+
- `AppRefusalKind` — `chat_creation_refused`; `useChatList` — `onCreateRefused` и `creationRefusal`, `ChatWorkspace` отдаёт отказ в `onTurnRefused`;
|
|
630
|
+
- `ChatListEntry.closed` и `ChatListItemView.closed` — пометка закрытого чата в списке, надпись `chatListClosed` («Закрыт», обязательное поле `ChatLabels`);
|
|
631
|
+
- `reloadKey` у `AgentChat`, `useAgentChat` и `ChatWorkspace` — перечитать историю, когда чат изменили не через экран;
|
|
632
|
+
- `onChatUnavailable` у `AgentChat` — служебный колбэк для `ChatWorkspace`, приложению не нужен;
|
|
633
|
+
- `ChatWorkspace` при `chatList: false` не мигает списком: он появляется после первой истории (и при `chatList: true` — тоже с этой короткой задержкой) и потом не запрашивается;
|
|
634
|
+
- `onTurnRefused` получает новые виды отказов, `onChatAccessChange` сообщает права и закрытие, `ChatWorkspace` при `chatList: false` показывает только чат.
|
|
635
|
+
- Скин: договор v20 — необязательный `ChatClosedNotice` и необязательный `list` у `ChatListSkin.ChatWorkspace` ([docs/skin-v20.md](https://github.com/ykurilov/agent-chat-sdk/blob/main/docs/skin-v20.md)). Скин v19 работает без правок. В Тандеме — `TandemChatClosedNotice`.
|
|
636
|
+
- Хранилища: колонка `closed` у чатов, миграция SQLite и PostgreSQL применяется сама. Своё хранилище может реализовать необязательный `setChatClosed(chatId, closed | null)` и отдавать `closed` в `getChat`; общий тест — `describeChatStorageContract(…, { chatClosure: "required" })`.
|
|
637
|
+
|
|
638
|
+
### Как обновить приложение
|
|
639
|
+
|
|
640
|
+
1. Найдите в коде приложения исчерпывающие `switch` по виду ошибки сервера чата, по `refusal.kind` из `onTurnRefused` и по `reason` в `turnNotStarted`. Искать: `kind: "turn_refused"`, `case "turn_refused"`, `case "access_refused"`, `HandlerErrorKind`, `AppRefusalKind`, `TurnNotStartedReason`, `case "busy"`.
|
|
641
|
+
2. Добавьте ветки для новых видов:
|
|
642
|
+
|
|
643
|
+
Было:
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
switch (refusal.kind) {
|
|
647
|
+
case "turn_refused": return showLessonClosed(refusal.message);
|
|
648
|
+
case "access_refused": return showNoAccess(refusal.message);
|
|
649
|
+
default: { const never: never = refusal.kind; return never; }
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Стало:
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
switch (refusal.kind) {
|
|
657
|
+
case "turn_refused": return showLessonClosed(refusal.message);
|
|
658
|
+
case "access_refused": return showNoAccess(refusal.message);
|
|
659
|
+
case "chat_closed":
|
|
660
|
+
case "action_forbidden": return; // чат уже показал текст сам
|
|
661
|
+
case "chat_creation_refused": return showToast(refusal.message);
|
|
662
|
+
default: { const never: never = refusal.kind; return never; }
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
В `switch` по `reason` в `turnNotStarted` добавьте `case "chat_closed":` рядом с `case "busy":`: резерв снимается так же.
|
|
667
|
+
|
|
668
|
+
3. Свой словарь `ChatLabels`, собранный целиком (не поверх `russianChatLabels`): добавьте `chatListClosed: "Закрыт"`.
|
|
669
|
+
4. Своё хранилище чата (не SQLite и не PostgreSQL из SDK): если нужен закрытый чат, реализуйте `setChatClosed` и отдавайте `closed` в `getChat`. Без этого `closeChat` бросает ошибку, а инструменты не получают `model.closeChat`.
|
|
670
|
+
5. Проверка: `pnpm typecheck` проходит, чат открывается и отвечает.
|
|
671
|
+
|
|
672
|
+
Заметка к 0.28: закрытие станет записью хода, и правка, «Переделать» и ветки в закрытом чате станут возможны. Чтобы сохранить нынешнее поведение, задайте права: `chatCapabilities: ({ chat }) => chat.closed ? { edit: false, regenerate: false, branches: false } : {}`.
|
|
673
|
+
- e0bb519: Маршруты OpenRouter и переход на запасного провайдера посреди ответа.
|
|
674
|
+
|
|
675
|
+
- **Маршруты OpenRouter.** `createOpenRouterProvider({ id, displayName, only, order, allowFallbacks, requireParameters })`.
|
|
676
|
+
Маршрут с `id` — отдельный провайдер каталога: например, `or-deepseek` только с хозяином `deepseek` и
|
|
677
|
+
`or-fireworks` только с `fireworks`. Он обслуживает те же модели, что `openrouter`: id моделей, цены, закреплённые
|
|
678
|
+
версии и отметка «видит картинку в результате инструмента» переходят сами. Своя запись модели для маршрута главнее.
|
|
679
|
+
Провайдер `openrouter` должен остаться в каталоге, иначе ошибка при сборке каталога. Маршруты попадают и в порядок
|
|
680
|
+
запасных провайдеров по умолчанию. С `order` сортировка `sort: "throughput"` по умолчанию больше не ставится; явный
|
|
681
|
+
`sort` остаётся. Один хозяин и в `only`, и в `ignore` — ошибка при создании; все хозяева `only` запрещены профилем
|
|
682
|
+
модели — понятная ошибка запроса.
|
|
683
|
+
- **Переход посреди ответа** (по желанию): `agent.providerFallback: { onSilence: "restart-step-on-next" }`. Модель
|
|
684
|
+
замолчала дольше `modelSilenceTimeout`, до первого куска или посреди ответа. Тогда шаг обрывается и начинается заново
|
|
685
|
+
у следующего провайдера списка `byModel`, без повтора у того же. Показанный текст шага стирается: клиент получает часть
|
|
686
|
+
`data-step-reset` («убрать всё от последней части `step-start`»). В сохранённом ответе и в запросе модели стёртого
|
|
687
|
+
нет. `useAgentChat` и мессенджеры убирают стёртое сами, и решения хука («Повторить», «Ответить заново») стёртого не
|
|
688
|
+
видят. Своя лента на сырых частях — `withoutResetSteps(parts)` из core или react. Ход с переходом сразу после `start`
|
|
689
|
+
присылает кусок `data-steps-may-reset` (только клиенту): мессенджер по нему держит части идущего шага в живом
|
|
690
|
+
сообщении до конца шага, а стёртый целиком текст заменяет фразой «Пишу ответ заново…».
|
|
691
|
+
Шаг, где инструмент уже получил вход целиком, не стирается: ответ продолжается у запасного.
|
|
692
|
+
`scope: "step"` — следующий шаг снова у основного провайдера (по умолчанию `"turn"`); только после перехода при
|
|
693
|
+
молчании, запасной по перегрузке остаётся до конца хода. Перегрузка у запасного ведёт к следующему за ним.
|
|
694
|
+
`maxSwitches` — переходов за ход, по умолчанию 1; каждый съедает шаг из предела шагов. Стёртый шаг оплачен и есть в
|
|
695
|
+
расходе хода и в записи после перезапуска сервера. Срок хода `turnTimeoutMs` идёт и во время перехода;
|
|
696
|
+
`maxOutputTokens` действует в каждом заходе; после начала `flush` новых заходов нет (`stopReason: "shutdown"`);
|
|
697
|
+
при «Стоп» или остановке службы перехода нет и показанное не стирается. В журнале сервера — строка
|
|
698
|
+
`[переход при молчании]`.
|
|
699
|
+
- **Остановка службы — для всех приложений.** С этой версии `flush` с первых мгновений ожидания идущих ответов
|
|
700
|
+
запрещает ходам новые заходы: к запасному провайдеру, автопродолжение, продолжение после сжатия. Ход, которому нужен новый
|
|
701
|
+
заход, кончается остановкой (`stopReason: "shutdown"`), даже без `onSilence`. Раньше такой ход мог начать новый
|
|
702
|
+
заход посреди остановки сервера.
|
|
703
|
+
|
|
704
|
+
### Как обновить приложение
|
|
705
|
+
|
|
706
|
+
Ничего делать не нужно: без `onSilence` всё работает как раньше. Включить переход — `agent.providerFallback:
|
|
707
|
+
{ byModel: { "deepseek-4.1-flash": ["or-fireworks"] }, onSilence: "restart-step-on-next" }` и маршруты в
|
|
708
|
+
`extendCatalog(..., { addProviders: [...] })`. Если лента строится на сохранённых или сырых частях без `useAgentChat` —
|
|
709
|
+
пропускайте их через `withoutResetSteps` (есть в core и в react). Если маршрут OpenRouter задаёт `order` и рассчитывал
|
|
710
|
+
на сортировку по скорости, задайте `sort: "throughput"` явно. Проверка: `pnpm typecheck`.
|
|
711
|
+
|
|
712
|
+
## 0.26.2
|
|
713
|
+
|
|
714
|
+
No changes in this release.
|
|
715
|
+
|
|
716
|
+
## 0.26.1
|
|
717
|
+
|
|
718
|
+
### Patch Changes
|
|
719
|
+
|
|
720
|
+
- 5b52515: Обрыв отправки больше не теряет сообщение. Раньше, если шлюз отвечал 502 (приложение за ним перезапускалось) или рвалась
|
|
721
|
+
связь, сообщение человека пропадало из ленты, а «Повторить» ничего не отправляла. Причина: после сбоя открывалась подписка
|
|
722
|
+
на события чата (`/watch`) и перечитывала историю сервера, в которой этого сообщения нет, — лента заменялась ею.
|
|
723
|
+
|
|
724
|
+
Теперь не дошедшее сообщение остаётся в ленте при любом перечитывании истории, а «Повторить» отправляет его заново с тем
|
|
725
|
+
же id. Сервер уже записал его, а ответ просто не дошёл — вкладка подключается к ходу или показывает готовый ответ, без
|
|
726
|
+
повтора. Повтор того же id, который сервер всё-таки записал, получает `409 invalid`; вкладка показывает историю сервера, в
|
|
727
|
+
ленте одно сообщение и один ответ. Человек вместо «Повторить» написал новое — не дошедшее больше не возвращается в ленту.
|
|
728
|
+
Если на сервере в этот момент идёт другой ход (фон, другая вкладка), текст не дошедшего возвращается в поле ввода.
|
|
729
|
+
|
|
730
|
+
Отказ 4xx со своим текстом работает как раньше: текст сервера виден, сообщение возвращается в поле.
|
|
731
|
+
|
|
732
|
+
**Что сделать в приложении:** ничего, достаточно обновить `@ykurilov/agent-chat-react`. Если в приложении есть свой обход
|
|
733
|
+
этой ошибки (например, браузерный тест, который ждёт пустую ленту после 502), его стоит перевернуть: после 502 сообщение
|
|
734
|
+
в ленте одно, «Повторить» отправляет его и даёт один ответ.
|
|
735
|
+
|
|
736
|
+
## 0.26.0
|
|
737
|
+
|
|
738
|
+
No changes in this release.
|
|
739
|
+
|
|
740
|
+
## 0.25.0
|
|
741
|
+
|
|
742
|
+
No changes in this release.
|
|
743
|
+
|
|
744
|
+
## 0.24.0
|
|
745
|
+
|
|
746
|
+
### Minor Changes
|
|
747
|
+
|
|
748
|
+
- 9b895cd: Ломает приложения: только исчерпывающие `switch` по видам ошибок сервера и событиям чата (новые значения) и код, который
|
|
749
|
+
сам читает ответ `resolveUser`. Без новых настроек поведение прежнее.
|
|
750
|
+
|
|
751
|
+
Агент внутри продукта: агент работает с объектами приложения — уроком, заказом, документом. Всё по желанию; без новых
|
|
752
|
+
настроек запрос к модели не меняется ни на байт. Подробно — `docs/in-product-agent.md`.
|
|
753
|
+
|
|
754
|
+
- **Проверка перед ходом.** `beforeTurn({ userId, chat, action, text? })` отказывает своими словами: 403 `turn_refused`
|
|
755
|
+
до любых записей и лимитов, на отправке, правке, повторе, переделке, подтверждении, виджете, служебных сообщениях, фоновой
|
|
756
|
+
задаче и в мессенджерах. `resolveUser` может вернуть `{ refused: { code, message, status? } }` → `access_refused` на всех
|
|
757
|
+
адресах чата, голоса, голосового режима (WebSocket 4401) и мессенджеров. React: `onTurnRefused`, текст отказа вместо «нет
|
|
758
|
+
связи», сообщение возвращается в поле. Ручная проверка — `service.assertTurnAllowed`.
|
|
759
|
+
- **Итог хода.** `afterTurn(turn)` после каждого хода с повторами и сроком (`afterTurnAttempts`, `afterTurnTimeoutMs`),
|
|
760
|
+
переживает перезапуск сервера. `TurnResult`: владелец и автор хода, текст, части, расход, действие, канал, объект,
|
|
761
|
+
сотрудник. События приложения `{ type, data? }` — из хука, из инструмента (`model.emitAppEvent`) и вне хода
|
|
762
|
+
(`service.publishAppEvent`); в React `appEvents` и `onAppEvent`, события переживают перезагрузку. Модели не уходят.
|
|
763
|
+
- **Чат объекта.** Ключ и данные объекта у чата, один чат на ключ у человека: `service.openSubjectChat`, на сервере
|
|
764
|
+
`subjectChats` (`POST …/subject`), в React `<AgentChat subject="lesson:42" />`. Данные объекта видят `beforeTurn`,
|
|
765
|
+
инструменты (`model.subject`) и `afterTurn`; в браузер и модели не уходят.
|
|
766
|
+
- **Приветствие.** `greeting: { text }` — первое сообщение от агента без вызова модели. Проверено на всех моделях каталога.
|
|
767
|
+
- **Лимит сообщений.** Окно лимитов с `limitMessages` вместо `limitUsd`, `exemptUsers` — люди без лимитов,
|
|
768
|
+
`onMessageThreshold` — пороги. Отказ — 429 с `unit: "messages"`.
|
|
769
|
+
- **Импорт старой переписки.** `service.importChat({ ownerId, chat, messages })` с датами и картинками
|
|
770
|
+
(`attachments.importFile`), без хода, памяти, расхода и лимитов.
|
|
771
|
+
- **Сотрудник проверяет чат человека.** Песочница с опцией `people`: админ видит чаты человека только на чтение и
|
|
772
|
+
проверяет ответы в копии у себя. Память человека в копии только читается, расход — на админа, человек ничего не видит.
|
|
773
|
+
Инструменты приложения получают `model.staff` и в копии в данные человека не пишут.
|
|
774
|
+
- **Свои кнопки у поля ввода.** `composerActions` с помощником `{ value, setValue, send, addFiles, busy, blocked, canSend }`.
|
|
775
|
+
Договор скина v19: `Composer.actions`; скин v18 работает без кнопок, Тандем готов.
|
|
776
|
+
- **Хранилища.** Миграции применяются сами: SQLite `0015`, PostgreSQL `0014` (ключ объекта, состояние хука итога).
|
|
777
|
+
|
|
778
|
+
### Что может сломаться
|
|
779
|
+
|
|
780
|
+
- **Исчерпывающие `switch` по закрытым спискам.** Новые значения: вид ошибки сервера `turn_refused` и `access_refused`
|
|
781
|
+
(`HandlerErrorKind`), события чата `after-turn-failed` и `app-event` (`ChatEvent`), `unit` у отказа лимита. Добавьте ветки
|
|
782
|
+
и надписи.
|
|
783
|
+
- **Список чатов без чатов объектов.** `listChats` и `GET {basePath}` по умолчанию не показывают чаты с ключом объекта.
|
|
784
|
+
Касается только приложений, которые начнут ими пользоваться; показать — `chatList: { subjectChats: true }` или
|
|
785
|
+
`listChats(userId, { subjects: "include" })`.
|
|
786
|
+
- **Свой `resolveUser` в обёртках.** Тип ответа — `ResolvedUser` (`{ id } | { refused } | null`). Код, который сам вызывает
|
|
787
|
+
`resolveUser` и читает `.id`, сначала проверяет `"refused" in user`.
|
|
788
|
+
- **Своё хранилище.** Для новых возможностей нужны необязательные методы (`findChatBySubject`, `importChat`,
|
|
789
|
+
`setAfterTurnState`, `listPendingAfterTurn`); без них эти возможности при включении дают ошибку при старте, остальное
|
|
790
|
+
работает. `listChats(ownerId, options?)` получил второй параметр — чаты с ключом скрывать по умолчанию.
|
|
791
|
+
- **Свой `ChatService`** (заглушки в тестах) добавляет новые методы: `openSubjectChat`, `importChat`, `assertTurnAllowed`, `peekHistory`,
|
|
792
|
+
`publishAppEvent`.
|
|
793
|
+
- **Своя реализация `SpendingLimits`** может добавить `takeMessage` — без него сообщения не считаются.
|
|
794
|
+
- **`AgentChat`: `chatId` стал необязательным** — нужен ровно один из `chatId` и `subject`; ни одного или оба — ошибка при
|
|
795
|
+
рендере. При смене `chatId` чат пересоздаётся: черновик и состояние прошлого чата не переносятся.
|
|
796
|
+
- **Окна лимитов: `limitUsd` стал необязательным** (у окна сообщений его нет). Код, который читает свои окна, проверяет
|
|
797
|
+
`limitMessages`.
|
|
798
|
+
- **Отказ сервера в React** (`ServerRefusal`) получил `kind` и `code`. 401 со словами приложения (`access_refused`) — это
|
|
799
|
+
отказ, а не «вход истёк».
|
|
800
|
+
- **«Пустой чат»** для «Нового чата»: чат объекта пустым не считается, обычный чат с одним приветствием — считается.
|
|
801
|
+
- Новый подпуть `@ykurilov/agent-chat-core/access` — помощники отказа в доступе для своих обработчиков.
|
|
802
|
+
|
|
803
|
+
### Как обновить приложение
|
|
804
|
+
|
|
805
|
+
1. `pnpm up "@ykurilov/agent-chat-*@^0.24.0"`. Миграции базы применяются сами при открытии хранилища.
|
|
806
|
+
2. Исчерпывающие `switch` по `HandlerErrorKind` (сервер) и `ChatEvent` (`chatEvents` ядра): найдите их поиском и добавьте
|
|
807
|
+
ветки `turn_refused`, `access_refused`, `after-turn-failed`, `app-event`. Обязательных надписей не прибавилось: текст
|
|
808
|
+
отказа приходит с сервера. Свой словарь для лимита сообщений — необязательные `spendingMessagesLeft`,
|
|
809
|
+
`spendingMessagesExhausted`, `editMessageLimit`.
|
|
810
|
+
|
|
811
|
+
Было: `switch (event.type) { case "tasks-changed": … case "notice-added": … default: assertNever(event) }`
|
|
812
|
+
Стало: те же ветки плюс `case "after-turn-failed": case "app-event": break;`
|
|
813
|
+
3. Свой код, который сам зовёт `resolveUser` и читает `.id`: сначала `if (user && "refused" in user) …`.
|
|
814
|
+
4. Ничего больше не нужно, если новые возможности не включать. Чтобы включить — `docs/in-product-agent.md`.
|
|
815
|
+
5. Свой скин: кнопки у поля появятся, когда `Composer` начнёт рисовать `actions` (договор v19, `docs/skin-v19.md`).
|
|
816
|
+
6. Проверка: `pnpm typecheck` без ошибок, чат открывается и отвечает как раньше.
|
|
817
|
+
|
|
818
|
+
## 0.23.0
|
|
819
|
+
|
|
820
|
+
### Minor Changes
|
|
821
|
+
|
|
822
|
+
- 30fb729: Живая надпись «думает»: пока модель думает и текста ответа нет, фраза сменяется каждые 3–4 с, рядом идёт время.
|
|
823
|
+
|
|
824
|
+
- Фразы не повторяются подряд. Набор по умолчанию — десять спокойных русских фраз («Прикидываю…», «Раскладываю по полочкам…», «Сверяю детали…»). Время: «12 с», после минуты — «1 мин 5 с».
|
|
825
|
+
- Новые необязательные ключи словаря: `thinkingPhrases` (свой набор фраз) и `thinkingElapsed` (запись времени). Пустой набор — одна фраза `reasoningRunning`, время остаётся. Словарь со своей `reasoningRunning` без `thinkingPhrases` показывает только её, а время без `thinkingElapsed` — цифрами («0:12»).
|
|
826
|
+
- Строка ожидания и блок рассуждений одного ответа идут по одним часам: счёт не сбрасывается, когда начинаются рассуждения или когда ответ прокрутили из вида и вернули. После перезагрузки страницы счёт начинается заново.
|
|
827
|
+
- Экранный диктор слышит одну постоянную `reasoningRunning`, а не каждую фразу. Смена фразы плавная только без prefers-reduced-motion.
|
|
828
|
+
- Новый хук `useThinkingStatus(active?)` → `{ phrase, seconds, elapsed }` для своего скина. Договор скина не менялся.
|
|
829
|
+
|
|
830
|
+
### Как обновить приложение
|
|
831
|
+
|
|
832
|
+
1. `pnpm up "@ykurilov/agent-chat-*@^0.23.0"`. Со скином Тандема больше ничего не нужно.
|
|
833
|
+
2. Свои фразы — в словаре: `<ChatLabelsProvider labels={{ ...russianLabels, thinkingPhrases: ["Собираю экран…", "Подключаю базу…"] }}>`.
|
|
834
|
+
3. Свой скин: в `Reasoning` (пока `streaming`) и в строке ожидания `AgentMessage` взять `const { phrase, elapsed } = useThinkingStatus()`; фразу и время пометить `aria-hidden`, а для диктора оставить постоянную `labels.reasoningRunning`.
|
|
835
|
+
4. Тесты приложения, которые искали на экране текст «Думает…», искать по роли `status` или по имени кнопки рассуждений — видимый текст теперь меняется.
|
|
836
|
+
|
|
837
|
+
## 0.22.0
|
|
838
|
+
|
|
839
|
+
No changes in this release.
|
|
840
|
+
|
|
841
|
+
## 0.21.0
|
|
842
|
+
|
|
843
|
+
No changes in this release.
|
|
844
|
+
|
|
845
|
+
## 0.20.0
|
|
846
|
+
|
|
847
|
+
### Minor Changes
|
|
848
|
+
|
|
849
|
+
- fd16b01: Вкладка показывает помощников, фоновые задачи и служебные сообщения. Договор скина `ChatSkin` поднялся с версии 17 до 18: добавились только необязательные `HelperCallRow`, `TaskCard`, `AgentNoticeView` и пометка мессенджера в списке чатов (`ChatListItem.origin`). Скин версии 17 работает без правок: помощник показывается обычной строкой инструмента, задачи не рисуются.
|
|
850
|
+
|
|
851
|
+
Скин Тандем уже умеет всё это. Вкладка сама обновляет карточку задачи и показывает итог без перезагрузки; ответ на подтверждение не затирается.
|
|
852
|
+
|
|
853
|
+
Что сделать в приложении со своим скином: ничего обязательного. Чтобы показать новое, реализуйте три компонента (описание: `docs/skin-v18.md`).
|
|
854
|
+
|
|
855
|
+
## 0.19.0
|
|
856
|
+
|
|
857
|
+
### Minor Changes
|
|
858
|
+
|
|
859
|
+
- aa0fd51: Гибкие лимиты расходов. Вместо одного бюджета на месяц можно задать любой набор окон сразу: сутки, неделю или месяц по
|
|
860
|
+
часовому поясу (со своим днём начала недели), «N часов от первого запроса» или на всё время. Область окна — человек, чат или
|
|
861
|
+
весь сервис. Ход начинается, только если не исчерпано ни одно окно.
|
|
862
|
+
|
|
863
|
+
- `createSpendingLimits({ windows: [...] })` вместо `budget` и `period`. Прежний `budget` работает как раньше (и получил
|
|
864
|
+
`period: "week"`).
|
|
865
|
+
- `carryOverrun: true` у окна: перерасход начатого хода переносится в начало следующего окна.
|
|
866
|
+
- `turn: { minRemainingUsd, maxCostUsd, maxToolCalls, overrun }` — запас на старт хода и потолки одного хода. На потолке
|
|
867
|
+
ход заканчивается итогом без новых вызовов инструментов.
|
|
868
|
+
- Человеку: свой лимит окна, блокировка (`setUserSettings`, `getUserSettings`) и ручной сброс окна (`resetWindow`).
|
|
869
|
+
- `onThreshold(event)` — событие при 80% и 100% лимита окна (пороги — `thresholds`), одно на порог в окне, проверка после
|
|
870
|
+
каждой записи расхода.
|
|
871
|
+
- `getSpending` отдаёт доллары по каждому окну (`windows`) и `blocked`.
|
|
872
|
+
- Сервер: в ответе 429 `spending_limit_reached` появились `reason` (`limit` | `blocked` | `reserve`), `windowId` и `hours`.
|
|
873
|
+
`GET …/spending` отдаёт `reason`, `windowId`, `hours`, `blocked` и список окон `windows` — только доли и время сброса,
|
|
874
|
+
без долларов.
|
|
875
|
+
- Браузер: надписи «на неделю», «на 5 ч» (с временем обновления), «сервиса», «Лимита … не хватит на новый ответ», «Новые
|
|
876
|
+
ответы для вас приостановлены». В `SpendingInfo` новые необязательные поля `hours`, `reason`, `windowId`, `windows`.
|
|
877
|
+
|
|
878
|
+
Скользящим окнам, личным настройкам, сбросу и порогам нужны методы состояния хранилища: в памяти, SQLite и PostgreSQL они есть
|
|
879
|
+
(запись `storage-limit-state`). Подробно — README пакета `@ykurilov/agent-chat-limits`, раздел «Окна лимитов».
|
|
880
|
+
|
|
881
|
+
**Ломает приложения:** только исчерпывающие проверки и свои словари по периоду. У `SpendingScope` появилось значение
|
|
882
|
+
`service`, у `SpendingPeriod` — `week` и `rolling` (в `@ykurilov/agent-chat-core` и `@ykurilov/agent-chat-react`).
|
|
883
|
+
|
|
884
|
+
### Как обновить приложение
|
|
885
|
+
|
|
886
|
+
1. Поищите в приложении `SpendingPeriod`, `SpendingScope`, `info.period`, `info.scope` и `spendingWarning` / `spendingExhausted`
|
|
887
|
+
в своём словаре надписей. Если надпись строится по таблице периодов, добавьте новые значения:
|
|
888
|
+
|
|
889
|
+
Было:
|
|
890
|
+
|
|
891
|
+
```ts
|
|
892
|
+
const periodPhrase = { month: " на месяц", day: " на день", none: "" };
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
Стало:
|
|
896
|
+
|
|
897
|
+
```ts
|
|
898
|
+
const periodPhrase = { month: " на месяц", week: " на неделю", day: " на день", none: "" };
|
|
899
|
+
const phrase = info.period === "rolling" ? ` на ${info.hours} ч` : periodPhrase[info.period];
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
То же для области: `info.scope === "service"` — лимит всего сервиса.
|
|
903
|
+
2. Свой словарь с `spendingExhausted`: учтите `info.reason` — `"blocked"` (человек заблокирован, `resetsAt` нет) и `"reserve"`
|
|
904
|
+
(остаток меньше запаса на старт хода).
|
|
905
|
+
3. Свой разбор ответа 429 или `GET …/spending` на сервере или в браузере: новые поля необязательные, старые на месте.
|
|
906
|
+
4. Ничего из этого нет в приложении — обновление ничего не требует.
|
|
907
|
+
5. Проверка: `pnpm typecheck` проходит; с исчерпанным лимитом чат показывает надпись и не даёт отправить сообщение.
|
|
908
|
+
- 6f26b93: Свои кнопки под ответами агента и событие «ход закончен» на экране.
|
|
909
|
+
|
|
910
|
+
- `messageActions` у `AgentChat`, `ChatWorkspace` и `AgentPanel`: список кнопок приложения `{ id, label, icon?, visible?, onRun }`.
|
|
911
|
+
Иконка — готовый элемент приложения (`<Heart />`). `visible({ message, status, last })` решает, показывать ли кнопку
|
|
912
|
+
у ответа; без него — у каждого законченного. `onRun({ chatId, messageId, message, text })`; пока его обещание не
|
|
913
|
+
выполнено, кнопка «занята», ошибка уходит в консоль, а не в чат. Пока ответ пишется, кнопок у него нет. Массив держите
|
|
914
|
+
постоянным (вне компонента или в `useMemo`).
|
|
915
|
+
- `onTurnFinished({ chatId, messageId, status })` у тех же компонентов: один раз на ход, когда ответ получил итог,
|
|
916
|
+
включая остановку и ошибку. Открытие чата с готовой историей события не даёт.
|
|
917
|
+
- Договор скина — версия 17: необязательное `AgentMessage.actions` (`MessageActionView`). Скин версии 16 работает без
|
|
918
|
+
правок, только своих кнопок в нём не будет. Без новых свойств всё как раньше.
|
|
919
|
+
|
|
920
|
+
## 0.18.6
|
|
921
|
+
|
|
922
|
+
No changes in this release.
|
|
923
|
+
|
|
924
|
+
## 0.18.5
|
|
925
|
+
|
|
926
|
+
### Patch Changes
|
|
927
|
+
|
|
928
|
+
- 56b9ab6: Лента едет за ответом, даже если сообщение отправлено сразу после открытия чата или сразу после конца прошлого ответа. Раньше лента в эти первые мгновения ещё «доводилась» к прежнему концу. Высокое сообщение, например вставленный длинный текст, уводило низ от этого конца, и новый ответ уходил под поле ввода на сотни пикселей. Приложению ничего делать не нужно.
|
|
929
|
+
|
|
930
|
+
## 0.18.4
|
|
931
|
+
|
|
932
|
+
No changes in this release.
|
|
933
|
+
|
|
934
|
+
## 0.18.3
|
|
935
|
+
|
|
936
|
+
### Patch Changes
|
|
937
|
+
|
|
938
|
+
- ce3c26f: Если сервер отказал ходу ответом 4xx со своим текстом (`{ error: "Разговор завершён" }` или `{ error: { message } }`),
|
|
939
|
+
чат показывает этот текст вместо «Что-то пошло не так». Когда повтор не поможет (все 4xx, кроме 408, 425 и 429),
|
|
940
|
+
кнопки «Повторить» нет, а отправленный текст возвращается в поле. Касается отправки, «Повторить», «Переделать»
|
|
941
|
+
и правки сообщения: у правки текст сервера появляется в редакторе. Отказы без текста, ошибки 5xx и обрыв связи
|
|
942
|
+
выглядят как раньше. Скин получает текст в новом необязательном `ErrorNotice.text` (договор скина 16), Тандем
|
|
943
|
+
его показывает; старые скины работают без правок.
|
|
944
|
+
|
|
945
|
+
## 0.18.2
|
|
946
|
+
|
|
947
|
+
### Patch Changes
|
|
948
|
+
|
|
949
|
+
- 7e7e966: Лента снова держит низ, если человек печатает в поле ввода. Раньше пробел, стрелки, Home и End, нажатые в поле, лента принимала за свою прокрутку. После отправки длинный ответ мог вырасти под полем ввода, а лента стояла на месте. Теперь клавиши из полей ввода (`textarea`, `input`, `select`, редактируемые блоки) ленту не трогают. Клавиши на самой ленте работают как раньше. Скину ничего делать не нужно.
|
|
950
|
+
|
|
951
|
+
## 0.18.1
|
|
952
|
+
|
|
953
|
+
### Patch Changes
|
|
954
|
+
|
|
955
|
+
- 179ad9d: У оборванного ответа с вызовами инструментов (ошибка, «Стоп», сбой сервера) рядом с «Повторить» появилась кнопка «Переделать ответ». «Повторить» по-прежнему продолжает ход с уже сделанных вызовов, чтобы не выполнить их второй раз. «Переделать ответ» пишет ответ с чистого листа от того же вопроса: прежние вызовы не переносятся ни в новый вариант, ни в запрос к модели. Оборванный вариант остаётся соседним. Скину ничего делать не нужно: кнопка приходит через прежнее свойство `onRegenerate`.
|
|
956
|
+
|
|
957
|
+
## 0.18.0
|
|
958
|
+
|
|
959
|
+
### Minor Changes
|
|
960
|
+
|
|
961
|
+
- 43d7ebe: Песочница агента для админа: новый пакет `@ykurilov/agent-chat-sandbox`. Админ говорит с боевым агентом и видит журнал:
|
|
962
|
+
каждое обращение к модели (что ушло, что пришло, токены, кэш, цена) и каждый вызов инструмента. Любое сообщение можно
|
|
963
|
+
поправить (текст и результаты обычных инструментов), правка создаёт ветку. Модель и системная инструкция меняются для одного
|
|
964
|
+
чата, боевые не трогаются. Память в песочнице только на чтение.
|
|
965
|
+
|
|
966
|
+
- сервер: `createSandboxHandler({ chat, isAdmin, journal })`, журнал `createSqliteSandboxJournal` (`/sqlite`) или
|
|
967
|
+
`createPostgresSandboxJournal` (`/postgres`, схема `agent_chat`), хранится 30 дней; не админ получает 404 на любом адресе;
|
|
968
|
+
- страница: `AgentSandbox` из `@ykurilov/agent-chat-sandbox/react`, готовый скин `tandemSandboxSkin`;
|
|
969
|
+
- `createChatServer` — новый экспорт server (на нём стоят и боевой, и песочный обработчик);
|
|
970
|
+
- core: `turnObserver`, `chatInstructions`, `lockedToolOutputs`, `editAnswer`; боевой запрос к модели остаётся прежним побайтно;
|
|
971
|
+
- договор скина чата версии 15: необязательные `AgentMessage.onEdit/edited` и `ToolCallRow.outputEdited`; новые надписи
|
|
972
|
+
`answerEdited`, `toolOutputEdited`, `editAnswer`; договор скина песочницы `SandboxSkin` версии 1.
|
|
973
|
+
|
|
974
|
+
Подробно: `docs/sandbox.md`.
|
|
975
|
+
|
|
976
|
+
### Как обновить приложение
|
|
977
|
+
|
|
978
|
+
Обычному приложению делать ничего не нужно: песочница включается отдельно, миграций боевой базы нет (таблицы `sandbox_*` создаёт
|
|
979
|
+
сам журнал).
|
|
980
|
+
|
|
981
|
+
1. Полный словарь надписей, написанный вручную без `{ ...russianLabels }`, — добавьте `answerEdited`, `toolOutputEdited`, `editAnswer`.
|
|
982
|
+
2. Свой скин версии 14 работает без правок; чтобы показать «изменено» и кнопку правки, реализуйте необязательные части версии 15.
|
|
983
|
+
3. Своя реализация сервера поверх `createChatHandler` продолжает работать; для песочницы возьмите `createChatServer`.
|
|
984
|
+
4. Проверка: `pnpm typecheck`, затем в чате отправить сообщение и получить ответ.
|
|
985
|
+
|
|
986
|
+
## 0.17.0
|
|
987
|
+
|
|
988
|
+
### Minor Changes
|
|
989
|
+
|
|
990
|
+
- e5d0860: Видео от агента: новый пакет `@ykurilov/agent-chat-videos`. Агент находит видео в каталоге приложения (`find_videos`) и
|
|
991
|
+
присылает карточкой (`show_video`), можно с нужной минуты. По нажатию плеер YouTube (`youtube-nocookie.com`) или Kinescope
|
|
992
|
+
играет в ленте, есть «На весь экран». До нажатия к площадкам не уходит ни одного запроса.
|
|
993
|
+
|
|
994
|
+
- сервер: `createVideoLibrary({ videos })` или `({ source: { search, get } })`, `videos.tools(model)` в `agent.tools`;
|
|
995
|
+
- страница: `videos={videosClient}` у `AgentChat`, `ChatWorkspace` и `AgentPanel`;
|
|
996
|
+
- договор скина версии 14: необязательная часть `AgentVideo` (`AgentVideoView`); скин без неё показывает видео ссылкой;
|
|
997
|
+
- надписи `videos.play`, `videos.from`, `videos.fullscreen` и `voice.videoOnScreen` («видео на экране» в голосовом режиме).
|
|
998
|
+
|
|
999
|
+
Подробно: `docs/videos.md`.
|
|
1000
|
+
|
|
1001
|
+
### Как обновить приложение
|
|
1002
|
+
|
|
1003
|
+
Обычному приложению делать ничего не нужно: видео включается отдельно, миграций нет.
|
|
1004
|
+
|
|
1005
|
+
1. Полный словарь надписей, написанный вручную без `{ ...russianLabels }`, — добавьте `voice.videoOnScreen`.
|
|
1006
|
+
2. Проверка: `pnpm typecheck`, затем в чате отправить сообщение и получить ответ.
|
|
1007
|
+
|
|
1008
|
+
## 0.16.0
|
|
1009
|
+
|
|
1010
|
+
### Minor Changes
|
|
1011
|
+
|
|
1012
|
+
- cf52f10: **Ломает только исчерпывающие проверки:** у `UsageSource` появилось значение `images`, у видов ошибок сервера — `widget_not_pending` и `invalid_answer`, у `ChatService` — метод `answerWidget`. Своё хранилище должно поддержать условную запись `AwaitingMessagePatch.expectedParts` (при несовпадении частей — `MessageNotAwaitingError`; помощник `sameMessageParts` из core), иначе две копии сервера могут затереть ответы друг друга; общие тесты хранилищ это проверяют. Остальное необязательно, приложение без правок работает как раньше.
|
|
1013
|
+
|
|
1014
|
+
Что нового: агент может спросить кнопками, звёздами или формой (виджеты), нарисовать схему блоком ` ```mermaid ` и создать картинки через Runware. Всё включается отдельно:
|
|
1015
|
+
|
|
1016
|
+
- виджеты: `agent: { widgets: { basic: true, custom: [свойВиджет] } }`, свой виджет — `defineWidget` из core и компонент в `widgets={{ … }}` у `AgentChat`; ответ человека идёт на `POST /:chatId/widgets/:toolCallId`, ход ждёт со статусом `awaiting_approval`;
|
|
1017
|
+
- схемы: новый пакет `@ykurilov/agent-chat-diagrams`, `agent: { diagrams: true }` на сервере и `diagrams={diagramsClient}` на странице;
|
|
1018
|
+
- картинки: новый пакет `@ykurilov/agent-chat-images` (нужен пакет вложений и `RUNWARE_API_KEY`), `images.tools(model)` в `agent.tools`, `images={imagesClient}` на странице; свой срок ожидания (210 с) инструмент сообщает ядру сам (`declareToolTimeout`), расход бронируется до заказа (`assertWithinLimit({ pendingUsd })`);
|
|
1019
|
+
- окно просмотра картинок и схем: Тандем готов, своему скину — необязательные части договора версии 13 (`Widget`, `SuggestedReplies`, `GeneratedImages`, `MediaViewer`, `diagramColors`, `renderDiagram`). `splitMermaidBlocks` экспортируется из react;
|
|
1020
|
+
- вложения: `Attachments.storage` и `detectFileKind` экспортируются из пакета вложений;
|
|
1021
|
+
- голосовой режим произносит вопрос и варианты ждущего виджета и говорит «схема на экране» вместо блока схемы;
|
|
1022
|
+
- «Стоп» на экране закрывает висящие вызовы так же, как сервер, разрешённые без результата — «результат неизвестен» (`stoppedToolCallReasons.resultUnknown`);
|
|
1023
|
+
- упавший свой виджет страницы заменяется запасным видом скина, лента цела.
|
|
1024
|
+
|
|
1025
|
+
Подробно: `docs/widgets.md`, `docs/diagrams.md`, `docs/images.md`, `docs/media-viewer.md`.
|
|
1026
|
+
|
|
1027
|
+
### Как обновить приложение
|
|
1028
|
+
|
|
1029
|
+
Обычному приложению на SQLite или PostgreSQL из набора делать ничего не нужно: миграций нет, всё новое включается отдельно.
|
|
1030
|
+
|
|
1031
|
+
1. Исчерпывающий `switch` по `UsageSource` — добавьте ветку `"images"`; по видам ошибок сервера — `"widget_not_pending"` и `"invalid_answer"`.
|
|
1032
|
+
2. Своя реализация `ChatService` — добавьте метод `answerWidget({ chatId, ownerId, toolCallId, answer, page }): Promise<{ resumed: boolean }>` (тип `WidgetAnswerRequest` из core, образец — служба из core).
|
|
1033
|
+
3. Своё хранилище: `updateAwaitingMessage` получает необязательное поле `expectedParts`. Если оно есть и части сообщения в базе с ним не совпадают (`sameMessageParts` из core), бросайте `MessageNotAwaitingError`; проверку и запись делайте в одной транзакции. Прогоните общие тесты хранилищ из core.
|
|
1034
|
+
4. Своя обёртка `assertWithinLimit` из limits — пробрасывайте новое необязательное поле `pendingUsd`.
|
|
1035
|
+
5. Проверка: `pnpm typecheck`, затем в чате отправить сообщение и получить ответ.
|
|
1036
|
+
|
|
1037
|
+
## 0.15.0
|
|
1038
|
+
|
|
1039
|
+
### Minor Changes
|
|
1040
|
+
|
|
1041
|
+
- f05ad23: Часовой пояс для лимитов расходов: `createSpendingLimits({ timeZone: "Europe/Moscow" })`. Сутки и месяцы режутся
|
|
1042
|
+
по полуночи этого пояса, а не по UTC (раньше московский суточный лимит обнулялся в 03:00). Переход на летнее время
|
|
1043
|
+
учтён: сутки бывают 23 и 25 часов. Для сегодняшней даты агента этот пояс только запасной: главнее пояс браузера
|
|
1044
|
+
человека, а пояс лимитов берётся, когда браузер свой пояс не прислал. Дата обновления лимита на экране — по поясу лимитов. Пояс не из базы IANA — ошибка при старте.
|
|
1045
|
+
|
|
1046
|
+
Новое: `timeZone` в настройках лимитов и необязательное поле `timeZone` у `SpendingLimits`, `SpendingStatus`,
|
|
1047
|
+
`SpendingLimitReachedError`, `SpendingReport` и `SpendingInfo` (экран); ответы `GET …/spending` и отказ 429 получают
|
|
1048
|
+
`timeZone`, если пояс задан. Ядро экспортирует `resolveTimeZone`.
|
|
1049
|
+
|
|
1050
|
+
### Как обновить приложение
|
|
1051
|
+
|
|
1052
|
+
Ничего делать не нужно: без `timeZone` лимиты по UTC, дата агента по поясу человека или сервера, как раньше.
|
|
1053
|
+
|
|
1054
|
+
Чтобы считать сутки по Москве, в файле, где создаются лимиты:
|
|
1055
|
+
|
|
1056
|
+
```ts
|
|
1057
|
+
const limits = createSpendingLimits({ storage, catalog, budget, period: "day", timeZone: "Europe/Moscow" });
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
`agent.timeZone` после этого задавать не нужно. Если он уже задан, он должен совпадать с поясом лимитов (другое имя того же пояса
|
|
1061
|
+
вроде `Asia/Calcutta` и `Asia/Kolkata` — совпадение), иначе
|
|
1062
|
+
служба чата не создастся (ошибка называет оба пояса). Свои надписи `spendingExhausted`, которые сами форматируют
|
|
1063
|
+
`resetsAt`, могут брать пояс из `info.timeZone`. Проверка: `limits.getSpending({ userId })` отдаёт `periodStart`
|
|
1064
|
+
вида `…T21:00:00.000Z` (полночь по Москве).
|
|
1065
|
+
|
|
1066
|
+
## 0.14.0
|
|
1067
|
+
|
|
1068
|
+
### Minor Changes
|
|
1069
|
+
|
|
1070
|
+
- 4f33e85: Браузерные инструменты: агент делает действие на странице человека — например, открывает карточку задачи. Приложение описывает их общим модулем через `tool({ description, inputSchema })` без `execute` и подключает на сервере `agent.browserTools`, а на странице — функции с теми же именами в `browserTools` у `AgentPanelProvider`. Когда модель зовёт такой инструмент, сервер шлёт временный кусок `data-browser-call` той вкладке, с которой пришёл ход, и ждёт ответа `POST {base}/:chatId/browser-tools/:toolCallId` (`{ tabId, output }` или `{ tabId, error }`, `204`; тело больше 64 КБ — `400 invalid`, дальше не читается). Результат уходит модели, ошибка — строкой «ошибка» с её текстом, ход продолжается. Срок ответа — `agent.browserToolTimeoutMs` (по умолчанию 30 с, от 1 с до 2³¹−1 мс), после него — «Страница не ответила за N с»; «стоп» и удаление чата снимают ожидание. Без вкладки — «Действие на экране недоступно: страница не подключена». Результат больше 16 КБ — «Слишком большой результат», ошибка обрезается до 1000 знаков, пустой результат — «готово». «Спросить разрешения» работает как у обычных инструментов: функция зовётся только после «Разрешить». Ответ чужой вкладки, повторный и поздний — `409 not_pending` (`BrowserCallNotPendingError` в ядре, новый вид `not_pending` у `HandlerErrorKind`). Описание с `execute` или имя, совпавшее с инструментом приложения или памяти, — ошибка при создании службы. Вкладка исполняет каждый вызов чата один раз, в том числе после перезагрузки. Ожидание живёт в памяти процесса: как и продолжение ответа, работает на одном сервере.
|
|
1071
|
+
|
|
1072
|
+
**Ломает приложения** только там, где сами реализуют интерфейс `ChatService` (например, поддельная служба в тестах): в него добавился метод `answerBrowserCall`.
|
|
1073
|
+
- 15fbbcc: Инструмент агента сообщает странице, что изменил данные. В ядре у `AgentToolsModel` новая функция `notifyDataChanged({ tag, details? })`: её зовут после записи, и сообщение уходит браузеру той вкладки, где идёт ответ, сразу, не дожидаясь конца вызова. Метка — 1–100 знаков, подробности — JSON до 4 КБ; неверная форма — `TypeError` внутри инструмента. После конца хода вызов ничего не делает и пишет в журнал сервера. В хранилище сообщение не попадает. В слое поведения `useAgentChat` с мостом передаёт такие сообщения приложению: на странице их ловят `useAgentDataChanges(tag, handler)` и `onDataChanged` провайдера, повтор одного и того же сообщения подписчикам не раздаётся, при закрытой панели сообщение тоже доходит.
|
|
1074
|
+
|
|
1075
|
+
**Ломает приложения** только там, где сами собирают `AgentToolsModel` (например, в тестах инструментов): в объект нужно добавить `notifyDataChanged`. Инструментам, которые читают `profile`, `chatId`, `ownerId` или `place`, ничего делать не нужно.
|
|
1076
|
+
- c09c1e2: Основа боковой панели агента. В ядре — типы `ScreenPlace` (где человек в приложении), `DataChange` (инструмент изменил данные), `TurnRequestContext` (с чем пришёл запрос хода) и имена временных кусков потока `data-app-change`, `data-browser-call`. В слое поведения — тип `AgentAppBridge` (связь чата с приложением), необязательные `appBridge` и `initialDraft` у `AgentChat` и `useAgentChat` (`initialDraft` кладёт текст в поле ввода при открытии чата), `appBridge` у `ChatWorkspace`, `rememberKey` у `useChatList` (свой ключ запомненного чата), новый вход `@ykurilov/agent-chat-react/panel` и договор скина версии 12 — отдельный `AgentPanelSkin` (`ChatSkin`, `ChatListSkin`, `MemorySkin` не менялись).
|
|
1077
|
+
|
|
1078
|
+
**Ломает приложения** только с полным словарём надписей `ChatLabels`, написанным вручную: в него добавились надписи `agentPanel…` (`agentPanelLauncher`, `agentPanelOpen`, `agentPanelClose`, `agentPanelLabel`, `agentPanelChats`, `agentPanelBackToChat`, `agentPanelBusy`). Тем, кто пишет `{ ...russianLabels, … }`, ничего делать не нужно. Без новых свойств чат работает как раньше.
|
|
1079
|
+
- 41992d0: Боковая панель агента во входе `@ykurilov/agent-chat-react/panel`. `AgentPanelProvider` ставится в корневой макет: панель, открытый чат и идущий ответ переживают переходы по страницам; открыта ли панель и какой чат открыт, помнится отдельно для каждого человека (`userKey`). `AgentPanel` рисует кнопку, рамку, список чатов поверх чата и сам чат через скин (`ChatSkin & ChatListSkin & AgentPanelSkin`). Хуки: `useAgentPanel` (открыть и закрыть, `openChat`, `openNewChat({ draft })`, `pageInset` для режима `push`, `busy`), `useAgentPlace` (где человек сейчас) и `useAgentDataChanges` (подписка на изменения данных по меткам). Браузерные инструменты — функции в `browserTools`; нет функции на странице — модель получает «Инструмент недоступен на этой странице». Новые типы `BrowserToolFunction`, `BrowserToolImplementations`; `AgentAppBridge.browserCall` принимает необязательный `{ signal }`. `useChatList` при смене `rememberKey` сбрасывает список и открытый чат прежнего ключа.
|
|
1080
|
+
|
|
1081
|
+
**Ломает приложения** только с полным словарём надписей `ChatLabels`, написанным вручную: добавилась надпись `agentPanelToolUnavailable`. Тем, кто пишет `{ ...russianLabels, … }`, ничего делать не нужно.
|
|
1082
|
+
- 062ce36: Человек, чат и место на экране для инструментов агента. Функция `agent.tools(model)` получает в `model` не только `ownerId` и `chatId`, но и `place` — где сейчас человек в приложении (`{ description, data? }` или `null`); функция зовётся раз на ход, поэтому два человека одновременно не путаются. Место уходит с каждым запросом хода (отправка, правка, повтор, переделка, ответ на подтверждение) полями `place` и `tabId` в теле, а модель видит его отдельной частью в самом конце последнего сообщения человека (после пометки режима и блока памяти). В историю чата и в сжатие место не пишется, инструкция и описания инструментов не меняются, поэтому кэш модели цел. Описание длиннее `agent.placeMaxLength` (по умолчанию 1000, не меньше 100) обрезается с «…», данные больше 4 КБ отбрасываются, обе обрезки пишутся в журнал сервера без текста. Неверная форма `place` или `tabId` — `400 invalid`, ничего не записано. В службе чата у `sendMessage` и `editMessage` в `options` появилось `page`, у `retry`, `regenerate` и `answerApproval` — последний аргумент `page` (`TurnRequestContext`). `useAgentChat` с `appBridge` кладёт `tabId` и текущее место в тела всех пяти запросов; без моста тела прежние. Старые приложения работают без изменений. **Ломает** только код, который сам собирает объект `AgentToolsModel` (например, тесты инструментов): в него добавилось обязательное поле `place` (`null`, если места нет); функции `tools(model)` приложений ничего менять не нужно.
|
|
1083
|
+
- 656233a: Долгая память агента. Агент сам запоминает устойчивое о человеке (предпочтения, правила общения, факты о работе) и помнит это в новых чатах. В новом пакете `@ykurilov/agent-chat-memory` лежат инструмент `manage_memory`, правила записей и блок памяти для запроса. Человек видит записи на экране памяти: может добавить, исправить, удалить запись или стереть всю память. Записи у каждого человека свои, удаление чата их не трогает. Раз в сутки после ответа та же модель предлагает слить повторные записи (опция `optimizer`); код проверяет группы и применяет годные, на экране появляется раздел «Объединено» с кнопкой «Вернуть как было». Отрицание не теряется («не любит кофе» не склеивается с «любит кофе»). После сбоев проверка ждёт дольше: час, 6 часов, сутки. Пределы записей и дубли держатся при одновременных действиях (замок на человека, а в SQLite и PostgreSQL ещё и проверка внутри транзакции), в том числе при возврате склейки. Память не меняет начало запроса к модели, поэтому кэш модели сохраняется.
|
|
1084
|
+
|
|
1085
|
+
**Ломает приложения** только с полным словарём надписей `ChatLabels`, написанным вручную (шаг 5): в него добавились надписи `memory…`. Остальным обновление ничего не ломает: без опции `memory` чат работает как раньше. Чтобы включить память, выполните шаги ниже. Новые адреса: `/api/chat/memory/merges` и `/api/chat/memory/merges/:runId/revert`. Миграции SQLite `0009` и `0010` и PostgreSQL `0008` и `0009` применяются сами при открытии базы (`0010` и `0009` — задел под склейку повторов записей).
|
|
1086
|
+
|
|
1087
|
+
### Как обновить приложение
|
|
1088
|
+
|
|
1089
|
+
1. **Включить память на сервере** (по желанию). Пакет ставится отдельно: `pnpm add @ykurilov/agent-chat-memory`. В файле, где создаётся `createChatHandler`:
|
|
1090
|
+
|
|
1091
|
+
Было:
|
|
1092
|
+
|
|
1093
|
+
```ts
|
|
1094
|
+
createChatHandler({ basePath: "/api/chat", storage, resolveUser /* … */ });
|
|
1095
|
+
```
|
|
1096
|
+
|
|
1097
|
+
Стало:
|
|
1098
|
+
|
|
1099
|
+
```ts
|
|
1100
|
+
import { createLongTermMemory } from "@ykurilov/agent-chat-memory";
|
|
1101
|
+
|
|
1102
|
+
createChatHandler({ basePath: "/api/chat", storage, resolveUser, memory: createLongTermMemory({ storage }) /* … */ });
|
|
1103
|
+
```
|
|
1104
|
+
|
|
1105
|
+
Инструкция агента и инструмент `manage_memory` добавляются сами: в `agent.instructions` и `agent.tools` ничего дописывать не нужно. Имя `manage_memory` занято: своего инструмента с таким именем быть не должно (иначе ошибка при создании). Адреса `/api/chat/memory` появляются сами. Без `memory` они отвечают `404`.
|
|
1106
|
+
2. **Подключить экран памяти** (по желанию). Найдите страницу, куда его поставить, и добавьте компонент; `createMemoryApi` вызывайте один раз (например, в `useMemo`):
|
|
1107
|
+
|
|
1108
|
+
```tsx
|
|
1109
|
+
import { AgentMemory, createMemoryApi } from "@ykurilov/agent-chat-react";
|
|
1110
|
+
import { tandemMemorySkin } from "@ykurilov/agent-chat-skin-tandem";
|
|
1111
|
+
|
|
1112
|
+
const memoryApi = useMemo(() => createMemoryApi({ api: "/api/chat" }), []);
|
|
1113
|
+
<AgentMemory api={memoryApi} skin={tandemMemorySkin} />
|
|
1114
|
+
```
|
|
1115
|
+
3. **Подпись вызова в ленте.** Было: `labels={russianLabels}`. Стало: `labels={withMemoryToolTitles(russianLabels)}`, где `withMemoryToolTitles` — из `@ykurilov/agent-chat-memory/labels`. Вместо `manage_memory` в ленте будет «Работа с памятью».
|
|
1116
|
+
4. **Свой скин (договор скина версии 11).** `ChatSkin` и `ChatListSkin` не менялись. Экрану памяти нужен отдельный `MemorySkin` из пяти компонентов (`MemoryScreen`, `MemoryGroup`, `MemoryNoteRow`, `MemoryNoteEditor`, `MemoryConfirm`). Скин Тандема даёт его готовым: `tandemMemorySkin`. Описание — `specs/017-long-term-memory/contracts/memory.md`, раздел 5.
|
|
1117
|
+
5. **Полный словарь надписей.** Если приложение задаёт объект `ChatLabels` целиком, а не `{ ...russianLabels, … }`, добавьте новые надписи: все ключи, которые начинаются с `memory` (`memoryHeading`, `memoryAdd`, `memoryClearAll` и другие, полный список — тип `ChatLabels` в `@ykurilov/agent-chat-react`). Без этого `pnpm typecheck` не пройдёт.
|
|
1118
|
+
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` проверят их сами.
|
|
1119
|
+
7. **Проверка.** `pnpm typecheck` и сборка проходят. Напишите агенту «Запомни, что я пишу на TypeScript», откройте экран памяти: там запись в разделе, который выбрал агент. В новом чате спросите, что он про вас знает.
|
|
1120
|
+
- be77ee2: Основа для надёжности хода агента (022). Новый вид ошибки `context_overflow` («Переписка не помещается в модель») у `ChatErrorKind` в ядре и в `react`, надпись `labels.errors.context_overflow` в русском словаре; Тандем показывает её сам. **Ломает:** исчерпывающий `switch` по виду ошибки и своя таблица надписей `labels.errors` без `context_overflow` перестанут проходить проверку типов — добавьте надпись. Ядро экспортирует `HANGING_TOOL_CALL_REASON` (текст для модели вместо результата прерванного вызова) и `cutAtCharBoundary(text, maxLength)` — обрезку без половинки эмодзи; веб-инструменты режут страницы и выдержки через неё, теперь эмодзи на границе не рвётся. Список перенесённых из pi-mono (MIT) признаков ошибок и лицензия — в `THIRD_PARTY_NOTICES.md`, он лежит и в пакете `core`.
|
|
1121
|
+
- 84ad64f: Укрепление по третьему проходу Pi (022, этап S9). **Большие фото уменьшаются при загрузке**: поворот по EXIF, не больше 2000×2000 пикселей и ~4,5 МБ в base64; хранится уменьшенная копия, старые картинки ужимаются при сборке запроса (результат запоминается), нечитаемая картинка и картинка больше 60 млн пикселей — отказ 422 `unreadable` (в старой истории — пометка «пропущена»). Новая зависимость `sharp` (готовые сборки для macOS и Linux, в том числе Alpine; порядок ужатия из pi-mono, MIT). **Секреты в ошибке инструмента** вычищаются до того, как текст уйдёт человеку, в базу и модели: пароль в адресе, `password=`/`password: …`/`token=`, `"secret": "…"`, заголовок `Authorization`, `X-Api-Key`, ключи сервисов, закрытые ключи PEM и значения секретных переменных окружения (кроме путей, адресов без пароля и простых слов); чистятся сообщение, стек и `cause`; `scrubSecrets` и `environmentSecrets` экспортированы из ядра. **Внешние картинки** в ответе модели и в сводке сжатия скин Тандема показывает ссылкой и не грузит (новая необязательная надпись словаря `externalImageLink`, по умолчанию «картинка»; свои словари без неё работают); инструкция веб-инструментов называет страницы данными, а не указаниями. **Ступени цены** по длине контекста: `pricing.tiers` (`inputTokensAbove` и цены), ход считается по шагам; у GPT 6 Luna выше 272 тысяч токенов. Пауза в `useAgentChat` больше не копит слушатели отмены.
|
|
1122
|
+
- aba59fe: Цикл агента и учёт расхода крепче (022, этап 8). Когда шаги хода кончаются, последний запрос идёт без инструментов (счёт по шагам всего хода, ход из одного шага не ограничивается) и с пометкой «подведи итог» в самом конце: ход кончается сводкой для человека, а не обрывом на вызове. Новая настройка `agent.sequentialTools` (`true` или список имён) пускает инструменты с последствиями по одному: «спиши деньги» не пойдёт одновременно с другим таким вызовом; очередь одна на чат (во всех ходах и повторах), ожидание очереди не съедает срок инструмента, «Стоп» отменяет ждущий вызов, и модель видит, что он не запускался. Оборванный вызов инструмента теперь говорит модели, что действие могло выполниться частично, и показывает последний промежуточный вывод. Ответ, упёршийся в предел длины, получает отметку: человек видит надпись «Ответ оборван по длине» (`labels.answerCutByLength`, в скине — `AgentMessage.cutByLength`), модель в следующих ходах видит пометку в конце своего ответа. Ответ из одних рассуждений (например, «Стоп» на рассуждении) больше не идёт в запрос, а сообщения человека, оказавшиеся подряд, склеиваются в одно (строгие провайдеры не принимают два подряд). Сегодняшняя дата словами идёт в конце последнего сообщения человека, только в запросе — начало запроса не меняется, кэш цел. Дата берётся по часовому поясу человека: браузер шлёт свой пояс (`timeZone`) с каждым запросом хода, сервер берёт только настоящий пояс IANA, иначе — новая настройка `agent.timeZone`, без неё — пояс сервера. Ни один ответ больше не бесплатен в журнале расхода: шаг, оборванный «Стопом» или сбоем после первых слов, и шаг, по которому провайдер не прислал числа, прикидываются по длине текста с учётом письма (кириллица, JSON и иероглифы плотнее латиницы; прикидка скорее завышает), а ответ, оборванный перезапуском сервера, записывается при следующем запуске (`recoverInterruptedAnswers`): только то, чего ещё нет в журнале, поэтому сбой после «Разрешить» не задваивает расход. Такие записи и итог ответа помечены «примерно» (`estimated`). В SQLite и PostgreSQL у записей расхода новая колонка `estimated`: миграция применяется сама при открытии хранилища. Своим хранилищам менять ничего не нужно: без колонки теряется только пометка. Фильтр `sumUsage` получил поля `messageId` и `source`: свои хранилища должны их учитывать (общие тесты `describeChatStorageContract` это проверяют).
|
|
1123
|
+
- f9b6162: Предел одновременных ответов одного человека. Проверка лимита расходов не резервирует деньги, поэтому 20 ходов, начатых разом, могли вместе потратить в разы больше бюджета. **Меняется поведение по умолчанию:** у человека одновременно идут не больше трёх ответов во всех его чатах; лишний ход (отправка, повтор, переделка, правка, продолжение после «Разрешить») получает `429 { error: { kind: "too_many_turns", limit } }` до любых записей. Настройка `maxConcurrentTurnsPerUser` у `createChatHandler` и `createChatService` (целое от 1 или `Infinity` — без предела; иначе ошибка при создании); ядро экспортирует `TooManyTurnsError` и `DEFAULT_MAX_CONCURRENT_TURNS_PER_USER`. Клиент показывает «Уже идут ответы в других чатах…», текст возвращается в поле, без «Нет связи». Заодно истёкший вход (401) клиент показывает как «Вход истёк. Войдите снова» с «Повторить», а не как «Нет связи» или «Что-то пошло не так», и не делает попыток переподключения. Новое: `useAgentChat().refusal`, тип `ChatRefusal`, тип `ErrorNoticeKind` (`ErrorNotice` скина получает и `signed_out`, `too_many_turns`), надписи `labels.errors.signed_out`, `labels.errors.too_many_turns`, `labels.editErrors.signedOut`, `labels.editErrors.tooManyTurns`, `labels.approvalTooManyTurns`; причины отказа правки `signed_out` и `too_many_turns`. Свой словарь целиком, без `{ ...russianLabels }`, — добавьте эти ключи.
|
|
1124
|
+
- 1b2f9f1: Фронт и API на разных адресах. `allowedOrigins` у `createChatHandler`, `createVoiceRelay`, `createVoiceHandler` и `mountDialogRelay` принимает шаблон поддоменов `https://*.example.com`: он пускает `app.example.com` и `a.b.example.com`, но не сам `example.com` и не `evil-example.com`; схема и порт должны совпасть, слишком широкий шаблон (`https://*.com`, `https://*.com.`) — ошибка при создании. Общие суффиксы вроде `*.co.uk` проверка не распознаёт: в шаблон вписывают свой домен. Отказ из-за того, что `Origin` не совпал с `Host` (старый браузер или `http://` за прокси, который подменил `Host`), журнал теперь называет прямо: «Origin не совпал с Host (…); за прокси передайте Host/X-Forwarded-Host» с готовыми строками для nginx. `onRefused` у `createRequestOriginCheck` получает `{ origin, reason, hosts? }` вместо строки; новые экспорты ядра — `originRefusalHint` и тип `OriginRefusal`. Клиент на 403 с `forbidden_origin` в теле показывает «Сервер чата не принимает сообщения с этой страницы» (у правки — только в редакторе), возвращает текст в поле и в режиме разработки пишет в консоль, какой адрес вписать в `allowedOrigins`; другой 403 (например, HTML-страница прокси) — обычный сбой. Новое: `ChatRefusal` и `ErrorNoticeKind` получили `forbidden_origin` (`ErrorNotice` скина может получить этот `kind`), причина отказа правки `forbidden_origin`.
|
|
1125
|
+
|
|
1126
|
+
**Изменение договора надписей:** в `ChatLabels` два новых обязательных ключа — `errors.forbidden_origin` и `editErrors.forbiddenOrigin`. Словарь на основе `{ ...russianLabels }` получает их сам; свой словарь целиком — дополните ими, иначе ошибка типов. Отдельного номера версии у договора надписей нет. Ещё: при отказе по месту хода (`too_many_turns`) или занятом чате файлы вложений больше не принимаются и не остаются ничьими; пока файлы принимаются, чат занят, и второе сообщение в него получает 409.
|
|
1127
|
+
- 00965e1: Голосовой режим: когда модель долго молчит, размышляет или вызывает инструмент, голос говорит короткую фразу («Хмм…», «Дай-ка подумаю…», «Сейчас поищу в интернете.», «Дай-ка вспомню.», «Ещё чуть-чуть…»). Фраза не попадает в чат и не уходит модели, настоящий ответ встаёт сразу за ней, перебивание её глушит.
|
|
1128
|
+
|
|
1129
|
+
- Включено само, если у режима настроена озвучка. Выключить: `speech.fillerPhrases: false` на сервере или `fillers: false` у `createVoiceDialogEngine`. Свои фразы: `speech.fillerPhrases`; пороги и привязка инструментов к видам фраз: `fillers.settings`. Подробности и пороги по умолчанию — в README пакета, раздел «Заполнители».
|
|
1130
|
+
- Если у вида одна фраза, она звучит и в следующих ходах; после инструмента без текста ответа «думает» взводится заново; список голосов не получен — фразы грузятся голосом из «готов».
|
|
1131
|
+
- Новые маршруты рядом с остальными: `GET {path}/fillers` и `GET {path}/fillers/{id}`; `mountDialogRelay` подключает их сам.
|
|
1132
|
+
- Звук фраз синтезируется один раз на голос и хранится в памяти сервера. Расход первого синтеза записывается как озвучка (`source: "speech"`) на человека, чей запрос его вызвал, и на его чат; умолчания стоят около 275 знаков на голос.
|
|
1133
|
+
- В `react` у моста чата `DialogChatBridge` появился необязательный метод `subscribeAssistantActivity` (начало и конец размышления или вызова инструмента); `useVoiceDialog` его реализует.
|
|
1134
|
+
|
|
1135
|
+
Обновление ничего не требует: приложение с настроенной озвучкой начнёт говорить заполнители само. Если не нужны — добавьте `fillerPhrases: false` в `speech`. Приложение, которое написало свой мост чата, продолжает работать: заполнитель «молчит» работает без нового метода.
|
|
1136
|
+
- 0e8e0ae: Голосовой режим: человек продолжил говорить, пока модель ещё думает, — вторая фраза дописывается к первой, а не уходит вторым сообщением. Ход останавливается, вместо прежнего сообщения уходит склеенное («Так. Дай-ка подумать.»), модель отвечает один раз; пустого оборванного ответа и отдельного «Так.» в чате нет.
|
|
1137
|
+
|
|
1138
|
+
- Склеивается, пока у хода нет ни текста, ни звука, ни начатого вызова инструмента (размышление и заполнитель не мешают). Текст или звук уже пошли, начат инструмент, сказано стоп-слово или два междометия — поведение прежнее: перебивание и отдельное сообщение. Подробности — в README пакета voice-dialog, раздел «Склейка фраз».
|
|
1139
|
+
- Склейка идёт через правку последнего сообщения человека: старая фраза остаётся в базе, но обрывком склейки: переключателя веток у склеенного сообщения нет, пролистать на обрывок нельзя (если человек потом сам правит склеенное сообщение, появляется настоящая развилка без обрывков). Признак — часть `data-voice-mode` склеенного сообщения (`{ replaces: id }`), без миграции базы. Отметка голосового режима и подсказки режима сохраняются, кэш модели цел. Название чата, выведенное из заменённого сообщения, обновляется.
|
|
1140
|
+
- `core`: `chatService.editMessage(userId, chatId, editedMessageId, message, options?)` принимает `{ voiceMode: true }`. `server`: `POST …/edit` принимает необязательное булево `voiceMode` (не булево — 400 `invalid`).
|
|
1141
|
+
- `react`: у состояния чата появился `replaceLastVoiceMessage(text)`, у моста `DialogChatBridge` — необязательный `replaceLastMessage(text)`; `useVoiceDialog` его реализует.
|
|
1142
|
+
|
|
1143
|
+
Обновление ничего не требует: движок склеивает сам, если мост и сервер той же версии. Приложение со своим мостом чата без `replaceLastMessage` работает как раньше: новая фраза в ожидании ответа перебивает ход и уходит отдельным сообщением.
|
|
1144
|
+
- 6b20d83: Голосовой режим («разговор»): человек говорит, слова сами уходят сообщением в чат, ответ агента звучит голосом, человека можно перебить. Новый пакет `@ykurilov/agent-chat-voice-dialog`: распознавание Soniox, озвучка ElevenLabs (голос по умолчанию «Профессиональный диктор для аудиокниг», модель `eleven_v4_turbo`), ключи только на сервере. Перебитый ответ остаётся в ленте с отметкой «Вы перебили: озвучено до «…»», она хранится и видна после перезагрузки. Расход на озвучку идёт в учёт и лимиты (`source: "speech"`). Человек выбирает голос в меню режима; выбор хранится на сервере.
|
|
1145
|
+
|
|
1146
|
+
Модель узнаёт о режиме двумя служебными пометками: одна при входе («ответы озвучиваются, отвечай коротко и разговорно»), одна при возврате к тексту («разговор снова текстом»). Они стоят в конце сообщения человека, где режим сменился, а не в каждом запросе. Сообщение из режима запоминается голосовым отметкой в самом сообщении (часть `data-voice-mode` в `parts`), поэтому миграции для этого нет, пометки выводятся из сохранённого и кэш модели цел. Тексты задают настройки агента `voiceModeHint` и `textModeNote` (есть умолчания).
|
|
1147
|
+
|
|
1148
|
+
Обновление ничего не ломает: без нового пакета чат работает как раньше. Диктовка в поле ввода (`@ykurilov/agent-chat-voice`) не меняется.
|
|
1149
|
+
|
|
1150
|
+
### Как обновить приложение
|
|
1151
|
+
|
|
1152
|
+
Нужно, только если вы хотите голосовой режим.
|
|
1153
|
+
|
|
1154
|
+
1. Поставьте пакет `@ykurilov/agent-chat-voice-dialog` той же версии, что и остальные пакеты SDK. Скин Тандем обновите вместе с SDK: договор скина теперь версии 10 (необязательный `dialog` у поля ввода, панель режима, меню голосов, отметка обрыва; свой скин может их не рисовать, тогда кнопки режима не будет).
|
|
1155
|
+
2. Сервер. Рядом с чатом подключите режим (нужен WebSocket; с Hono и `@hono/node-ws`):
|
|
1156
|
+
|
|
1157
|
+
```ts
|
|
1158
|
+
import { mountDialogRelay } from "@ykurilov/agent-chat-voice-dialog/hono";
|
|
1159
|
+
|
|
1160
|
+
mountDialogRelay(app, {
|
|
1161
|
+
path: "/api/voice-dialog",
|
|
1162
|
+
upgradeWebSocket,
|
|
1163
|
+
storage,
|
|
1164
|
+
resolveUser, // тот же вход, что у чата
|
|
1165
|
+
spendingLimits, // по желанию
|
|
1166
|
+
logError: (message) => console.error(message),
|
|
1167
|
+
speech: {
|
|
1168
|
+
voices: [{ id: "oE0n0S3wK5Mj9C62iehn", name: "Профессиональный диктор для аудиокниг" }],
|
|
1169
|
+
defaultVoiceId: "oE0n0S3wK5Mj9C62iehn",
|
|
1170
|
+
pricePerThousandChars: { eleven_v4_turbo: 0.11, eleven_v4: 0.22 }, // ваша цена, $ за 1000 знаков
|
|
1171
|
+
},
|
|
1172
|
+
});
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
Ключи `ELEVENLABS_API_KEY` и `SONIOX_API_KEY` — в окружении сервера. Next.js: WebSocket нужен свой Node-сервер, см. README пакета (`createDialogRelay`).
|
|
1176
|
+
3. Браузер:
|
|
1177
|
+
|
|
1178
|
+
```tsx
|
|
1179
|
+
const voiceDialog = createVoiceDialogEngine({ basePath: "/api/voice-dialog" });
|
|
1180
|
+
<AgentChat api="/api/chat" skin={tandemSkin} voiceDialog={voiceDialog} />
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
4. Настройки. Слова остановки и междометия по умолчанию только русские; для другого языка задайте `bargeIn.stopWords` и `bargeIn.ignoreWords` у движка и `language` на сервере. Строгая политика содержимого страницы: добавьте `worklet-src blob:`.
|
|
1184
|
+
5. Хранилище. SQLite получает миграцию 0008, PostgreSQL — 0007 (схема `agent_chat`) сами при открытии базы. Свой `ChatStorage`: добавьте необязательные методы `setVoiceCut`, `getVoiceChoice`, `setVoiceChoice` (без них отметка обрыва и выбор голоса живут только до перезагрузки) и значение `"speech"` в `UsageSource`, если учёт расхода у вас свой.
|
|
1185
|
+
6. Свой экран (не наш скин): части сообщения с типом `data-voice-mode` не рисуйте, показывайте только текстовые части.
|
|
1186
|
+
7. Проверка: `pnpm typecheck` проходит; в чате появилась кнопка режима, после нажатия виден статус «Слушаю», фраза уходит сообщением, ответ звучит.
|
|
1187
|
+
- d12dcf3: Голосовой режим: звуки человека (смех, вздох, кашель…) доходят до модели.
|
|
1188
|
+
|
|
1189
|
+
- Звук с микрофона параллельно с Soniox уходит с нашего сервера в ElevenLabs Scribe (`scribe_v2`, `tag_audio_events`); слова Scribe не использует, ключ остаётся на сервере. Звук уходит кусками прямо во время фразы: каждые `sounds.chunkSeconds` (1,5 с) нового звука, с перекрытием `sounds.overlapSeconds` (0,5 с), на конце фразы — только хвост; повторы на перекрытии схлопываются. Тишина (тише `sounds.silenceRms`, 0,01) не отправляется. На конце фразы ждётся только хвост и ещё не вернувшиеся куски, не дольше `sounds.waitMs` (1000 мс, по замеру Scribe отвечает за 0,7–0,95 с): не успел — звуки приклеятся к следующей фразе. Куски не копятся в очередь (не больше двух запросов). Расход считается по фактически отправленным секундам, включая перекрытия. Фраза из одного смеха без слов ход модели не запускает.
|
|
1190
|
+
- Звуки лежат в сообщении человека, в части `data-voice-mode` (`data: { sounds: ["laughter"] }`), уходят на сервер как `voiceSounds` вместе с `voiceMode` (значение не из закрытого списка — 400 `invalid`). Пометка для модели («были слышны его звуки: смех») выводится из сохранённого при сборке запроса: начало запроса от хода к ходу не меняется, кэш цел; склейка фраз сохраняет звуки обеих. На экране звуков нет. В пометке входа в режим появилось короткое правило про пометку звуков.
|
|
1191
|
+
- Эхо: звук того же вида, пойманный, пока играл кусок озвучки агента с таким же звуком (тег «[laughs]», заполнитель, отдельное «Ха-ха!»), плюс короткий хвост на задержку микрофона (`soundEchoTailMs`, 0,6 с), звуком человека не считается; смех в ответ на шутку агента доходит.
|
|
1192
|
+
- Ошибка или недоступность Scribe режим не ломает: разговор идёт без звуков, в журнал — предупреждение. Расход пишется записью `source: "sounds"` (`inputTokens` — секунды звука) и входит в лимиты расходов.
|
|
1193
|
+
- Новое: `sounds` у `createDialogRelay` и `mountDialogRelay` (`pricePerHour` обязательна), `sendVoiceMessage(text, { sounds })` и `replaceLastVoiceMessage(text, { sounds })` (`react`, `DialogChatBridge.send/replaceLastMessage` получили `options`), `voiceSounds` у `SendMessageOptions` и в теле `…/messages`, `…/edit`, экспорты `core`: `VOICE_SOUNDS`, `isVoiceSound`, `uniqueVoiceSounds`, `voiceSoundsNote`, `soundsOfMessage`, `UsageSource` получил `"sounds"`; `react`: тип `VoiceSound`; `voice-dialog/server`: `createScribeSounds`, `createSoundsUsage`, `HUMAN_SOUND_KINDS`.
|
|
1194
|
+
|
|
1195
|
+
Как обновить приложение: ничего не требуется, пока `sounds` не задан. Чтобы включить, добавьте `sounds: { pricePerHour }` (цену Scribe берите из прайса ElevenLabs; v2 — 0,22 $ за час) в настройки режима; ключ тот же `ELEVENLABS_API_KEY`. Своё хранилище расходов должно принимать `source: "sounds"` (текстовое поле, миграций не нужно). Пометка входа в режим по умолчанию стала длиннее на одно правило: кэш модели у идущих голосовых чатов один раз промахнётся. Свой `agent.voiceModeHint` (строка) заменяет пометку входа целиком, правило про звуки в нём не появится, но пометка звуков у сообщений всё равно добавится.
|
|
1196
|
+
- 946e5d8: Голосовой режим: теги эмоций и подачи голоса.
|
|
1197
|
+
|
|
1198
|
+
- Модель получает список тегов Eleven v4 («[laughs]», «[sighs]», «[whispers]», «[excited]» и других) и может ставить их в ответ; голос их отыгрывает, а на экране их нет: ни в тексте ответа, ни при копировании, ни в поиске по прошлым разговорам. Это касается только ответов голосовых ходов (сообщение человека с отметкой `data-voice-mode`): в текстовом режиме и в сводке сжатия скобки остаются как есть («[docs]», «[error]» там обычные слова). Обычные скобки («[1]», ссылки, чекбоксы, код) не трогаются. В истории для модели теги остаются, кэш цел.
|
|
1199
|
+
- Одиночный тег больше не обрывает реплику: сервер не отправляет в озвучку текст без букв и цифр, а тег приклеивается к соседнему предложению; тег в конце ответа («Да. [laughs]») — к последнему.
|
|
1200
|
+
- Смех и вздох агента в микрофоне не считаются перебиванием: звуки сверяются по шаблону («ха-ха-ха-ха», «ооох»), составные слова («Дай-ка») — по частям.
|
|
1201
|
+
- Новые экспорты: `stripVoiceTags`, `splitVoiceTags` (`core` и `react`), `voiceAnswerIds` (`react`), `isVoiceMessage`, `VOICE_TAGS`, `DEFAULT_VOICE_MODE_HINT`, `DEFAULT_TEXT_MODE_NOTE` (`core`). `messageText` (`react`) и `searchableText` (`history-search`) получили параметр `{ voiceAnswer }`. `sendVoiceMessage` кладёт отметку `data-voice-mode` в сообщение сразу.
|
|
1202
|
+
|
|
1203
|
+
Как обновить приложение: ничего не требуется. Пометки по умолчанию обновятся сами; кэш модели у идущих голосовых чатов один раз промахнётся. Если вы задавали свои `voiceModeHint` и `textModeNote`, теги в них не появятся: добавьте их сами, если нужны. Свой скин, который сам выводит текст ответа, должен вырезать теги только у ответов голосовых ходов (`voiceAnswerIds`). Побочный эффект: в ответе голосового хода слово в скобках, похожее на тег («[Google]»), тоже не показывается.
|
|
1204
|
+
|
|
1205
|
+
### Patch Changes
|
|
1206
|
+
|
|
1207
|
+
- 42d17cc: Панель агента надёжнее. Место вложенной страницы всегда главнее места макета, в котором она стоит, — и при первом показе, и после переходов; `data` с циклом или `BigInt` больше не роняет страницу. Смена человека (`userKey`) в том же кадре убирает чат и список прежнего, а его недоигранный ответ не раздаёт изменения данных и не делает действий на странице; «Новый чат», переименование и удаление прежнего, закончившиеся после смены, нового не трогают. Сообщение об изменении данных не раздаётся второй раз и после перезагрузки страницы во время ответа (вкладка помнит последние 500 в `sessionStorage`). У дубликата вкладки («Дублировать вкладку») свой номер: браузерный вызов не исполнится дважды. Esc при открытом списке чатов сначала возвращает к чату, второй — закрывает панель. Делать в приложении ничего не нужно.
|
|
1208
|
+
- 5e99fb2: Место на экране с данными, которые не превращаются в JSON (цикл, `BigInt`), больше не роняет отправку сообщения: место уходит без `data`, а если не сериализуется и описание — без места.
|
|
1209
|
+
- ecd63f8: Лента сообщений: медленная прокрутка тачпадом вверх у низа больше не «сопротивляется». Раньше лента дёргалась обратно вниз, пока читатель не сдвигал её резко (больше 96 px за одно событие).
|
|
1210
|
+
|
|
1211
|
+
- Шаг вверх после ввода читателя (колесо, касание, клавиша) снимает прилипание к низу сразу, без порога. Порог 96 px остался для обратного: лента снова прилипает, когда читатель вернулся к низу.
|
|
1212
|
+
- Низ доводится, только если список вырос или скроллер стал ниже, а не на любую перестановку окна ленты, и не пока читатель ведёт ленту вверх.
|
|
1213
|
+
- Лента по-прежнему едет за растущим ответом, открывается внизу и ведёт вниз после отправки; экранная клавиатура и рост поля ввода по-прежнему доводят низ.
|
|
1214
|
+
|
|
1215
|
+
Обновление ничего не требует.
|
|
1216
|
+
- 96098be: Мелкие исправления (022). Символ с кодом ноль в первом сообщении больше не роняет отправку на Postgres: он выбрасывается из названия чата, из записей памяти и из сводки сжатия истории (иначе запись сжатия падала бы при каждой попытке); в самом тексте сообщения он лежит как раньше. Вложения отмечаются прикреплёнными до записи сообщения, а не после: если сервер упал между двумя шагами, чистка брошенных файлов не удалит их через сутки (файл, отмеченный зря, безвреден и уходит вместе с чатом). Во всех пакетах появилось поле `engines: { node: ">=22" }`; голосовой сервер (`createVoiceRelay`) и голосовой диалог (`createDialogRelay`) на Node 20 сразу при создании говорят понятной ошибкой, что нужен Node 22 (раньше падали при первом соединении; со своим `connectUpstream` проверки нет). В пакете вложений `react` и `@ykurilov/agent-chat-react` стали необязательными peer-зависимостями: серверу без экрана они больше не ставятся. Приложению, которое берёт `@ykurilov/agent-chat-attachments/react`, достаточно иметь их в проекте — для чата на экране они нужны и так; если они подтягивались только через вложения, добавьте их в зависимости приложения.
|
|
1217
|
+
- 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`).
|
|
1218
|
+
- 6335921: Голосовой режим: отметка «Вы перебили: озвучено до «…»» больше не пропадает с экрана. Раньше её писали прямо в сообщение, а идущий поток ответа (пока остановка доходит до сервера) заменял сообщение своей копией без отметки. Теперь отметки хранятся отдельно и накладываются на ленту при выдаче.
|
|
1219
|
+
|
|
1220
|
+
Обновление ничего не требует.
|
|
1221
|
+
- 1402afe: Голосовой режим: исправления по ревью.
|
|
1222
|
+
|
|
1223
|
+
- Потолок знаков озвучки в минуту (`speech.maxCharsPerMinute`) теперь общий для всех вкладок человека, а не на каждое соединение. Следующая реплика ждёт записи расхода предыдущей, лимит её видит.
|
|
1224
|
+
- Фразы, сказанные, пока останавливается прежний ответ, уходят одним сообщением и не пропадают.
|
|
1225
|
+
- Конец и хвост перебитого ответа больше не закрывают и не озвучивают новую реплику. Голосовая отправка читает «занято» в момент вызова.
|
|
1226
|
+
- Команды диктора не задерживают звук с микрофона и перебивание.
|
|
1227
|
+
- Звук с микрофона ограничен реальным временем с запасом: больше — закрытие кодом 4413.
|
|
1228
|
+
- Новая необязательная настройка `allowedOrigins` у `mountDialogRelay`: список страниц, которым можно открывать режим (чужая получает 403). Без неё всё как раньше; для сервера, доступного из браузера, рекомендуем задать.
|
|
1229
|
+
- Новая необязательная настройка `recognition.context`: подсказка распознаванию (тема, термины, имена).
|
|
1230
|
+
|
|
1231
|
+
## 0.13.0
|
|
1232
|
+
|
|
1233
|
+
### Minor Changes
|
|
1234
|
+
|
|
1235
|
+
- 4793e4d: Чат запоминает последний выбор человека: провайдера, модель и уровень рассуждений. Новый чат открывается на них, а не на модели по умолчанию. Если модели больше нет в каталоге или у её провайдера нет ключа, новый чат открывается на выборе по умолчанию из настроек; уровень, которого у модели нет, заменяется её уровнем по умолчанию. У каждого человека свой выбор, старые чаты с сообщениями остаются на своём, а пустой чат, который открывается кнопкой «Новый чат», переходит на последний выбор. Запоминается последний выбор, в том числе сделанный в старом чате. При двух открытых вкладках побеждает позже пришедший запрос. Выбор хранится по человеку и общий для всех обработчиков на одной базе; недоступная модель заменяется моделью по умолчанию. Новый чат сразу показывает запомненный выбор, без мигания моделью по умолчанию.
|
|
1236
|
+
|
|
1237
|
+
Обновление ничего не требует: SQLite и PostgreSQL получают новую таблицу сами при открытии базы (SQLite — миграция 0007, PostgreSQL — миграция 0006 в схеме `agent_chat`), встроенное хранилище в памяти тоже умеет запоминать.
|
|
1238
|
+
|
|
1239
|
+
### Как обновить приложение со своим хранилищем
|
|
1240
|
+
|
|
1241
|
+
Нужно только если приложение передаёт в `createChatHandler` собственное хранилище (свою реализацию `ChatStorage`). Без этого шага всё продолжит работать как раньше: новые чаты будут открываться на `defaultChoice`.
|
|
1242
|
+
|
|
1243
|
+
1. В своём хранилище добавьте два необязательных метода из `ChatStorage`:
|
|
1244
|
+
|
|
1245
|
+
```ts
|
|
1246
|
+
getUserModelChoice(ownerId: string): Promise<{ modelId: string; providerId: string; reasoning?: string } | null>;
|
|
1247
|
+
saveUserModelChoice(ownerId: string, choice: { modelId: string; providerId: string; reasoning?: string }): Promise<void>;
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
Один выбор на человека (`ownerId`): повторная запись заменяет прежний, запись без `reasoning` стирает сохранённый уровень. С чатами выбор не связан: удаление чата его не трогает. Пример запросов — в `packages/storage-sqlite/src/sqlite-storage.ts`.
|
|
1251
|
+
2. Добавьте таблицу или коллекцию под выбор (SQLite: `owner_id` — первичный ключ, `model_id`, `provider_id`, `reasoning` — может быть пустым, `updated_at`).
|
|
1252
|
+
3. В тестах хранилища вызов `describeChatStorageContract(createStorage)` сам проверит новые методы; если они есть, проверки пройдут, если нет — будут пропущены.
|
|
1253
|
+
4. Проверка: `pnpm typecheck` проходит; сменить модель в чате, открыть новый чат — на нём та же модель.
|
|
1254
|
+
|
|
1255
|
+
## 0.12.0
|
|
1256
|
+
|
|
1257
|
+
No changes in this release.
|
|
1258
|
+
|
|
1259
|
+
## 0.11.0
|
|
1260
|
+
|
|
1261
|
+
### Minor Changes
|
|
1262
|
+
|
|
1263
|
+
- 9d510b8: **Ломает приложения:** только если вы сами собрали словарь надписей целиком (без `{ ...russianLabels, … }`): в `ChatLabels` появилась надпись `voicePolishing`. Кроме того, клиент голоса `mode: "relay"` теперь шлёт серверу команду `finalize`, которую старый сервер relay не знает (закрывает соединение кодом 4400): сервер обновляйте раньше клиента или вместе с ним.
|
|
1264
|
+
|
|
1265
|
+
Голос стал точнее и быстрее, а основной путь — через ваш сервер.
|
|
1266
|
+
|
|
1267
|
+
Что изменилось:
|
|
1268
|
+
|
|
1269
|
+
- **Через сервер — основной путь.** Ключ Soniox не уходит в браузер даже в виде временного. В документации relay первый и рекомендуемый; прямой режим (`createVoiceHandler`) работает как раньше, но помечен «не рекомендуется».
|
|
1270
|
+
- **Soniox `stt-rt-v5`.** Включено определение конца фразы. По «стоп» клиент шлёт `finalize`, Soniox закрепляет всё сказанное и присылает метку `<fin>`; после неё текст готов. Не ответил за `stopTimeoutMs` (3 с) — остаётся то, что пришло, без ошибки.
|
|
1271
|
+
- **Чистка текста моделью (по желанию, только relay):** `createVoiceRelay({ polish: { provider: "together" } })` (ключ `TOGETHER_API_KEY`, модель по умолчанию `deepseek-ai/DeepSeek-V4.1-Flash`) или `polish: { clean: async (text) => … }`. После «стоп» сервер убирает «э-э», повторы и оговорки, расставляет знаки и заглавные. Заменяется только надиктованный кусок: набранное руками и правка во время чистки не теряются. Сбой или тишина дольше 5 секунд — остаётся текст Soniox. Пока идёт чистка, кнопка микрофона крутится (`VoiceInputView.polishing`, подпись `voicePolishing`). Ответ, похожий на ответ на вопрос или отказ (мало слов из исходника, слишком короткий или длинный), отбрасывается. Текст уходит в модель чистки (Together), ключ остаётся на сервере. Лимиты расходов (`@ykurilov/agent-chat-limits`) чистку не учитывают; у неё свой лимит `polishRateLimit` (6 в минуту на человека, `false` — выключить). Новый `maxConnectionsPerUser` (по умолчанию 2 соединения на человека, `false` — выключить).
|
|
1272
|
+
- Серверная сессия Soniox вынесена в `createSonioxSession` (`@ykurilov/agent-chat-voice/server`): её можно использовать без поля ввода.
|
|
1273
|
+
- Исправлено: два сообщения Soniox подряд могли продублировать хвост фразы в поле; при длинной диктовке поле теперь прокручивается к последним словам.
|
|
1274
|
+
|
|
1275
|
+
### Как обновить приложение
|
|
1276
|
+
|
|
1277
|
+
1. Сначала обновите сервер, потом клиент (или оба вместе): `pnpm update "@ykurilov/agent-chat-*" --latest`.
|
|
1278
|
+
2. Только если словарь надписей собран целиком: добавьте `voicePolishing: "Приводим текст в порядок…"`.
|
|
1279
|
+
3. Хотите основной путь: замените `createVoiceHandler` на `createVoiceRelay` + `mountVoiceRelay` и `createVoiceInput({ api: "/api/voice", mode: "relay" })` (код — в README пакета `voice`). Нужен WebSocket на сервере и (если есть прокси) `ws: true`.
|
|
1280
|
+
4. Хотите чистку: добавьте `polish: { provider: "together" }` в `createVoiceRelay` и `TOGETHER_API_KEY` в окружение сервера.
|
|
1281
|
+
5. Проверка: `pnpm typecheck`; продиктуйте фразу с «э-э» и нажмите «стоп»: текст должен замениться на исправленный.
|
|
1282
|
+
|
|
1283
|
+
## 0.10.0
|
|
1284
|
+
|
|
1285
|
+
### Minor Changes
|
|
1286
|
+
|
|
1287
|
+
- 4a76ff3: **Ломает приложения:** у `ChatStorage` появились обязательные операции `updateAwaitingMessage` и `getMessage`, у словаря `ChatLabels` — новые ключи `approval*`; касается приложений со своим хранилищем и с полным словарём надписей.
|
|
1288
|
+
|
|
1289
|
+
Подтверждение перед опасными действиями агента. Инструмент, помеченный `needsApproval: true` (или функцией от входных данных), не выполняется без разрешения человека: ход останавливается, в ленте появляется карточка с названием действия, входными данными и кнопками «Разрешить», «Отклонить» и «Всегда в этом чате». Кнопка «Всегда» есть только у инструмента с `needsApproval: true`; у инструмента с функцией её нет, каждый вызов спрашивает заново. Ждать можно сколько угодно: состояние лежит на сервере, переживает перезагрузку страницы и перезапуск сервера, ответить можно с любого устройства.
|
|
1290
|
+
|
|
1291
|
+
Что изменилось:
|
|
1292
|
+
|
|
1293
|
+
- `@ykurilov/agent-chat-core`: статус ответа `awaiting_approval`; `answerApproval` у сервиса чата; ответ после решения дописывается в то же сообщение, инструмент выполняется ровно раз. «Отклонить» — модель получает «пользователь отклонил» и продолжает. Новое сообщение вместо ответа на карточку считается отказом. Сбой или «Стоп» после «Разрешить» не оставляют вызов без итога: он закрывается как прервавшийся («результат неизвестен», если инструмент уже мог сработать). Ответ на карточку проверяет лимит расходов (429), а расход паузы и продолжения не считается дважды. После смены модели во время ожидания продолжение идёт на модели ждущего сообщения. Кэш модели не страдает: инструкция и набор инструментов не меняются.
|
|
1294
|
+
- `@ykurilov/agent-chat-server`: адрес `POST /:chatId/approvals/:approvalId` с телом `{ "decision": "allow" | "deny" | "always" }`. Ошибки: 409 `busy` (в чате идёт другой ход), 409 `approval_not_pending` (на этот запрос отвечать уже нечем), 429 `spending_limit_reached`.
|
|
1295
|
+
- `@ykurilov/agent-chat-storage-sqlite`, `@ykurilov/agent-chat-storage-postgres`: реализуют `updateAwaitingMessage` и `getMessage`. Схема базы не меняется, миграции не нужны: статус ответа хранится обычным текстом.
|
|
1296
|
+
- `@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`.
|
|
1297
|
+
- `@ykurilov/agent-chat-skin-tandem`: карточка подтверждения (группа с названием для читалки, «Ждёт вашего решения» и итог озвучиваются, фокус не теряется, на телефоне длинные поля не выпирают) и пометка решения в строке вызова.
|
|
1298
|
+
|
|
1299
|
+
### Как обновить приложение
|
|
1300
|
+
|
|
1301
|
+
Если опасных инструментов нет, а хранилище и скин стандартные, обновление ничего не требует.
|
|
1302
|
+
|
|
1303
|
+
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` проходит.
|
|
1304
|
+
2. Полный свой словарь надписей (`ChatLabels`, объект без `...russianLabels`): добавьте двенадцать ключей `approval*` из списка выше. Словарь `{ ...russianLabels, … }` менять не нужно.
|
|
1305
|
+
3. Свой скин: если `ToolCallRow` свой, добавьте разбор `approval` — `{ state: "requested", onDecide, pending, failed, failureText?, canAlways }` (нарисовать кнопки, `onDecide("allow" | "deny" | "always")`; «Всегда» показывать только при `canAlways`; при `failed` показать `failureText`, если он есть) и `{ state: "allowed" | "always" | "denied" }` (итог решения). Нет `approval` — рисуйте строку как раньше. Название действия — в `title`.
|
|
1306
|
+
4. Чтобы включить подтверждение: у опасного инструмента задайте `needsApproval: true` (можно функцией от входных данных; с функцией кнопки «Всегда» не будет) и `title` — название для карточки. Больше ничего подключать не надо.
|
|
1307
|
+
5. Проверка: `pnpm typecheck`; попросите агента вызвать опасный инструмент — в ленте карточка, после «Разрешить» действие выполнено, ответ дописан в то же сообщение.
|
|
1308
|
+
|
|
1309
|
+
## 0.9.0
|
|
1310
|
+
|
|
1311
|
+
### Minor Changes
|
|
1312
|
+
|
|
1313
|
+
- d07a1c3: **Ломает приложения:** только если вы сами реализовали `ChatStorage` или собрали словарь надписей целиком (без `{ ...russianLabels, … }`); остальным ничего делать не нужно.
|
|
1314
|
+
|
|
1315
|
+
Новый пакет `@ykurilov/agent-chat-limits`: лимиты расходов. Бюджет в долларах на человека, на чат или на то и другое; период — календарный месяц (по умолчанию), сутки или без обнуления. Исчерпанный лимит останавливает ход до его начала; при 80% чат предупреждает.
|
|
1316
|
+
|
|
1317
|
+
Что изменилось:
|
|
1318
|
+
|
|
1319
|
+
- Ядро: цены в профиле модели (`ModelProfile.pricing` по провайдерам; вход, выход, чтение и запись кэша, рассуждения считаются как выход). Цены есть у всех стандартных моделей. Журнал расхода `usage_records` — токены и стоимость в долларах (`costUsd`, по ценам модели на момент хода) каждого ответа и фонового сжатия, в том числе сжатия, которое не удалось, но было оплачено; запись датируется началом хода. Журнал только дописывается и переживает удаление чата. Сумма лимита берётся из записей, поэтому смена цен и удаление модели из каталога прошлый расход не меняют. Новые методы хранилища `insertUsageRecord` и `sumUsage`.
|
|
1320
|
+
- Ядро: `createChatService({ limits })` и `ChatService.getSpendingStatus`. Проверка идёт перед отправкой, повтором, переделкой и правкой, до любых записей; исчерпание — `SpendingLimitReachedError`.
|
|
1321
|
+
- Сервер: `createChatHandler({ limits })`. Исчерпанный лимит — `429 {"error":{"kind":"spending_limit_reached","scope","period","resetsAt"}}`; новый адрес `GET /api/chat/:id/spending` (без сумм в долларах).
|
|
1322
|
+
- `@ykurilov/agent-chat-limits`: `createSpendingLimits({ storage, catalog, budget, period?, warnAt?, unpricedModels?, now?, logError? })` и `getSpending({ userId, chatId })` для страницы «тариф» (расход чата считается только среди записей этого человека). Своя модель без цены — понятная ошибка при создании; явно `unpricedModels: "ignore"` — такие ходы пишутся с нулевой стоимостью. `compactNow` лимит не проверяет: ручное сжатие остаётся доступным.
|
|
1323
|
+
- Хранилища: SQLite (миграция `0006`) и PostgreSQL (миграция `0005`) добавляют таблицу `usage_records`; миграции применяются при открытии хранилища сами, существующие данные не трогаются. Проверено на копии базы песочницы.
|
|
1324
|
+
- Браузер: `AgentChat` принимает `spendingLimits`. Договор скина версии 8: необязательный компонент `SpendingNotice({ id, state, text })` (`state` бывает `none`: область для диктора всегда на странице) и необязательные свойства `Composer.sendingBlocked` и `Composer.sendingBlockedReasonId` (для `aria-describedby`). Скин Тандема их рисует; при остановке фокус переходит на надпись и возвращается в поле, когда лимит снят. Отказ 429 не запускает переподключения: текст возвращается в поле (при правке сообщения редактор остаётся открытым с текстом и надписью `editErrors.spendingLimit`), показывается «Лимит на месяц исчерпан, обновится 1 ноября»; исчерпание снимается само в момент обновления.
|
|
1325
|
+
- Словарь надписей: `spendingWarning` и `spendingExhausted` (функции от сведений о лимите) и `editErrors.spendingLimit`.
|
|
1326
|
+
|
|
1327
|
+
Не входит в расход: веб-инструменты (`web-tools`, другой сервис), незаконченный шаг остановленного хода и ход, оборванный перезапуском сервера. Ход, начатый в пределах бюджета, доводится до конца; одновременные ходы могут вместе выйти за бюджет (проверка не резервирует деньги).
|
|
1328
|
+
|
|
1329
|
+
### Как обновить приложение
|
|
1330
|
+
|
|
1331
|
+
Если лимиты не нужны, делать ничего не нужно, кроме двух случаев:
|
|
1332
|
+
|
|
1333
|
+
1. **Только если вы реализовали `ChatStorage` сами:** добавьте `insertUsageRecord(record)` и `sumUsage(filter)` (запись расхода несёт `costUsd`, сумма возвращает `costUsd`) и прогоните `describeChatStorageContract` (в нём появился блок «записи расхода»). Хранилища SQLite и PostgreSQL из этого репозитория обновляются сами.
|
|
1334
|
+
2. **Только если словарь надписей написан целиком** (не через `{ ...russianLabels, … }`): добавьте функции `spendingWarning` и `spendingExhausted` и надпись `editErrors.spendingLimit`. Значения — в `russianLabels`.
|
|
1335
|
+
|
|
1336
|
+
Чтобы включить лимиты:
|
|
1337
|
+
|
|
1338
|
+
1. `pnpm add @ykurilov/agent-chat-limits`.
|
|
1339
|
+
2. На сервере: `const limits = createSpendingLimits({ storage, catalog, budget: async ({ userId }) => ({ user: 5 }) })` и `createChatHandler({ …, limits })`. `storage` и `catalog` — те же, что у чата.
|
|
1340
|
+
3. В браузере: `<AgentChat … spendingLimits />`. Скин свой — добавьте `SpendingNotice` (с `id`, состоянием `none` и `role="status"`) и учтите `Composer.sendingBlocked` и `sendingBlockedReasonId`; нет их — надписи не будет, но отправка остановится.
|
|
1341
|
+
4. Проверка: с маленьким бюджетом чат сначала предупреждает, потом останавливает отправку и пишет дату обновления.
|
|
1342
|
+
|
|
1343
|
+
## 0.8.0
|
|
1344
|
+
|
|
1345
|
+
### Minor Changes
|
|
1346
|
+
|
|
1347
|
+
- a24c79d: **Ломает приложения:** у `ChatStorage` три новых обязательных метода (`listChats`, `renameChat`, `deleteChat`), у `AttachmentStorage` — один (`removeAllOfChat`), а у `ChatLabels` — 16 новых обязательных надписей `chatList*`. Приложения на готовых хранилищах и готовом словаре надписей ничего менять не должны; поломка только у своих реализаций.
|
|
1348
|
+
|
|
1349
|
+
Список чатов в боковой панели: создать чат, переименовать, удалить. Чат сам получает название по первому сообщению. Это расширение по желанию: приложение без `chatList` и без `ChatWorkspace` работает как раньше.
|
|
1350
|
+
|
|
1351
|
+
Что изменилось:
|
|
1352
|
+
|
|
1353
|
+
- `@ykurilov/agent-chat-core`: `createChatService` умеет `listChats(userId)`, `renameChat(userId, chatId, title)`, `deleteChat(userId, chatId)`. Название ставится по первому сообщению (до 60 знаков), вручную заданное не перезаписывается; длина названия до 100 знаков. Удаление настоящее: идущий ответ и сжатие останавливаются, затем убираются сообщения, записи сжатия, выбор веток, файлы вложений и сам чат. Чужой и несуществующий чат — одинаково `ChatNotFoundError`. Экспортируются `deriveChatTitle`, `normalizeChatTitle`, `InvalidChatTitleError`, тип `ChatListItem`. `Chat.title` необязательное. Набор проверок `describeChatStorageContract` расширен.
|
|
1354
|
+
- `@ykurilov/agent-chat-server`: `createChatHandler({ chatList: true })` включает три адреса: `GET {basePath}` (список), `PATCH {basePath}/:id` с телом `{ title }`, `DELETE {basePath}/:id`. Без `chatList` эти адреса отвечают 404. Чужой чат — 404, неверное название — 400. Идущий ответ удалению не мешает: он останавливается. `409` при удалении — только если чат уже удаляется или в этот миг запускается ход; пока чат удаляется, сообщение, сжатие и вложения в него тоже получают `409`. Переименование `409` не даёт.
|
|
1355
|
+
- `@ykurilov/agent-chat-storage-sqlite` и `@ykurilov/agent-chat-storage-postgres`: колонка `title` у чатов, методы `listChats`, `renameChat`, `deleteChat` (удаление в одной транзакции). Миграция сама заполняет названия старых чатов по их первому сообщению.
|
|
1356
|
+
- `@ykurilov/agent-chat-attachments`: `AttachmentStorage` получил `removeAllOfChat(chatId)`; `createAttachments` отдаёт ядру `removeChatFiles`, и файлы удаляются вместе с чатом. Набор `describeAttachmentStorageContract` расширен.
|
|
1357
|
+
- `@ykurilov/agent-chat-react`: компонент `ChatWorkspace` (список и открытый чат вместе), хук `useChatList`, у `AgentChat` необязательное `onBusyChange`. Договор скина версии 7: скин, который хочет работать с `ChatWorkspace`, дополнительно даёт `ChatWorkspace` и `ChatList` (тип `ChatListSkin`); договор самого чата не менялся. 16 новых надписей `chatList*` в словаре (`ChatLabels`).
|
|
1358
|
+
- `@ykurilov/agent-chat-skin-tandem`: `TandemChatWorkspace` и `TandemChatList`. На широком экране список слева, на узком — панель по кнопке «Чаты».
|
|
1359
|
+
|
|
1360
|
+
### Как обновить приложение
|
|
1361
|
+
|
|
1362
|
+
Если список чатов не нужен, а хранилища и словарь надписей готовые, обновление ничего не требует.
|
|
1363
|
+
|
|
1364
|
+
Если у приложения своё `ChatStorage`:
|
|
1365
|
+
|
|
1366
|
+
1. Добавьте методы `listChats(ownerId)`, `renameChat(chatId, title | null)` и `deleteChat(chatId)`. Список — только чаты владельца, самые недавно менявшиеся первыми; `deleteChat` убирает сообщения, записи сжатия, выбор веток и чат одной транзакцией. Точные правила — в общем наборе проверок.
|
|
1367
|
+
2. Храните название чата (`Chat.title`, необязательное) и проведите свою миграцию.
|
|
1368
|
+
3. Проверка: `describeChatStorageContract` из `@ykurilov/agent-chat-core/storage-contract` проходит.
|
|
1369
|
+
|
|
1370
|
+
Если у приложения своё `AttachmentStorage`: добавьте `removeAllOfChat(chatId): Promise<number>` (удалить все файлы чата, вернуть сколько) и прогоните `describeAttachmentStorageContract`.
|
|
1371
|
+
|
|
1372
|
+
Если у приложения свой полный словарь надписей (`ChatLabels` целиком, а не `{ ...russianLabels, … }`): добавьте 16 новых ключей, иначе `pnpm typecheck` упадёт. Простые строки: `chatListHeading` («Чаты»), `chatListNewChat` («Новый чат»), `chatListUntitled` («Новый чат»), `chatListLoading`, `chatListLoadFailed`, `chatListCreateFailed`, `chatListRenameFailed`, `chatListDeleteFailed`, `chatListRename`, `chatListRenameField`, `chatListDelete`, `chatListDeleteTitle`, `chatListDeleteConfirm`, `chatListDeleteCancel`. Функции от названия чата: `chatListRowMenu(title)` («Действия с чатом …») и `chatListDeleteBody(title)`.
|
|
1373
|
+
Было: `const labels: ChatLabels = { …прежние надписи }`. Стало: те же надписи плюс 16 новых; проще всего `{ ...russianLabels, …свои }`.
|
|
1374
|
+
|
|
1375
|
+
Чтобы включить список чатов:
|
|
1376
|
+
|
|
1377
|
+
1. Сервер: `createChatHandler({ …, chatList: true })`. Базы SQLite и PostgreSQL обновятся сами при открытии.
|
|
1378
|
+
2. Браузер: вместо `<AgentChat api chatId … />` — `<ChatWorkspace api="/api/chat" skin={tandemSkin} … />` (те же остальные свойства, кроме `chatId`). Открытый чат приложение может показывать в адресе: `initialChatId` и `onChatChange`.
|
|
1379
|
+
3. Если скин свой: добавьте `ChatWorkspace` и `ChatList` по договору версии 7 (`ChatListSkin`). Скин Тандема обновляется вместе с пакетом.
|
|
1380
|
+
4. Если приложение само создавало чат кнопкой «Новый чат», уберите её: «Новый чат» теперь в списке.
|
|
1381
|
+
5. Проверка: `pnpm typecheck` (ловит недостающие методы хранилищ и надписи); в списке видны чаты пользователя, «Новый чат» открывает пустой чат, после первого сообщения чат получает название, переименование и удаление (с подтверждением) работают.
|
|
1382
|
+
|
|
1383
|
+
## 0.7.0
|
|
1384
|
+
|
|
1385
|
+
### Minor Changes
|
|
1386
|
+
|
|
1387
|
+
- b8f9e9f: **Ломает приложения со своими хранилищами и своими скинами:** ответ агента можно переделать, а отправленное сообщение исправить; все варианты сохраняются, между ними переключает «‹ 2/3 ›».
|
|
1388
|
+
|
|
1389
|
+
Что изменилось:
|
|
1390
|
+
|
|
1391
|
+
- Под ответом агента появилась кнопка «Переделать ответ»: агент отвечает на тот же вопрос заново, прежний ответ остаётся вариантом. Под сообщением собеседника появился карандаш «Изменить сообщение»: правка идёт на месте пузыря, отправляется новой веткой с новым ответом агента.
|
|
1392
|
+
- Когда у сообщения больше одного варианта, рядом стоит переключатель «‹ 2/3 ›». Выбранный вариант запоминается на сервере: после обновления страницы или на другом устройстве открывается он же.
|
|
1393
|
+
- Ничего не удаляется и не переписывается: журнал переписки только дописывается, выбор варианта — отдельная запись в новой таблице `branch_selections`.
|
|
1394
|
+
- Модель видит только текущую ветку: после правки она не знает прежнего текста, после переключения на прежний вариант — знает. Инструкция и инструменты в запросе не меняются, кэш модели ветки не задевают. Сжатие истории и поиск по старой переписке работают по текущей ветке (сводка действует на ветке, где она сделана).
|
|
1395
|
+
- Правка сообщения с файлами (пакет вложений): файлы остаются как были и переходят в новый вариант, меняется только текст; убрать или добавить файл при правке нельзя. Сообщение из одних файлов можно отправить и с пустым текстом. Файлы проходят ту же проверку сервера, что и при обычной отправке.
|
|
1396
|
+
- Пока идёт ответ, переключать, переделывать и править нельзя: сервер отвечает 409 `busy`, кнопки в экране неактивны. Если отказ всё же пришёл (ответ пошёл с другой вкладки), поле правки остаётся открытым с текстом собеседника и понятной ошибкой: текст не пропадает.
|
|
1397
|
+
- Новые адреса в `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>`: правка началась, не началась или сервер отказал).
|
|
1398
|
+
- Базы SQLite (миграция 0004) и PostgreSQL (миграция 0003) получают новую таблицу сами при первом запуске. Ничего запускать вручную не нужно.
|
|
1399
|
+
- В скине Тандема строка с карандашом и переключателем занимает прежний промежуток между сообщениями: высота сообщений не меняется.
|
|
1400
|
+
|
|
1401
|
+
### Как обновить приложение
|
|
1402
|
+
|
|
1403
|
+
1. Обновите все пакеты SDK одной командой: `pnpm update "@ykurilov/agent-chat-*" --latest`. Базы обновятся сами при первом запуске сервера.
|
|
1404
|
+
2. **Если у приложения своё хранилище** (свой класс, реализующий `ChatStorage`): добавьте два метода и таблицу под них.
|
|
1405
|
+
|
|
1406
|
+
```ts
|
|
1407
|
+
insertBranchSelection(record: StoredBranchSelection): Promise<void>; // дописать запись «выбран этот вариант»
|
|
1408
|
+
listBranchSelections(chatId: string): Promise<StoredBranchSelection[]>; // записи чата по порядку добавления
|
|
1409
|
+
```
|
|
1410
|
+
|
|
1411
|
+
`StoredBranchSelection` — `{ id, chatId, messageId, selectedAt }` (оба времени — строка ISO). Записи только добавляются, не меняются и не удаляются. Тип и общий тест `describeChatStorageContract` берутся из `@ykurilov/agent-chat-core`; тест теперь проверяет и эти методы. Приложения на памяти, SQLite и PostgreSQL из наших пакетов ничего не меняют.
|
|
1412
|
+
3. **Если у приложения свой скин** (объект `ChatSkin` не из `@ykurilov/agent-chat-skin-tandem`): договор скина теперь версии 6 (версия 5 — голосовой ввод и вложения, необязательные `Composer.voice`, `Composer.attachments`, `UserMessage.attachments`). Добавьте два компонента и новые свойства сообщений.
|
|
1413
|
+
|
|
1414
|
+
```tsx
|
|
1415
|
+
const BranchSwitcher: ChatSkin["BranchSwitcher"] = ({ position, count, onPrevious, onNext }) => {
|
|
1416
|
+
const labels = useChatLabels();
|
|
1417
|
+
return (
|
|
1418
|
+
<div role="group" aria-label={labels.branchPosition(position, count)}>
|
|
1419
|
+
<button type="button" aria-label={labels.previousBranch} disabled={!onPrevious} onClick={onPrevious}>‹</button>
|
|
1420
|
+
{position}/{count}
|
|
1421
|
+
<button type="button" aria-label={labels.nextBranch} disabled={!onNext} onClick={onNext}>›</button>
|
|
1422
|
+
</div>
|
|
1423
|
+
);
|
|
1424
|
+
};
|
|
1425
|
+
const MessageEditor: ChatSkin["MessageEditor"] = ({ value, onValueChange, onSave, onCancel, canSave, error, attachments }) => {
|
|
1426
|
+
const labels = useChatLabels();
|
|
1427
|
+
return (
|
|
1428
|
+
<div>
|
|
1429
|
+
{attachments && <ul aria-label={attachments.label}>{attachments.items.map((item) => <li key={item.id}>{item.name}</li>)}</ul>}
|
|
1430
|
+
<textarea aria-label={labels.editMessage} value={value} onChange={(event) => onValueChange(event.target.value)} />
|
|
1431
|
+
{error && <p role="alert">{error}</p>}
|
|
1432
|
+
<button type="button" onClick={onCancel}>{labels.cancelEdit}</button>
|
|
1433
|
+
<button type="button" disabled={!canSave} onClick={onSave}>{labels.saveEdit}</button>
|
|
1434
|
+
</div>
|
|
1435
|
+
);
|
|
1436
|
+
};
|
|
1437
|
+
const skin = { ...mySkin, BranchSwitcher, MessageEditor };
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
`UserMessage` получает свойства `branchSwitcher?: ReactNode` (положить рядом с сообщением) и `onEdit?: () => void` (кнопка «Изменить сообщение»; когда правка недоступна, свойства нет). `AgentMessage` получает `branchSwitcher?: ReactNode` и `onRegenerate?: () => void` (кнопка «Переделать ответ», только у последнего законченного ответа). Свойства необязательны: скин без них не ломается, только не показывает кнопок. Пример — `skins/tandem/src` в репозитории SDK.
|
|
1441
|
+
4. **Если словарь надписей написан целиком**, а не через `{ ...russianLabels, … }`: добавьте ключи `previousBranch` («Предыдущий вариант»), `nextBranch` («Следующий вариант»), `branchPosition(position, count)` («Вариант 2 из 3»), `regenerate` («Переделать ответ»), `editMessage` («Изменить сообщение»), `saveEdit` («Отправить»), `cancelEdit` («Отмена»), `editErrors` — `{ busy: "Ответ ещё идёт. Дождитесь его конца и отправьте снова", offline: "Нет связи с сервером. Текст сохранён в поле", rejected: "Сервер не принял правку. Текст сохранён в поле" }`.
|
|
1442
|
+
5. **Если приложение само собирает ленту через `useAgentChat`**: подключите `forks` и `selectBranch`, как в `packages/react/src/agent-chat.tsx`. Готовый `AgentChat` ничего менять не просит.
|
|
1443
|
+
6. Проверка: `pnpm typecheck` проходит; отправьте сообщение, нажмите «Переделать ответ» — под ответом появится «2/2», стрелка «‹» вернёт первый вариант, после обновления страницы выбор сохранится. Подробности — раздел «Ветки ответов и правка сообщений» в `docs/getting-started.md`.
|
|
1444
|
+
- 7c205c4: Новый пакет `@ykurilov/agent-chat-attachments`: вложения к сообщению. В поле ввода появляются скрепка, перетаскивание и вставка файлов из буфера, превью с удалением; в ленте у сообщения — миниатюры картинок и плашки файлов. Картинки, PDF и текстовые файлы; тип определяется по содержимому.
|
|
1445
|
+
|
|
1446
|
+
Что изменилось:
|
|
1447
|
+
|
|
1448
|
+
- `@ykurilov/agent-chat-attachments`: `createAttachments({ storage, limits? })`, хранилища файлов `createDiskAttachmentStorage({ directory })` и `createMemoryAttachmentStorage()`, `attachmentsInstructions`, общий набор проверок своего хранилища `describeAttachmentStorageContract` (подпуть `/storage-contract`). Подпуть `/react`: `attachmentsClient`. Пределы по умолчанию: 10 файлов, 20 МБ на файл, потолок извлечённого текста; меняются в `limits`.
|
|
1449
|
+
- Картинки уходят только моделям с `acceptsImages`; если модель не видит, человек узнаёт об этом до отправки. Текст PDF и текстовых файлов достаётся на сервере и идёт любой модели. Кэш модели не страдает: извлечённый текст не меняется от хода к ходу.
|
|
1450
|
+
- `@ykurilov/agent-chat-core`: `createChatHandler`/`createChatService` принимают необязательное `attachments`; меню моделей несёт `acceptsImages`; задание сжатия просит назвать прикреплённые файлы в сводке (оригиналы остаются). Без `attachments` файлы в сообщении отклоняются.
|
|
1451
|
+
- `@ykurilov/agent-chat-server`: новые адреса под адресом чата: `GET /attachments/settings`, `POST /:chatId/attachments`, `GET` и `DELETE /:chatId/attachments/:id`.
|
|
1452
|
+
- `@ykurilov/agent-chat-react`: `AgentChat` принимает необязательное `attachments`. Договор скина версии 5: `Composer` и `UserMessage` получают необязательное `attachments`, у моделей в `ModelChoiceView` появилось необязательное `acceptsImages`.
|
|
1453
|
+
- `@ykurilov/agent-chat-skin-tandem`: скрепка, превью, миниатюры и плашки файлов. Встроенный микрофон полного поля Тандема спрятан: голос — отдельная кнопка (пакет `voice`).
|
|
1454
|
+
- Разбор PDF идёт в отдельном потоке с потолком памяти и сроком: зависший или «раздутый» файл обрывается, сервер живёт дальше; текст собирается до потолка символов.
|
|
1455
|
+
- Лимиты на человека: `maxUnsentFiles` (30), `maxUnsentBytes` (200 МБ), `maxParallelUploads` (8), `maxParallelUploadsPerUser` (3); отказы 429 `too_many_unsent` и `busy`. Файлы с ошибкой в лимит не входят. Тело загрузки читается потоком без лишних копий. Поле ввода грузит файлы по два одновременно, остальные ждут в очереди, а на отказ `busy` повторяет несколько раз.
|
|
1456
|
+
- Диск чистит недописанные остатки (`.tmp` и `.bin` без описания) по возрасту; неотправленные файлы убираются через 24 часа, прикреплённые не трогаются. Просроченный файл возвращается в поле с пометкой «загрузите снова».
|
|
1457
|
+
- Файл помечается «прикреплён» только после записи сообщения (`confirmUserParts` в контракте `MessageAttachments`; `acceptUserParts` файлы не помечает). Отказ отправки возвращает текст и файлы в поле, повторный клик не очищает его раньше времени. Контракт `AttachmentsController` в `@ykurilov/agent-chat-react`: вместо `takeFiles` — `readFiles`, `markSent`, `restoreFiles`; `useAgentChat.submit` возвращает признак «принято».
|
|
1458
|
+
- Текстовые логи с цветовыми кодами терминала (ANSI) принимаются как текст, коды убираются. Закрывающий тег `</attachment>` в тексте файла обезвреживается без учёта регистра.
|
|
1459
|
+
- `MessageAttachments.instructions`: ядро само дописывает инструкцию про вложения к инструкции агента (один раз, дубль не появляется). Вручную `attachmentsInstructions` добавлять больше не нужно, но и не вредно.
|
|
1460
|
+
- `@ykurilov/agent-chat-skin-tandem`: пока грузится список моделей, в поле со вложениями нет пустой кнопки «Выберите модель».
|
|
1461
|
+
- Схема базы чата не меняется, миграции не нужны.
|
|
1462
|
+
|
|
1463
|
+
### Как обновить приложение
|
|
1464
|
+
|
|
1465
|
+
Если вложения не нужны, обновление ничего не требует: без `attachments` чат работает как раньше.
|
|
1466
|
+
|
|
1467
|
+
Чтобы включить вложения:
|
|
1468
|
+
|
|
1469
|
+
1. `pnpm add @ykurilov/agent-chat-attachments`.
|
|
1470
|
+
2. На сервере: `const attachments = createAttachments({ storage: createDiskAttachmentStorage({ directory: "./data/attachments" }) })`, передайте `attachments` в `createChatHandler` и инструкцию агенту про вложения ядро добавляет само. Папку с файлами включите в резервные копии вместе с базой.
|
|
1471
|
+
3. В браузере: `<AgentChat … attachments={attachmentsClient} />` (`attachmentsClient` из `@ykurilov/agent-chat-attachments/react`).
|
|
1472
|
+
4. Если скин свой: `Composer` получает `attachments` (скрепка, превью, `notice`), `UserMessage` — `attachments` (миниатюры и плашки). Нет свойства — ничего про вложения не рисовать. Свой скин на `@ykurilov/agent-chat-skin-tandem` обновляется вместе с пакетом.
|
|
1473
|
+
5. Проверка: `pnpm typecheck`; в поле ввода есть скрепка, картинка и PDF прикрепляются, в ленте видны миниатюра и плашка, агент отвечает по содержимому.
|
|
1474
|
+
- e741e6d: Новый пакет `@ykurilov/agent-chat-voice`: голосовой ввод через Soniox. В поле ввода появляется кнопка микрофона, слова видны по ходу речи, человек правит и отправляет сам. Главный ключ Soniox остаётся на сервере, браузеру выдаётся временный.
|
|
1475
|
+
|
|
1476
|
+
Что изменилось:
|
|
1477
|
+
|
|
1478
|
+
- `@ykurilov/agent-chat-voice/server`: `createVoiceHandler({ basePath, resolveUser })` — адрес `POST /api/voice/session`, ключ из `SONIOX_API_KEY`. Адреса Soniox настраиваются (`apiBaseUrl`, `apiHeaders`, `websocketUrl`) — для сервера в России.
|
|
1479
|
+
- `@ykurilov/agent-chat-voice`: `createVoiceInput({ api })` — микрофон и соединение с Soniox в браузере. Формат записи выбирается явно (webm/opus → ogg/opus → mp4/AAC).
|
|
1480
|
+
- Режим «звук через наш сервер»: `createVoiceInput({ api, mode: "relay" })` в браузере, `createVoiceRelay` в `…/voice/server` и `mountVoiceRelay` в новом подпути `…/voice/hono` (Hono на Node через `@hono/node-ws`). Нужен там, где Soniox недоступен из браузера; адрес и заголовки прокси настраиваются (`websocketUrl`, `upstreamHeaders`). Next.js WebSocket в route handlers не держит — нужен отдельный Node-сервер.
|
|
1481
|
+
- Ограничение частоты: `rateLimit` (по умолчанию 10 диктовок в минуту на человека, в памяти процесса; общие лимиты — на приложении). Ответ с ключом идёт с `Cache-Control: no-store`; исключение из `resolveUser` даёт 503 и запись в `logError`.
|
|
1482
|
+
- Предел диктовки 5 минут: запись останавливается за две секунды до него, человек видит причину.
|
|
1483
|
+
- Отправка сообщения во время диктовки останавливает диктовку.
|
|
1484
|
+
- Ручная правка посреди слова, которое Soniox ещё не закрепил, больше не портит текст: сказанное учитывается только по закреплённой части.
|
|
1485
|
+
- `AgentChat` принимает необязательное свойство `voice`; без него кнопки нет. Договор скина версии 5: `Composer` получает необязательное `voice` (`VoiceInputView`). Скин Тандема рисует кнопку сам (`TandemVoiceButton`).
|
|
1486
|
+
- В словаре надписей новые ключи: `voiceStart`, `voiceStop`, `voiceConnecting`, `voiceErrors` (виды ошибок, в том числе `rate_limited` и `time_limit`).
|
|
1487
|
+
- `mountChat` и `createNextRouteHandlers` теперь принимают любой объект с `basePath` и `fetch`, а не только обработчик чата — так подключается и голос.
|
|
1488
|
+
|
|
1489
|
+
### Как обновить приложение
|
|
1490
|
+
|
|
1491
|
+
Если голос не нужен, ничего делать не нужно, кроме одного случая:
|
|
1492
|
+
|
|
1493
|
+
1. **Только если словарь надписей написан целиком** (не через `{ ...russianLabels, … }`): добавьте ключи `voiceStart`, `voiceStop`, `voiceConnecting` и `voiceErrors` (по видам `permission_denied`, `no_microphone`, `unsupported`, `network`, `service_unavailable`, `rate_limited`, `time_limit`, `other`). Значения — в `russianLabels` из `@ykurilov/agent-chat-react`.
|
|
1494
|
+
|
|
1495
|
+
Чтобы включить голос:
|
|
1496
|
+
|
|
1497
|
+
1. `pnpm add @ykurilov/agent-chat-voice`, ключ Soniox положите в окружение сервера как `SONIOX_API_KEY`.
|
|
1498
|
+
2. На сервере рядом с чатом: `mountChat(app, createVoiceHandler({ basePath: "/api/voice", resolveUser }))` (в Next.js — второй файл маршрута `app/api/voice/[[...path]]/route.ts`).
|
|
1499
|
+
3. В браузере: `<AgentChat … voice={createVoiceInput({ api: "/api/voice" })} />`, объект создаётся один раз.
|
|
1500
|
+
4. Если скин свой: `Composer` получает `voice`; нет его — кнопку не рисовать.
|
|
1501
|
+
5. Проверка: кнопка «Диктовать» есть в поле ввода, слова появляются по ходу речи.
|
|
1502
|
+
|
|
1503
|
+
## 0.6.0
|
|
1504
|
+
|
|
1505
|
+
### Minor Changes
|
|
1506
|
+
|
|
1507
|
+
- 5252793: **Ломает приложения со своими хранилищами и своими скинами:** длинная переписка теперь сжимается сама, а у агента появился инструмент «поиск по старой переписке».
|
|
1508
|
+
|
|
1509
|
+
Что изменилось:
|
|
1510
|
+
|
|
1511
|
+
- Когда модель видит больше 200 тысяч токенов (у моделей с маленьким окном — 80% окна), ядро в фоне после ответа заменяет старую часть переписки короткой сводкой и оставляет свежие ~20 тысяч токенов как есть. Сообщения из базы не удаляются. В ленте на месте сжатия появляется строка «История сжата», по клику открывается сводка. Сжатие включено по умолчанию; `compaction: false` в `createChatHandler` (или `createChatService`) его выключает, `compaction: { thresholdTokens, tailTokens, summaryCeilingTokens, model, instruction }` настраивает.
|
|
1512
|
+
- Новый пакет `@ykurilov/agent-chat-history-search`: инструмент `search_earlier_conversation`, который ищет точные детали в оригиналах старой переписки. Подключается по желанию.
|
|
1513
|
+
- Новый адрес `GET /api/chat/:id/compactions` (в `createChatHandler`), новый метод службы `compactNow`, набор инструментов агента получает `chatId` хода.
|
|
1514
|
+
- «Стоп» обрывает ожидание сжатия сразу (само сжатие доделывается в фоне). `chat.flush()` при остановке сервера ждёт идущие сжатия до `compaction.flushWaitMs` (30 секунд по умолчанию) и отменяет остальные без записи полусводки.
|
|
1515
|
+
- Разделитель появляется без перезагрузки надёжно: сервер ставит признак «идёт сжатие» до конца потока ответа, а экран опрашивает `/compactions`, пока признак стоит (до 12 минут). `/compactions` стал лёгким: читает связи сообщений, а не тексты с выводами инструментов.
|
|
1516
|
+
- Сводку пишет самый малый уровень рассуждений модели (или без них, где можно), а потолок вывода имеет запас, так что рассуждения не обрывают сводку по длине. Пауза после сбоя теперь действует и на исход «нечего сжимать».
|
|
1517
|
+
- Поиск нормализует текст каждого сообщения один раз за вызов.
|
|
1518
|
+
- Дедупликация сжатий живёт в памяти процесса, как и реестр идущих ответов: при нескольких копиях сервера возможно двойное сжатие и двойной расход токенов.
|
|
1519
|
+
- Базы SQLite и PostgreSQL получают новую таблицу и новые поля сами при первом запуске. Ничего запускать вручную не нужно.
|
|
1520
|
+
|
|
1521
|
+
### Как обновить приложение
|
|
1522
|
+
|
|
1523
|
+
1. Обновите все пакеты SDK одной командой: `pnpm update "@ykurilov/agent-chat-*" --latest`. Базы обновятся сами при первом запуске сервера.
|
|
1524
|
+
2. **Если у приложения своё хранилище** (свой класс, реализующий `ChatStorage`): добавьте три метода.
|
|
1525
|
+
|
|
1526
|
+
```ts
|
|
1527
|
+
insertCompaction(record: StoredCompaction): Promise<void>; // записать отметку сжатия
|
|
1528
|
+
listCompactions(chatId: string): Promise<StoredCompaction[]>; // отметки чата по порядку добавления
|
|
1529
|
+
listMessageLinks(chatId: string): Promise<MessageLink[]>; // { id, parentId, createdAt } сообщений чата, в порядке listMessages
|
|
1530
|
+
```
|
|
1531
|
+
|
|
1532
|
+
Типы `StoredCompaction`, `MessageLink` и общий тест `describeChatStorageContract` берутся из `@ykurilov/agent-chat-core`; тест теперь проверяет и эти методы.
|
|
1533
|
+
Приложения на памяти, SQLite и PostgreSQL из наших пакетов ничего не меняют.
|
|
1534
|
+
3. **Если у приложения свой скин** (объект `ChatSkin` не из `@ykurilov/agent-chat-skin-tandem`): добавьте компонент `CompactionDivider`, договор скина теперь версии 4.
|
|
1535
|
+
|
|
1536
|
+
```tsx
|
|
1537
|
+
const CompactionDivider: ChatSkin["CompactionDivider"] = ({ summary, open, onOpenChange }) => {
|
|
1538
|
+
const labels = useChatLabels();
|
|
1539
|
+
return (
|
|
1540
|
+
<div>
|
|
1541
|
+
<button type="button" aria-expanded={open} onClick={() => onOpenChange(!open)}>
|
|
1542
|
+
{labels.historyCompacted}
|
|
1543
|
+
</button>
|
|
1544
|
+
{open && (
|
|
1545
|
+
<div>
|
|
1546
|
+
<p>{labels.historyCompactedHint}</p>
|
|
1547
|
+
<pre>{summary}</pre>
|
|
1548
|
+
</div>
|
|
1549
|
+
)}
|
|
1550
|
+
</div>
|
|
1551
|
+
);
|
|
1552
|
+
};
|
|
1553
|
+
const skin = { ...mySkin, CompactionDivider };
|
|
1554
|
+
```
|
|
1555
|
+
|
|
1556
|
+
`summary` — сводка в Markdown; свёрнутый разделитель — строка «История сжата». Скин Тандема обновляется вместе с пакетом.
|
|
1557
|
+
4. **Если словарь надписей написан целиком**, а не через `{ ...russianLabels, … }`: добавьте ключи `historyCompacted` («История сжата») и `historyCompactedHint`.
|
|
1558
|
+
5. Чтобы агент мог искать в сжатой переписке (по желанию): `pnpm add @ykurilov/agent-chat-history-search`, затем в `agent` обработчика добавьте `historySearchInstructions` в `instructions` и `...historySearch(model)` в `tools`; шаги и код — в разделе «Сжатие истории и поиск по старой переписке» файла `docs/getting-started.md`.
|
|
1559
|
+
6. Проверка: `pnpm typecheck` проходит, чат открывается и отвечает. Кэш модели после первого сжатия обнуляется один раз (запрос после него дороже обычного), это ожидаемо.
|
|
1560
|
+
|
|
1561
|
+
## 0.5.0
|
|
1562
|
+
|
|
1563
|
+
### Minor Changes
|
|
1564
|
+
|
|
1565
|
+
- 62027c6: Ломает приложения: в поле ввода появился выбор модели и уровня рассуждений, поэтому «какой моделью отвечать» теперь решает не сервер приложения на каждый ход, а пользователь в чате. Настройка `resolveModel` заменена на `defaultChoice` (модель для нового чата).
|
|
1566
|
+
|
|
1567
|
+
Что нового простыми словами:
|
|
1568
|
+
|
|
1569
|
+
- Стандартный набор моделей: DeepSeek 4.1 Flash, GLM 5.3 Flash, GPT 6 Luna, Gemini 3.8 Flash. Провайдеры: Fireworks, DeepSeek, Together и новый OpenRouter (ключ `OPENROUTER_API_KEY`, среди хозяев модели выбирается самый быстрый).
|
|
1570
|
+
- В поле ввода одно меню моделей (провайдер → модель) и рядом выбор уровня рассуждений. Уровни свои у каждой модели: у DeepSeek «Выкл/Вкл», у GLM от «Низкий», у GPT и Gemini несколько уровней. У GPT рассуждения приходят зашифрованными: человеку виден только пересказ. На телефоне меню плоское («модель · провайдер»).
|
|
1571
|
+
- Выбор запоминается в чате. Модель можно сменить посреди чата: рассуждения прежней модели в запрос новой не уходят, но в базе и на экране остаются. Если сохранённой модели больше нет в каталоге, чат открывается на выборе по умолчанию, сообщения не меняются.
|
|
1572
|
+
- `extendCatalog` умеет больше: `removeProviders` (провайдер уходит, модель без провайдеров уходит вместе с ним) и `pinModelVersions` (закрепить версию модели у провайдера: `{ "gpt-6-luna": { openrouter: "openai/gpt-6-luna-2026-09-30" } }`). Ошибочная правка — ошибка при создании каталога.
|
|
1573
|
+
- Новые адреса сервера: `GET /api/chat/models` (список для меню, без ключей) и `PUT /api/chat/:id/model` (записать выбор чата).
|
|
1574
|
+
- Скин Тандема рисует `Composer` варианта `extended`. Договор скина стал версии 3: `Composer` получает необязательное `modelChoice`; свой скин без него работает как раньше. В `@ykurilov/agent-chat-react` есть хук `useModelChoice`.
|
|
1575
|
+
- Кэш: у Gemini через OpenRouter метка кэша теперь стоит и на последнем сообщении истории (скользящая точка, на следующем ходе переезжает на новое), поэтому кэшируется вся переписка, а не только инструкция. Для своего провайдера то же задаётся полем `lastMessageProviderOptions` в `requestOptions`.
|
|
1576
|
+
- Профиль модели получил отметку `toolResultImageProviders`: провайдеры, через которых модель видит картинку в результате инструмента (по живым замерам). У GPT 6 Luna и Gemini 3.8 Flash это `["openrouter"]`, у GLM 5.3 Flash и DeepSeek 4.1 Flash отметки нет. Ядро отметку не читает; по ней веб-инструменты решают, выдавать ли `page_screenshot`.
|
|
1577
|
+
- Если ключа выбранного провайдера временно нет, ответ идёт на запасной модели, но выбор пользователя в чате не перезаписывается: когда ключ вернётся, чат снова отвечает выбранной моделью.
|
|
1578
|
+
- Хранилища: у чата новое поле `reasoning` (уровень рассуждений). SQLite: колонка `chats.reasoning`, PostgreSQL: колонка `agent_chat.chats.reasoning`; миграции применяются сами при открытии базы, данные не теряются.
|
|
1579
|
+
|
|
1580
|
+
### Как обновить приложение
|
|
1581
|
+
|
|
1582
|
+
1. Сервер (там, где `createChatHandler` или `createChatService`). Ищите `resolveModel`.
|
|
1583
|
+
|
|
1584
|
+
Было:
|
|
1585
|
+
|
|
1586
|
+
```ts
|
|
1587
|
+
createChatHandler({
|
|
1588
|
+
// …
|
|
1589
|
+
resolveModel: async () => ({ modelId: "deepseek-4.1-flash", providerId: "deepseek" }),
|
|
1590
|
+
catalog: standardCatalog,
|
|
1591
|
+
});
|
|
1592
|
+
```
|
|
1593
|
+
|
|
1594
|
+
Стало:
|
|
1595
|
+
|
|
1596
|
+
```ts
|
|
1597
|
+
createChatHandler({
|
|
1598
|
+
// …
|
|
1599
|
+
// Только для нового чата; дальше модель выбирает пользователь. Можно не задавать:
|
|
1600
|
+
// тогда берётся первая модель каталога у первого провайдера с ключом.
|
|
1601
|
+
defaultChoice: { modelId: "deepseek-4.1-flash", providerId: "deepseek" },
|
|
1602
|
+
catalog: standardCatalog,
|
|
1603
|
+
});
|
|
1604
|
+
```
|
|
1605
|
+
|
|
1606
|
+
Раньше `resolveModel` мог выбирать модель по пользователю или чату. Теперь это делает пользователь в меню; ограничить выбор можно каталогом (`extendCatalog(standardCatalog, { removeProviders, removeModels, addModels, addProviders, pinModelVersions })`) и ключами на сервере: в меню попадают только провайдеры, чей ключ есть в окружении.
|
|
1607
|
+
2. Ключи. Хотите GPT 6 Luna, Gemini 3.8 Flash или GLM через OpenRouter — добавьте `OPENROUTER_API_KEY` в окружение сервера. Без него этих моделей в меню не будет; остальное работает как раньше.
|
|
1608
|
+
3. Свой словарь надписей. Если вы задали `ChatLabelsProvider labels={…}` не через `{ ...russianLabels, … }`, добавьте новые ключи: `reasoningLevels` (названия уровней: `off`, `on`, `minimal`, `low`, `medium`, `high`, `max`), `modelChoiceFailed` («Не удалось сменить модель») и `modelMenuFailed` («Список моделей не загрузился»). Через `{ ...russianLabels, … }` ничего делать не нужно.
|
|
1609
|
+
4. Свой скин. Договор версии 3: `Composer` получает необязательное `modelChoice` (`providers`, `current`, `onChange`, `reasoningLevels`, `reasoning`, `onReasoningChange`, `error`). Не рисуете его — выбора модели в чате не будет, всё остальное работает. Подробно: `specs/003-models-and-picker/contracts/skin.md`. Скин Тандема (`tandemSkin`) обновляется вместе с пакетом, править нечего.
|
|
1610
|
+
5. Своё хранилище (только если вы реализовали `ChatStorage` сами). Добавьте необязательное поле `reasoning?: string` в чат: `createChat` принимает и сохраняет его, `getChat` отдаёт (без значения — поля нет), `updateChatModel(chatId, { modelId, providerId, reasoning? })` записывает его целиком и без `reasoning` стирает сохранённый уровень. Наборы проверок `describeChatStorageContract` из `@ykurilov/agent-chat-core/storage-contract` проверяют это. Хранилища SQLite и PostgreSQL из SDK обновляются сами.
|
|
1611
|
+
6. Проверка: `pnpm typecheck` проходит; в браузере в поле ввода есть меню моделей и выбор рассуждений; выбор остаётся после обновления страницы; сообщение отправляется и ответ приходит по частям.
|
|
1612
|
+
7. Только если вы уже ставили `@ykurilov/agent-chat-web-tools` из main до этого выпуска: опция `screenshotProviders` убрана. Снимок страницы выдаётся по отметке `toolResultImageProviders` в профиле модели.
|
|
1613
|
+
|
|
1614
|
+
Было:
|
|
1615
|
+
|
|
1616
|
+
```ts
|
|
1617
|
+
const webTools = createWebTools({ ...firecrawl, screenshotProviders: ["openrouter"] });
|
|
1618
|
+
```
|
|
1619
|
+
|
|
1620
|
+
Стало:
|
|
1621
|
+
|
|
1622
|
+
```ts
|
|
1623
|
+
const webTools = createWebTools(firecrawl);
|
|
1624
|
+
// Своя модель, у которой картинка из инструмента доходит через провайдера: добавьте отметку в профиль.
|
|
1625
|
+
// extendCatalog(standardCatalog, { addModels: [{ ...профиль, toolResultImageProviders: ["openrouter"] }] })
|
|
1626
|
+
```
|
|
1627
|
+
- 2deeab3: Веб-инструменты стали строже к адресам, а лента показывает их результаты удобнее.
|
|
1628
|
+
|
|
1629
|
+
- Скачивание снимка страницы (его делает сервер приложения) больше не идёт по переходам вслепую: переходы выполняются вручную, не больше трёх, и каждый проходит проверку адреса. Имя дополнительно разрешается по DNS: если хоть один адрес частный, снимок не скачивается.
|
|
1630
|
+
- Проверка адреса закрывает IPv4 внутри IPv6 (`[::7f00:1]`, `::ffff:`, `64:ff9b::`), `fec0::/10`, `100.64.0.0/10`, `192.0.0.0/24`, `198.18.0.0/15` и служебные диапазоны. Для чтения страниц проверяется только адрес до вызова Firecrawl (README пакета описывает границы честно).
|
|
1631
|
+
- Снимки кэшируются в памяти сервера (до 20 штук, до 20 МБ, час): повторные ходы не качают одно и то же. Новая необязательная опция `resolveHost` у `createWebTools` (по умолчанию системный DNS).
|
|
1632
|
+
- В результаты поиска и чтения попадают только адреса `http(s)`.
|
|
1633
|
+
- Скин Тандем: ссылки в развёрнутой строке веб-инструмента кликабельны (только `http(s)`, новая вкладка, `rel="noopener noreferrer"`), снимок страницы рисуется картинкой (ленивая загрузка, ограниченный размер; при ошибке — текст и ссылка на страницу).
|
|
1634
|
+
- В словарь `ChatLabels` добавлены два ключа: `screenshotAlt` и `screenshotUnavailable`.
|
|
1635
|
+
|
|
1636
|
+
### Как обновить приложение
|
|
1637
|
+
|
|
1638
|
+
1. Поднимите все пакеты `@ykurilov/agent-chat-*` до одной версии.
|
|
1639
|
+
2. Если приложение задаёт словарь надписей целиком, а не поверх `russianLabels`, добавьте в него `screenshotAlt` («Снимок страницы») и `screenshotUnavailable` («Снимок не загрузился.»); иначе `pnpm typecheck` покажет ошибку. Словарь вида `{ ...russianLabels, … }` ничего не требует.
|
|
1640
|
+
3. Если своя строка вызова (`ToolCallRow`) показывает результат веб-инструментов, при желании сделайте так же: ссылки только `http(s)` с `rel="noopener noreferrer"`, снимок картинкой с запасным текстом. Готовый пример — `skins/tandem/src/tool-call-row.tsx`.
|
|
1641
|
+
4. Для сервера, у которого во внутренней сети есть что-то ценное, запретите исходящие соединения в частные сети на уровне сети: окно DNS rebinding между проверкой и запросом остаётся.
|
|
1642
|
+
|
|
1643
|
+
## 0.4.0
|
|
1644
|
+
|
|
1645
|
+
### Patch Changes
|
|
1646
|
+
|
|
1647
|
+
- 2e526d2: Чат стал меньше перерисовываться: ввод в поле больше не перерисовывает ленту сообщений, а в конце ответа пропал лишний кадр. Внутри кусок ответа теперь проходит один слой обработки вместо двух. Обновление ничего не требует.
|
|
1648
|
+
|
|
1649
|
+
## 0.3.0
|
|
1650
|
+
|
|
1651
|
+
No changes in this release.
|
|
1652
|
+
|
|
1653
|
+
## 0.2.0
|
|
1654
|
+
|
|
1655
|
+
### Minor Changes
|
|
1656
|
+
|
|
1657
|
+
- ea50099: Первый выпуск Agent Chat SDK на складе GitHub Packages. Все пять пакетов теперь выходят вместе и
|
|
1658
|
+
с одной версией: «SDK 0.2.0» значит, что каждый пакет `@ykurilov/agent-chat-*` имеет версию 0.2.0.
|
|
1659
|
+
|
|
1660
|
+
Что изменилось по сравнению с тем, как пакеты подключали раньше:
|
|
1661
|
+
|
|
1662
|
+
- Пакеты приходят собранными (JavaScript и типы), а не исходниками TypeScript. Голый `node`
|
|
1663
|
+
запускает серверные пакеты без `tsx`.
|
|
1664
|
+
- Скин Тандема (`@ykurilov/agent-chat-skin-tandem`) теперь тоже пакет. Раньше папку `skins/tandem/src`
|
|
1665
|
+
копировали в приложение; копию можно удалить и подключить пакет.
|
|
1666
|
+
- Дизайн-система Тандем ставится вместе со скином как `@ykurilov/tandem-ui` (в коде остаётся
|
|
1667
|
+
импорт `@tandem/ui`).
|
|
1668
|
+
|
|
1669
|
+
### Как обновить приложение
|
|
1670
|
+
|
|
1671
|
+
Если приложение подключало пакеты из этого репозитория без склада (ссылкой на папку или копией),
|
|
1672
|
+
переведите его на склад:
|
|
1673
|
+
|
|
1674
|
+
1. В корне приложения в `.npmrc` оставьте одну строку:
|
|
1675
|
+
|
|
1676
|
+
```ini
|
|
1677
|
+
@ykurilov:registry=https://npm.pkg.github.com
|
|
1678
|
+
```
|
|
1679
|
+
|
|
1680
|
+
2. Поставьте пакеты: `pnpm add @ykurilov/agent-chat-core @ykurilov/agent-chat-server @ykurilov/agent-chat-storage-sqlite @ykurilov/agent-chat-react`.
|
|
1681
|
+
3. Если использовали скин Тандема из копии:
|
|
1682
|
+
- удалите папку с копией скина (например, `src/chat-skin`);
|
|
1683
|
+
- `pnpm add @ykurilov/agent-chat-skin-tandem "@tandem/ui@npm:@ykurilov/tandem-ui@^0.3.0"`;
|
|
1684
|
+
- в коде замените импорт `./chat-skin` на `@ykurilov/agent-chat-skin-tandem`;
|
|
1685
|
+
- в главном CSS-файле замените `@source` на папку скина на
|
|
1686
|
+
`@source "../node_modules/@ykurilov/agent-chat-skin-tandem/dist";`
|
|
1687
|
+
(путь считайте от CSS-файла до корня приложения).
|
|
1688
|
+
4. Проверка: `pnpm typecheck`, сборка приложения, чат открывается и отвечает.
|