@ykurilov/agent-chat-core 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +2 -0
  3. package/dist/access.js +2 -2
  4. package/dist/{chunk-SNPF4BPB.js → chunk-FBULM4V6.js} +2 -2
  5. package/dist/{chunk-PWGI6NH6.js → chunk-FOAIG4DY.js} +1 -1
  6. package/dist/chunk-FOAIG4DY.js.map +1 -0
  7. package/dist/{chunk-7M7M7DAN.js → chunk-KAG3QTZC.js} +661 -46
  8. package/dist/chunk-KAG3QTZC.js.map +1 -0
  9. package/dist/index.js +77 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/storage-contract.js +87 -5
  12. package/dist/storage-contract.js.map +1 -1
  13. package/dist/types/agent-run/garbled-tail.d.ts +69 -0
  14. package/dist/types/agent-run/next-attempt.d.ts +12 -1
  15. package/dist/types/agent-run/reply-checks.d.ts +3 -1
  16. package/dist/types/agent-run/run-agent-turn.d.ts +6 -0
  17. package/dist/types/agent-run/staff-replies.d.ts +7 -0
  18. package/dist/types/bursts/message-bursts.d.ts +12 -0
  19. package/dist/types/chat-events/chat-events.d.ts +7 -0
  20. package/dist/types/chat-service.d.ts +37 -0
  21. package/dist/types/helpers/run-helper.d.ts +3 -0
  22. package/dist/types/index.d.ts +5 -3
  23. package/dist/types/provider-errors/classify-provider-error.d.ts +5 -0
  24. package/dist/types/provider-errors/error-signs.d.ts +2 -2
  25. package/dist/types/provider-errors/provider-outages.d.ts +41 -0
  26. package/dist/types/storage/chat-storage.d.ts +47 -0
  27. package/dist/types/storage/postgres-migrations.d.ts +28 -0
  28. package/dist/types/storage/storage-contract.d.ts +5 -0
  29. package/dist/types/turn-hooks/turn-gate.d.ts +2 -0
  30. package/dist/types/turn-hooks/turn-hooks.d.ts +7 -0
  31. package/package.json +1 -1
  32. package/dist/chunk-7M7M7DAN.js.map +0 -1
  33. package/dist/chunk-PWGI6NH6.js.map +0 -1
  34. /package/dist/{chunk-SNPF4BPB.js.map → chunk-FBULM4V6.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,40 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.58.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 332d90e: Новый параметр службы `abandonedBursts: { serviceFor }`: при нескольких службах на одном хранилище обход брошенных пачек
8
+ (`answerAfterRestart`) отдаёт пачку службе, которую назвало приложение, — её инструкция и модель отвечают. `serviceFor`
9
+ может создать службу по требованию; вернула `null` или упала — пачка ждёт следующего обхода, чужая служба её не берёт. Тот же параметр — у `createChatServer`.
10
+ - 2ed600d: Пустой счёт (402) или непринятый ключ (401, 403 со словами о ключе или деньгах) у провайдера — без повторов сразу следующий из запасных (`providerFallback`), в любом шаге хода и у помощников. Отказавший провайдер 5 минут на паузе: новые ходы службы сразу идут к запасному, после паузы его пробует один ход. Прочие 403 (регион, доступ к модели) — запасной без паузы, 403 модерации — ошибка без перехода. 429 со словами о квоте — лимит запросов, а не пустой счёт (кроме `insufficient_quota`). Основной отказал в этом ходе, а запасные не ответили — человеку его причина, а если запасной лишь перегружен — перегрузка. Строки журнала `[запасной провайдер] … причина=insufficient_funds|invalid_key` и громкая `[ВНИМАНИЕ провайдер]` не чаще раза в 15 минут на провайдер.
11
+ - 760962e: Хранилища пакетов на PostgreSQL можно открывать одновременно на совсем пустой базе. Раньше пакеты разом создавали
12
+ схему `agent_chat`, проигравший падал на `CREATE SCHEMA IF NOT EXISTS`, и его хранилище не работало до перезапуска.
13
+ Теперь схема создаётся под общей блокировкой всех пакетов, она держится только на создании схемы. Миграции идут под
14
+ своей блокировкой пакета: разные пакеты друг друга не ждут, взаимной блокировки нет. Столкновение «такое уже есть»
15
+ повторяется само. Новое в core: `migratePostgresSchema({ connect, lockKey, migrate })` — для своих пакетов на той же
16
+ схеме.
17
+
18
+ ### Как обновить приложение
19
+
20
+ Ничего делать не нужно.
21
+
22
+ ## 0.57.0
23
+
24
+ ### Minor Changes
25
+
26
+ - ade99cc: Окно тишины `sendBurstMessage` ждётся по часам службы (`now`): `service.tickBursts()` сверяет ждущие пачки с подменёнными
27
+ часами — в тестах без настоящих пауз. Новый флаг `answerAfterRestart`: ждущая пачка записана в хранилище (новые
28
+ необязательные методы `savePendingBurst`, `takeAbandonedBursts`, `renewPendingBurst`, `removePendingBurst`; таблица `pending_bursts`,
29
+ миграции SQLite 0018 и PostgreSQL 0017) и после перезапуска получает ответ без повтора вызова — событием `turn-started`
30
+ с `unattended: true`. Отвечает по-прежнему одна копия.
31
+ - 86afa20: Ответ, сорвавшийся в кашу (строка с ролью после законченного ответа или повторный `</think>` плюс ещё один признак), обрезается: при сохранении, в потоке (вызовы после срыва не запускаются, выполненные остаются в записи) и перед отправкой модели (только в запросе). Строка в журнал `[хвост обрезан]`.
32
+
33
+ ### Patch Changes
34
+
35
+ - 624c10c: Реплика сотрудника (`appendStaffMessage`) в запросе модели идёт в одном сообщении с соседними словами человека: пометки
36
+ «ход оборвался до первых слов» рядом с ней больше нет. Запрос зависит только от истории — кэш цел.
37
+
3
38
  ## 0.56.0
4
39
 
5
40
  ### Minor Changes
package/README.md CHANGED
@@ -117,6 +117,8 @@ import type { ChatStorage } from "@ykurilov/agent-chat-core";
117
117
  сотрудника в конец ветки. Модель видит её словами сотрудника («Живой сотрудник «Ольга» ответил человеку вместо тебя…»),
118
118
  экран — сообщением сотрудника (`StaffMessage` скина). Проверки ответа агента её текст знают, а `person_link_view`
119
119
  ссылкой человека её не считает. Ход сама не начинает; `wakeAgent: true` — агент отвечает сразу. Занятый чат — `busy`, пустой текст — `TypeError`.
120
+ В запросе модели реплика идёт в одном сообщении с соседними словами человека (через пустую строку), поэтому пометки
121
+ «ход оборвался» рядом с ней нет (с 0.57).
120
122
 
121
123
  ## Надёжность хода
122
124
 
package/dist/access.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  accessRefusedResponse,
3
3
  allowedUser
4
- } from "./chunk-SNPF4BPB.js";
5
- import "./chunk-PWGI6NH6.js";
4
+ } from "./chunk-FBULM4V6.js";
5
+ import "./chunk-FOAIG4DY.js";
6
6
  export {
7
7
  accessRefusedResponse,
8
8
  allowedUser
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  normalizeTurnRefusal
3
- } from "./chunk-PWGI6NH6.js";
3
+ } from "./chunk-FOAIG4DY.js";
4
4
 
