@ykurilov/agent-chat-core 0.80.1 → 0.81.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 +79 -0
- package/dist/{chunk-PORXDIWX.js → chunk-YGHGURNF.js} +26 -12
- package/dist/chunk-YGHGURNF.js.map +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/storage-contract.js +1 -1
- package/dist/types/agent-run/channel-notes.d.ts +14 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/notices/agent-notice.d.ts +12 -1
- package/package.json +1 -1
- package/dist/chunk-PORXDIWX.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# @ykurilov/agent-chat-core
|
|
2
2
|
|
|
3
|
+
## 0.81.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 58204d2: **Ломает приложения со своим хранилищем мессенджеров или сценариев.** Хранилища из пакетов (память, SQLite,
|
|
8
|
+
Postgres) уже готовы — если вы передаёте их, менять ничего не нужно.
|
|
9
|
+
|
|
10
|
+
- `MessengerStore`: новый обязательный метод `saveLinkIfFree` и признак `guarded: true`. Привязка аккаунта по коду
|
|
11
|
+
идёт только через `saveLinkIfFree`: два кода разных людей на один аккаунт мессенджера больше не переписывают друг
|
|
12
|
+
друга.
|
|
13
|
+
- `FlowStore`: новый обязательный метод `swapDelivery` и признак `guarded: true`.
|
|
14
|
+
- Без `guarded: true` `createMessengers` и `createAgentFlow` сразу бросают `TypeError`. Признак — обещание, что
|
|
15
|
+
хранилище выполняет условия держателя и аренды (список — в шаге 3 ниже).
|
|
16
|
+
|
|
17
|
+
Новое для своей доставки сценария (`attach({ deliver })`):
|
|
18
|
+
|
|
19
|
+
- Исход `{ status: "await_repair" }` — канал сломан и ждёт починки (например, отозван токен бота). Ответ ждёт,
|
|
20
|
+
попытка не тратится, новых ходов и касаний нет. Починили — `flow.resumeDelivery({ ownerId, chatId, messageId })`:
|
|
21
|
+
тот же `messageId`, текст и части уходят ближайшим обходом таймеров. Ответ: `resumed`, `pending`, `busy`,
|
|
22
|
+
`not_found` или `dropped` (человек уже написал после ответа — ответ снят и помечен недоставленным). Ожидание
|
|
23
|
+
переживает перезапуск. В `reportDelivery` этот исход — `TypeError`.
|
|
24
|
+
- Пока идёт `deliver`, аренда чата продлевается: вторая копия сервера ту же попытку не начинает. Копия упала посреди
|
|
25
|
+
отправки — та же попытка повторяется через её паузу. Итог устаревшей копии не пишется.
|
|
26
|
+
- Перед самым `deliver`, уже под резервом, SDK ещё раз сверяет: разговор не закончен после хода (`flow.end`), ответ
|
|
27
|
+
есть в ветке, после починки человек не написал после него. Иначе `deliver` не зовётся, ответ помечен недоставленным.
|
|
28
|
+
- Повтор доставки прощания у законченного разговора теперь доходит — без нового хода и без открытия разговора.
|
|
29
|
+
- «Ровно один раз» снаружи SDK не обещает: после падения копии тот же ответ может уйти ещё раз с тем же `messageId`.
|
|
30
|
+
Двойную отправку отсекает приложение по `messageId`.
|
|
31
|
+
|
|
32
|
+
Остальные исправления аудита работают без правок в приложении:
|
|
33
|
+
|
|
34
|
+
- Ход группы мессенджера остаётся групповым и тогда, когда агент пишет первым, когда человек правит сообщение
|
|
35
|
+
группы, шлёт поправку к ходу или нажимает кнопку-подсказку: без памяти и личных блоков владельца, только
|
|
36
|
+
разрешённые группе инструменты, с пределом сообщений участника.
|
|
37
|
+
- Удаление чата ждёт хода на любой копии сервера: идёт ход — `ChatBusyError`, чат цел.
|
|
38
|
+
- Не удалось отметить вложения сообщения прикреплёнными — ошибка доходит до вызывающего, сообщение не записывается,
|
|
39
|
+
место в счёте сообщений отдаётся назад; повтор того же сообщения проходит.
|
|
40
|
+
- Сценарий, который не сходится с подтверждениями приложения (`confirmations`, `exitGuards`), проверяется до записи
|
|
41
|
+
версии и закрепления чата: чат идёт без сценария, перевод чатов на такую версию — отказ.
|
|
42
|
+
- Копия сервера, у которой забрали аренду таймера сценария или обновления мессенджера, больше не шлёт касание и не
|
|
43
|
+
переписывает срок и итог другой копии.
|
|
44
|
+
|
|
45
|
+
### Как обновить приложение
|
|
46
|
+
|
|
47
|
+
Нужно только если приложение передаёт в `createMessengers` или `createAgentFlow` своё хранилище (не
|
|
48
|
+
`createMemoryMessengerStore`, `createSqliteMessengerStore`, `createPostgresMessengerStore`, `memoryFlowStore`,
|
|
49
|
+
`sqliteFlowStore`, `postgresFlowStore`).
|
|
50
|
+
|
|
51
|
+
1. Найдите свою реализацию `MessengerStore` (поиск по `MessengerStore` или `saveLink(`).
|
|
52
|
+
2. Добавьте метод — одной атомарной операцией базы:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
async saveLinkIfFree(link) {
|
|
56
|
+
// Аккаунт свободен или уже за link.userId — записать (имя и время обновить) и вернуть "saved".
|
|
57
|
+
// Аккаунт за другим пользователем — ничего не менять и вернуть "taken".
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
В SQL это `insert … on conflict (messenger, bot_id, external_user_id) do update set … where user_id = $userId
|
|
62
|
+
returning …`: строка вернулась — `"saved"`, нет — `"taken"`. Отдельные «найти, потом записать» не годятся: между
|
|
63
|
+
ними успевает второй код.
|
|
64
|
+
3. Добавьте признак `guarded: true` в объект хранилища. Он обещает:
|
|
65
|
+
- `finishUpdate(key, status, now, holder)`, `renewUpdates` и `releaseUpdate` пишут, только пока обновление за этим
|
|
66
|
+
держателем; `acquireLease` не отдаёт чужую живую аренду;
|
|
67
|
+
- `saveLinkIfFree` — одна атомарная операция.
|
|
68
|
+
4. Своё `FlowStore` (если есть): метод `swapDelivery(chatId, { expected, next, due, claim, freeAt, endedAt })` одной
|
|
69
|
+
транзакцией. Заменить запись доставки, только если сейчас там `expected` (совпали `messageId`, `attempt` и `token`;
|
|
70
|
+
`null` — записи нет). С `claim` — только если аренда чата свободна, уже у `claim.owner` или у `claim.from`, и взять
|
|
71
|
+
её до `now + leaseMs`. С `freeAt` — только если живой аренды нет. С `endedAt` — только если конец разговора всё
|
|
72
|
+
ещё этот (`null` — не закончен). `due` записать вместе с записью. Вернуть `true`,
|
|
73
|
+
если заменил этот вызов. Плюс `guarded: true`, который обещает:
|
|
74
|
+
- `setDue(…, { leaseOwner })` пишет, только пока аренда у этой копии; `{ unlessDelivery: true }` — только если
|
|
75
|
+
доставки не ждёт;
|
|
76
|
+
- `leaseDue` не берёт живую аренду, а законченные разговоры берёт только со сроком `FLOW_DELIVERY_EXIT`;
|
|
77
|
+
- `endChat` при ждущей доставке не снимает срок и аренду.
|
|
78
|
+
Образец — `postgresFlowStore` и `sqliteFlowStore` в `@ykurilov/agent-chat-flows`.
|
|
79
|
+
5. Проверка: `pnpm typecheck` проходит. Без признака или без `saveLinkIfFree` запуск сразу бросает `TypeError` с
|
|
80
|
+
подсказкой, без `swapDelivery` — `attach({ deliver })`.
|
|
81
|
+
|
|
3
82
|
## 0.80.1
|
|
4
83
|
|
|
5
84
|
### Patch Changes
|
|
@@ -1700,9 +1700,13 @@ function channelOfMessage(message) {
|
|
|
1700
1700
|
}
|
|
1701
1701
|
var isNoticeMessage = (message) => message.parts.some((part) => part.type === AGENT_NOTICE_PART_TYPE);
|
|
1702
1702
|
var isHumanMessage = (message) => message.role === "user" && !isNoticeMessage(message) && !isStateMessage(message);
|
|
1703
|
+
var hasChannelPart = (message) => message.parts.some((part) => part.type === CHANNEL_PART_TYPE);
|
|
1703
1704
|
function turnChannelOf(branch) {
|
|
1704
|
-
const
|
|
1705
|
-
return
|
|
1705
|
+
const source = branch.findLast((message) => isHumanMessage(message) || message.role === "user" && isNoticeMessage(message) && hasChannelPart(message));
|
|
1706
|
+
return source ? channelOfMessage(source) : { channel: "web" };
|
|
1707
|
+
}
|
|
1708
|
+
function inheritedGroupPart(scope) {
|
|
1709
|
+
return scope.group ? channelPart(scope.channel, scope.group) : void 0;
|
|
1706
1710
|
}
|
|
1707
1711
|
function turnOriginOf(branch) {
|
|
1708
1712
|
const parent = branch.at(-2);
|
|
@@ -8639,7 +8643,8 @@ function shortOutput(value) {
|
|
|
8639
8643
|
var MAX_SERVER_TURNS_IN_ROW = 5;
|
|
8640
8644
|
function noticeMessageParts(input) {
|
|
8641
8645
|
const data = { notices: input.notices, deliveryKey: input.deliveryKey, wakeAgent: input.wakeAgent };
|
|
8642
|
-
|
|
8646
|
+
const channel = channelPart(input.channel ?? "web", input.group);
|
|
8647
|
+
return [{ type: AGENT_NOTICE_PART_TYPE, data }, ...channel ? [channel] : []];
|
|
8643
8648
|
}
|
|
8644
8649
|
function noticeDataOf(message) {
|
|
8645
8650
|
if (message.role !== "user") return void 0;
|
|
@@ -13188,9 +13193,9 @@ function createChatService(input) {
|
|
|
13188
13193
|
throw error;
|
|
13189
13194
|
}
|
|
13190
13195
|
}
|
|
13191
|
-
async function confirmFiles(
|
|
13196
|
+
async function confirmFiles(fileParts) {
|
|
13192
13197
|
if (fileParts.length === 0) return;
|
|
13193
|
-
await options2.attachments.confirmUserParts(fileParts)
|
|
13198
|
+
await options2.attachments.confirmUserParts(fileParts);
|
|
13194
13199
|
}
|
|
13195
13200
|
async function ownChat(userId, chatId) {
|
|
13196
13201
|
const chat = await storage.getChat(chatId);
|
|
@@ -13907,7 +13912,7 @@ function createChatService(input) {
|
|
|
13907
13912
|
// Окно тишины пачки считается от этого времени: по часам службы.
|
|
13908
13913
|
...burst && { createdAt: writtenAt.toISOString() }
|
|
13909
13914
|
});
|
|
13910
|
-
await confirmFiles(
|
|
13915
|
+
await confirmFiles(fileParts);
|
|
13911
13916
|
await storage.insertMessage(question3);
|
|
13912
13917
|
return question3;
|
|
13913
13918
|
});
|
|
@@ -14143,14 +14148,20 @@ function createChatService(input) {
|
|
|
14143
14148
|
return await writingCountedMessage(userId, chatId, async () => {
|
|
14144
14149
|
const queued = await steering.accept(chatId, async (parentId) => {
|
|
14145
14150
|
const fileParts = await acceptFiles(userId, chatId, files);
|
|
14151
|
+
const groupPart = inheritedGroupPart(turnChannelOf(await currentBranch(chatId)));
|
|
14146
14152
|
const correction = newStoredMessage({
|
|
14147
14153
|
id: message.id,
|
|
14148
14154
|
chatId,
|
|
14149
14155
|
parentId,
|
|
14150
14156
|
role: "user",
|
|
14151
|
-
parts: [
|
|
14157
|
+
parts: [
|
|
14158
|
+
...await filePartsWithNumbers(chatId, fileParts),
|
|
14159
|
+
...text.trim().length > 0 ? [{ type: "text", text }] : [],
|
|
14160
|
+
steerPart(),
|
|
14161
|
+
...groupPart ? [groupPart] : []
|
|
14162
|
+
]
|
|
14152
14163
|
});
|
|
14153
|
-
await confirmFiles(
|
|
14164
|
+
await confirmFiles(fileParts);
|
|
14154
14165
|
await storage.insertMessage(correction);
|
|
14155
14166
|
return correction;
|
|
14156
14167
|
}, page);
|
|
@@ -14438,7 +14449,7 @@ function createChatService(input) {
|
|
|
14438
14449
|
await compaction.cancel(chatId);
|
|
14439
14450
|
await options2.attachments?.removeChatFiles?.(chatId);
|
|
14440
14451
|
await storage.deleteChat(chatId);
|
|
14441
|
-
}).finally(() => deleting.delete(chatId));
|
|
14452
|
+
}, { lease: "required" }).finally(() => deleting.delete(chatId));
|
|
14442
14453
|
await options2.background?.chatDeleted(userId, chatId).catch((error) => logError(`\u0424\u043E\u043D: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0443\u0431\u0440\u0430\u0442\u044C \u0437\u0430\u0434\u0430\u0447\u0438 \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 ${chatId}: ${String(error)}`));
|
|
14443
14454
|
await options2.limits?.chatDeleted?.({ userId, chatId }).catch((error) => logError(`\u041B\u0438\u043C\u0438\u0442\u044B: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0441\u0442\u0435\u0440\u0435\u0442\u044C \u043E\u043A\u043D\u0430 \u0443\u0434\u0430\u043B\u0451\u043D\u043D\u043E\u0433\u043E \u0447\u0430\u0442\u0430 ${chatId}: ${String(error)}`));
|
|
14444
14455
|
},
|
|
@@ -14620,11 +14631,13 @@ function createChatService(input) {
|
|
|
14620
14631
|
)
|
|
14621
14632
|
] : [],
|
|
14622
14633
|
// Правленое сообщение пачки остаётся в своей пачке: отметка с прежним временем записи.
|
|
14623
|
-
...edited2.parts.filter((part) => part.type === BURST_PART_TYPE)
|
|
14634
|
+
...edited2.parts.filter((part) => part.type === BURST_PART_TYPE),
|
|
14635
|
+
// Правленое сообщение группы остаётся в границах группы: отметка — из записанного сообщения, не из запроса.
|
|
14636
|
+
...[inheritedGroupPart(channelOfMessage(edited2))].filter((part) => part !== void 0)
|
|
14624
14637
|
],
|
|
14625
14638
|
createdAt: nextSiblingTime(messages, selections, edited2.parentId)
|
|
14626
14639
|
});
|
|
14627
|
-
await confirmFiles(
|
|
14640
|
+
await confirmFiles(fileParts);
|
|
14628
14641
|
await storage.insertMessage(question3);
|
|
14629
14642
|
return { branch: branch2, index: index2, edited: edited2, question: question3 };
|
|
14630
14643
|
});
|
|
@@ -15228,6 +15241,7 @@ export {
|
|
|
15228
15241
|
THOUGHT_TITLE_METADATA_KEY,
|
|
15229
15242
|
thoughtTitlesOfParts,
|
|
15230
15243
|
CHANNEL_PART_TYPE,
|
|
15244
|
+
isHumanMessage,
|
|
15231
15245
|
turnOriginOf,
|
|
15232
15246
|
GREETING_LEAD,
|
|
15233
15247
|
STEER_PART_TYPE,
|
|
@@ -15352,4 +15366,4 @@ export {
|
|
|
15352
15366
|
InvalidWidgetAnswerError,
|
|
15353
15367
|
createChatService
|
|
15354
15368
|
};
|
|
15355
|
-
//# sourceMappingURL=chunk-
|
|
15369
|
+
//# sourceMappingURL=chunk-YGHGURNF.js.map
|