5
5
  // src/access/resolved-user.ts
6
6
  function allowedUser(resolved) {
@@ -17,4 +17,4 @@ export {
17
17
  allowedUser,
18
18
  accessRefusedResponse
19
19
  };
20
- //# sourceMappingURL=chunk-SNPF4BPB.js.map
20
+ //# sourceMappingURL=chunk-FBULM4V6.js.map
@@ -53,4 +53,4 @@ export {
53
53
  TurnRefusedError,
54
54
  validateAppEvent
55
55
  };
56
- //# sourceMappingURL=chunk-PWGI6NH6.js.map
56
+ //# sourceMappingURL=chunk-FOAIG4DY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/turn-hooks/turn-hooks.ts"],"sourcesContent":["import type { AgentChannel, Chat, ChatErrorKind, ChatSubject, FinalAnswerStatus, StoredMessage, TokenUsage } from \"../conversation/types\";\n\n/** Что человек делает с чатом, когда приложение решает, пускать ли ход. */\nexport type TurnAction = \"send\" | \"edit\" | \"retry\" | \"regenerate\" | \"approval\" | \"widget\" | \"notice\";\n\n/** Отказ приложения в ходе: код для программы, текст для человека. */\nexport interface TurnRefusal {\n /** Латиница в нижнем регистре, цифры и `.`, `_`, `-`; от 1 до 64 знаков. */\n code: string;\n /** До 300 знаков; длиннее — обрезается. */\n message: string;\n}\n\n/**\n * Сотрудник, который действует в чужом чате (копия чата человека в песочнице). Пока он есть у хода (`model.staff`),\n * инструменты приложения данные `ownerId` только читают: любая запись в них запрещена — это обязанность инструментов.\n */\nexport interface StaffActor {\n id: string;\n}\n\n/** Событие приложения в ответе агента: `type` — 1–100 знаков, `data` — JSON до 16 КБ. */\nexport interface AppEvent {\n type: string;\n data?: unknown;\n}\n\n/** Записать сообщение человека без хода: агент промолчит (рамки дня, пауза сценария). */\nexport interface TurnHold {\n hold: true;\n}\n\n/** Ход с готовым текстом, без модели: путь `reply` из `deliverNotices`. */\nexport interface TurnReply {\n reply: { text: string };\n}\n\n/** Что `beforeTurn` может решить, кроме «пускать»: отказ, «записать без хода» или «ответить готовым текстом». */\nexport type BeforeTurnDecision = TurnRefusal | TurnHold | TurnReply;\n\n/**\n * Проверка приложения перед ходом: вернуть отказ — ход не начнётся и ничего не запишется. `{ hold: true }` и\n * `{ reply }` — с 0.49 (сценарии): сообщение записывается, хода модели нет.\n */\nexport type BeforeTurnHandler = (input: {\n userId: string;\n chat: Chat;\n action: TurnAction;\n /** Текст сообщения человека (`send`, `edit`). */\n text?: string;\n /** Id сообщения человека (`send`, `edit`): по нему приложение свяжет свой резерв с ходом. */\n messageId?: string;\n /**\n * Ход на пачку сообщений человека (`sendBurstMessage`): id всех её сообщений по порядку, последнее — `messageId`, а\n * `text` — их тексты через пустую строку. Одно решение на всю пачку; счёт сообщений — по длине списка.\n */\n burstMessageIds?: string[];\n /**\n * Ход на пачку начал сам сервер после перезапуска или за упавшую копию (`answerAfterRestart`): вызова, который ждёт\n * ответ или отказ, нет. Отказ дойдёт только до журнала — сообщить человеку может само приложение.\n */\n unattended?: true;\n} & AbandonedAnswers) => Promise<BeforeTurnDecision | void> | BeforeTurnDecision | void;\n\n/**\n * Ответы агента, которые уйдут с текущей ветки, если ход начнётся (`regenerate`, `edit`, `retry`): по ним приложение откатывает\n * побочные дела брошенных ответов (правки формы и т. п.). Ход не начался (`turnNotStarted`) — ветка прежняя, ничего не\n * ушло: тот хук получает те же поля. Посчитаны по ветке на миг проверки; сообщения нет на ветке — полей нет.\n */\nexport interface AbandonedAnswers {\n /** Ответы агента, которые уходят с ветки, сверху вниз; у `edit` без ответов ниже — пустой список. */\n abandonedAnswerIds?: string[];\n /**\n * Ответ, который заменит новый: переделываемый (`regenerate`), ответ на правленый вопрос (`edit`), если был, или\n * оборванный ответ, который «Повторить» (`retry`) заменяет соседом. У «Повторить» вопроса без ответа полей нет.\n */\n previousAnswerId?: string;\n}\n\n/** Почему ход не начался после того, как `beforeTurn` его пропустил. */\nexport type TurnNotStartedReason =\n /** В чате уже идёт ответ, или служба останавливается (`flush`). */\n | \"busy\"\n /** У человека уже идёт предел одновременных ходов. */\n | \"too_many_turns\"\n /** Лимит расходов исчерпан. */\n | \"spending_limit\"\n /** Сообщение с таким id уже есть (повтор запроса). */\n | \"duplicate_message\"\n /** Запрос неверен: сообщение, файл, решение, ответ на виджет. */\n | \"invalid\"\n /** Чат закрыли, пока ход запускался (`closeChat` между проверкой и началом хода). */\n | \"chat_closed\"\n /** Хода и не будет: проверка без хода (`assertTurnAllowed`), ответ ждёт ещё решений, служебное сообщение без ответа. */\n | \"no_turn\"\n /** Сбой (база, сеть): ход не начат. */\n | \"error\";\n\n/**\n * Ход не начался, хотя `beforeTurn` его пропустил: приложение снимает свой резерв. Каждый пропуск `beforeTurn`\n * закрывается ровно одним: ходом (`afterTurn`) или этим хуком. Ошибка хода наружу прежняя; бросок хука — в журнал.\n */\nexport type TurnNotStartedHandler = (input: {\n userId: string;\n chat: Chat;\n action: TurnAction;\n messageId?: string;\n text?: string;\n /** Пачка сообщений, как в `beforeTurn`. */\n burstMessageIds?: string[];\n /** Ход без вызова, как в `beforeTurn`: об отказе (например, `spending_limit`) человеку может сказать только приложение. */\n unattended?: true;\n reason: TurnNotStartedReason;\n} & AbandonedAnswers) => Promise<void> | void;\n\n/** Кто остановил ход: человек («Стоп»), общий срок хода (`agent.turnTimeoutMs`) или остановка службы (`flush`). */\nexport type TurnStopReason = \"user\" | \"timeout\" | \"shutdown\";\n\n/** Итог законченного хода для приложения. */\nexport interface TurnResult {\n ownerId: string;\n /** Кто сделал ход: владелец чата, а в копии у сотрудника — сотрудник. */\n actorId: string;\n chatId: string;\n messageId: string;\n status: FinalAnswerStatus;\n /** Кто остановил ход; только у `status: \"stopped\"` живого хода (у доделанного после перезапуска нет). */\n stopReason?: TurnStopReason;\n /** Ответ упёрся в предел токенов (`finishReason: \"length\"`, см. `agent.maxOutputTokens`) и оборван. */\n lengthCut?: true;\n /**\n * Ход дошёл до предела шагов (`agent.maxSteps`): последний шаг был итоговым, без инструментов. Это «дошли до потолка»,\n * а не обязательно «недоделали»: модель могла закончить дело как раз на последнем шаге. Статус обычный (`complete`).\n * В журнале SDK — строка `[предел шагов]`.\n */\n stepLimit?: true;\n /** Текстовые части ответа подряд. */\n text: string;\n /** Части ответа, как они записаны, кроме служебной части с блоками контекста (`data-frozen-context`). */\n parts: StoredMessage[\"parts\"];\n usage: TokenUsage | null;\n /** Цена хода в долларах; `null` — у модели нет цены. */\n costUsd: number | null;\n error?: { kind: ChatErrorKind };\n /**\n * Каким действием начат ход (см. `TurnAction`). `null` — итог доделан после перезапуска (`recovered`), а ядро не\n * помнит действие; ход по служебному сообщению и тогда — `notice`.\n */\n trigger: TurnAction | null;\n channel: AgentChannel;\n /**\n * Ход сделал сотрудник в копии чата человека (песочница): `ownerId` и `subject` — человека и его исходного чата.\n * При `staff` в данные человека не писать.\n */\n staff?: StaffActor;\n /** Объект чата (урок, заказ…): ключ и данные чата объекта. Нет — обычный чат. */\n subject?: ChatSubject;\n /** Данные чата (`Chat.data`) на момент итога; нет данных — поля нет. */\n chatData?: Record<string, unknown>;\n /** Итог доделан после перезапуска сервера, а не сразу после хода. */\n recovered: boolean;\n}\n\n/**\n * Хук итога хода: зовётся после записи итога и расхода, до `finish` клиенту; пока он идёт, чат занят. Получает любой\n * итог: законченный, остановленный, с ошибкой, прерванный (в том числе восстановлением после перезапуска) и ждущий\n * человека. Ждущий ответ (`awaiting_approval`) после решения человека продолжается тем же сообщением — хук позовут\n * второй раз, с новым статусом. Хук может быть вызван повторно (повтор после сбоя, доделка после перезапуска):\n * ключ повтора — `messageId` + `status`. Возвращённые `events` ложатся в конец ответа частями `data-app-event`.\n */\nexport type AfterTurnHandler = (\n result: TurnResult,\n) => Promise<{ events?: AppEvent[] } | void> | { events?: AppEvent[] } | void;\n\nexport const TURN_REFUSAL_MESSAGE_MAX_LENGTH = 300;\nexport const APP_EVENT_TYPE_MAX_LENGTH = 100;\nexport const APP_EVENT_DATA_MAX_BYTES = 16 * 1024;\n\nconst TURN_REFUSAL_CODE = /^[a-z0-9_.-]{1,64}$/;\n\nexport function isTurnRefusalCode(code: unknown): code is string {\n return typeof code === \"string\" && TURN_REFUSAL_CODE.test(code);\n}\n\n/** Отказ по правилам: код проверен (иначе `TypeError`), текст обрезан до 300 знаков. */\nexport function normalizeTurnRefusal(refusal: TurnRefusal): TurnRefusal {\n if (!isTurnRefusalCode(refusal?.code)) throw new TypeError(\"Код отказа — [a-z0-9_.-], от 1 до 64 знаков\");\n if (typeof refusal.message !== \"string\") throw new TypeError(\"Текст отказа — строка\");\n // По знакам, а не по половинкам: эмодзи на границе не рвётся.\n const message = Array.from(refusal.message).slice(0, TURN_REFUSAL_MESSAGE_MAX_LENGTH).join(\"\");\n return { code: refusal.code, message };\n}\n\n/** Приложение отказало в ходе: ход не начат, ничего не записано. */\nexport class TurnRefusedError extends Error {\n readonly code: string;\n\n constructor(refusal: TurnRefusal) {\n const normalized = normalizeTurnRefusal(refusal);\n super(normalized.message);\n this.name = \"TurnRefusedError\";\n this.code = normalized.code;\n }\n\n get refusal(): TurnRefusal {\n return { code: this.code, message: this.message };\n }\n}\n\n/** Событие по правилам или `TypeError`. Отдаёт копию: данные прошли через JSON. */\nexport function validateAppEvent(event: unknown): AppEvent {\n if (event === null || typeof event !== \"object\") throw new TypeError(\"Событие приложения — объект { type, data? }\");\n const { type, data } = event as { type?: unknown; data?: unknown };\n if (typeof type !== \"string\" || type.length < 1 || type.length > APP_EVENT_TYPE_MAX_LENGTH) {\n throw new TypeError(`Тип события приложения — строка от 1 до ${APP_EVENT_TYPE_MAX_LENGTH} знаков`);\n }\n if (data === undefined) return { type };\n let json: string | undefined;\n try {\n json = JSON.stringify(data);\n } catch {\n json = undefined;\n }\n if (json === undefined) throw new TypeError(\"Данные события приложения — значение JSON\");\n if (new TextEncoder().encode(json).length > APP_EVENT_DATA_MAX_BYTES) {\n throw new TypeError(`Данные события приложения больше ${APP_EVENT_DATA_MAX_BYTES} байт`);\n }\n return { type, data: JSON.parse(json) };\n}\n"],"mappings":";AA8KO,IAAM,kCAAkC;AACxC,IAAM,4BAA4B;AAClC,IAAM,2BAA2B,KAAK;AAE7C,IAAM,oBAAoB;AAEnB,SAAS,kBAAkB,MAA+B;AAC/D,SAAO,OAAO,SAAS,YAAY,kBAAkB,KAAK,IAAI;AAChE;AAGO,SAAS,qBAAqB,SAAmC;AACtE,MAAI,CAAC,kBAAkB,SAAS,IAAI,EAAG,OAAM,IAAI,UAAU,iJAA6C;AACxG,MAAI,OAAO,QAAQ,YAAY,SAAU,OAAM,IAAI,UAAU,iHAAuB;AAEpF,QAAM,UAAU,MAAM,KAAK,QAAQ,OAAO,EAAE,MAAM,GAAG,+BAA+B,EAAE,KAAK,EAAE;AAC7F,SAAO,EAAE,MAAM,QAAQ,MAAM,QAAQ;AACvC;AAGO,IAAM,mBAAN,cAA+B,MAAM;AAAA,EACjC;AAAA,EAET,YAAY,SAAsB;AAChC,UAAM,aAAa,qBAAqB,OAAO;AAC/C,UAAM,WAAW,OAAO;AACxB,SAAK,OAAO;AACZ,SAAK,OAAO,WAAW;AAAA,EACzB;AAAA,EAEA,IAAI,UAAuB;AACzB,WAAO,EAAE,MAAM,KAAK,MAAM,SAAS,KAAK,QAAQ;AAAA,EAClD;AACF;AAGO,SAAS,iBAAiB,OAA0B;AACzD,MAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,OAAM,IAAI,UAAU,qKAA6C;AAClH,QAAM,EAAE,MAAM,KAAK,IAAI;AACvB,MAAI,OAAO,SAAS,YAAY,KAAK,SAAS,KAAK,KAAK,SAAS,2BAA2B;AAC1F,UAAM,IAAI,UAAU,sMAA2C,yBAAyB,uCAAS;AAAA,EACnG;AACA,MAAI,SAAS,OAAW,QAAO,EAAE,KAAK;AACtC,MAAI;AACJ,MAAI;AACF,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B,QAAQ;AACN,WAAO;AAAA,EACT;AACA,MAAI,SAAS,OAAW,OAAM,IAAI,UAAU,2MAA2C;AACvF,MAAI,IAAI,YAAY,EAAE,OAAO,IAAI,EAAE,SAAS,0BAA0B;AACpE,UAAM,IAAI,UAAU,qLAAoC,wBAAwB,2BAAO;AAAA,EACzF;AACA,SAAO,EAAE,MAAM,MAAM,KAAK,MAAM,IAAI,EAAE;AACxC;","names":[]}