@ykurilov/agent-chat-flows 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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/store/flow-store.ts","../src/store/flow-store-shared.ts"],"sourcesContent":["import type { AgentFlow } from \"../format/flow-format\";\nimport type { FlowEndReason, Transition } from \"../engine/flow-run\";\nimport type { StateMap } from \"../engine/migrate-run\";\n\n// Договор хранилища сценариев: память (тесты), SQLite и PostgreSQL ведут себя одинаково.\n// Времена — строки ISO 8601, как у хранилища чатов и фона.\n\n/** Что случилось с чатом вне хода; применит `prepareTurn` следующего хода. Повтор с тем же `key` ничего не добавляет. */\nexport type PendingFlowChange =\n | {\n kind: \"transition\";\n key: string;\n transition: Transition;\n /** Последнее сообщение ветки, на которой решён переход; `null` — пустой чат. Ветку сменили — ожидание снимается. */\n branchHead: string | null;\n /**\n * Переход по таймеру уже применён в срок без хода (писать первым нельзя): запись чата и `onTransition` сделаны,\n * таймеры идут от новой ноды. Ход только переносит его в состояние чата, не сообщая заново.\n */\n appliedAt?: string;\n }\n | { kind: \"event\"; key: string; name: string; data?: unknown; at: string }\n | { kind: \"migrate\"; key: string; versionId: string; map: StateMap; at: string };\n\n/** Чат, который идёт по сценарию: версия закреплена, состояние и ближайший таймер — для выборок и тика. */\nexport interface FlowChatRecord {\n chatId: string;\n ownerId: string;\n flowId: string;\n versionId: string;\n /** Копия состояния из `FlowRun` для счёта чатов по состояниям. Правда — в состоянии чата. */\n stateId: string;\n pending: PendingFlowChange[];\n /** Ближайший таймер: когда, какое условие и на какой ветке он посчитан. */\n dueAt: string | null;\n dueExitId: string | null;\n dueBranchHead: string | null;\n /** Пояс человека (IANA): от него «время суток» и тихие часы. Запасной: сначала — пояс человека (`readTimeZone`). */\n timeZone: string;\n updatedAt: string;\n /** Разговор закончился: таймеров и касаний больше нет. */\n endedAt: string | null;\n endReason: FlowEndReason | null;\n /** Разговор открыт заново (`flow.reopen`) в этот момент, по часам чата; место догоняет отметку на ближайшем чтении. С 0.68. */\n reopenedAt: string | null;\n /** Пауза «отвечает живой сотрудник» до этого момента; таймеры не берутся. */\n pausedUntil: string | null;\n /** Канал приложения закрыл окно: первым не писать до сообщения человека. */\n windowClosed: boolean;\n /** День сценария («ГГГГ-ММ-ДД»), который агент закрыл (`flow_close_day`): сегодня первым не пишет. */\n dayClosedOn: string | null;\n /** Недоставленный ответ, который ждёт повтора. */\n delivery: FlowPendingDelivery | null;\n /** Куда приложение перенесло чат при закреплении; первый ход кладёт это место в состояние чата и снимает запись. */\n placement: FlowChatPlacement | null;\n}\n\n/** Моменты, от которых считаются дни и сроки перенесённого чата (ISO). */\nexport interface FlowChatPlacement {\n startedAt: string;\n enteredAt: string;\n // Перенос идущего чата целиком (0.50). Записи 0.49 этих полей не имеют.\n /** Последнее сообщение человека. */\n lastHumanAt?: string;\n /** Последнее сообщение агента: ответ или касание. */\n lastAgentAt?: string;\n /** Следующий шаг ряда касаний. */\n nudgeStep?: number;\n /** Сколько сообщений человек написал за день сценария к моменту `at`: день считается по поясу человека, когда он известен. */\n humanToday?: { at: string; count: number };\n}\n\n/** Срок записи о повторе доставки в таблице сроков. Законченный разговор берётся таймером только с этим сроком. */\nexport const FLOW_DELIVERY_EXIT = \"@delivery\";\n\n/** Повтор доставки того же текста по ключу `messageId`, без нового хода модели. */\nexport interface FlowPendingDelivery {\n messageId: string;\n /** Номер следующей попытки, с 1. */\n attempt: number;\n retryAt: string;\n /** Причины хода «пишет первым» — для `FlowOutgoing.reasons`. */\n reasons: string[];\n /**\n * Версия записи: своя у каждой отправки и каждого возобновления, даже при том же `attempt`. По ней `swapDelivery`\n * отличает устаревшего писателя. С 0.81; записи до 0.81 её не имеют.\n */\n token?: string;\n /** Канал ждёт починки снаружи (`await_repair`): срока нет, попытка не потрачена, отправит `resumeDelivery`. С 0.81. */\n pausedAt?: string;\n /** Возобновлено после починки (`resumeDelivery`): перед отправкой ответ проверяется на устаревание. С 0.81. */\n resumedAt?: string;\n}\n\n/**\n * Условная замена записи доставки (`swapDelivery`): одной операцией, только если запись — та, что прочитал вызывающий\n * (`messageId`, `attempt`, `token`; `null` — записи нет).\n */\nexport interface SwapDeliveryInput {\n expected: FlowPendingDelivery | null;\n next: FlowPendingDelivery | null;\n /** Срок чата вместе с записью: `null` — снять; нет поля — не трогать. Аренду не снимает. */\n due?: FlowDueInput | null;\n /**\n * Взять аренду таймера на `owner` до `now + leaseMs`: только если она свободна (нет или кончилась к `now`), уже у\n * `owner` или у `from` (аренда срабатывания, которое передаёт её отправке).\n */\n claim?: { owner: string; now: string; leaseMs: number; from?: string };\n /** Писать, только если к этому моменту ни у кого нет живой аренды: идёт отправка — не мешать ей. */\n freeAt?: string;\n /** Писать, только если конец разговора всё ещё этот: `null` — не закончен. Нет поля — не сверять. */\n endedAt?: string | null;\n}\n\nexport interface PinFlowChatInput {\n chatId: string;\n ownerId: string;\n flowId: string;\n versionId: string;\n stateId: string;\n timeZone: string;\n /** Чат перенесён в заданное место: до первого хода хранится здесь, переживая перезапуск сервера. */\n placement?: FlowChatPlacement;\n at: string;\n}\n\nexport type FlowChatPatch = Partial<Pick<FlowChatRecord, \"versionId\" | \"stateId\" | \"timeZone\" | \"placement\">>;\n\nexport interface FlowDueInput {\n at: string;\n exitId: string;\n branchHead: string | null;\n}\n\nexport interface RenewLeaseInput {\n chatId: string;\n owner: string;\n now: string;\n leaseMs: number;\n}\n\nexport interface SetDueOptions {\n /** Не писать, если чат ждёт повтора доставки: у повтора свой срок, его не затереть. */\n unlessDelivery?: boolean;\n /**\n * Писать, только если таймер ещё в аренде у этой копии: копия, у которой аренду забрали (зависла дольше срока), чужой\n * срок не переписывает. Хранилище приложения без этого условия такой гарантии не даёт.\n */\n leaseOwner?: string;\n}\n\nexport interface LeaseDueInput {\n now: string;\n /** Копия сервера, которая берёт таймеры. */\n owner: string;\n leaseMs: number;\n limit: number;\n}\n\nexport interface SavedFlowVersion {\n flowId: string;\n versionId: string;\n /** Отпечаток сценария: одна версия — один отпечаток. */\n hash: string;\n flow: AgentFlow;\n createdAt: string;\n}\n\n/** `same` — эта версия уже сохранена с тем же отпечатком; `conflict` — под тем же номером другой сценарий. */\nexport type SaveFlowVersionResult = \"new\" | \"same\" | \"conflict\";\n\nexport interface ChatsInState {\n versionId: string;\n stateId: string;\n chats: number;\n}\n\n/** Чат сценария для отбора при переводе на новую версию. */\nexport type FlowChatSummary = Pick<FlowChatRecord, \"chatId\" | \"ownerId\" | \"flowId\" | \"versionId\" | \"stateId\" | \"endedAt\" | \"endReason\">;\n\nexport interface ListFlowChatsInput {\n flowId: string;\n /** Без чатов, которые уже на этой версии. */\n exceptVersionId?: string;\n /** Только эти чаты. */\n chatIds?: string[];\n}\n\nexport interface MigrateBatchInput {\n flowId: string;\n toVersionId: string;\n /** Куда перевести чаты из удалённых состояний. */\n map: StateMap;\n /** Только эти чаты; нет — любые чаты сценария не на `toVersionId`. */\n chatIds?: string[];\n limit: number;\n at: string;\n}\n\n/** Поле карточки клиента: долгая память о человеке по всем его чатам. */\nexport interface ClientCardEntry {\n fieldId: string;\n value: unknown;\n updatedAt: string;\n /** Где узнали. */\n chatId: string;\n flowId: string;\n}\n\n/** Свободный факт о человеке («ведёт блог»): не поле сценария, а короткая фраза, которую агент записал сам. */\nexport interface ClientFact {\n /** Стабильный по тексту (`factId`): тот же факт — тот же id. */\n id: string;\n text: string;\n updatedAt: string;\n /** Где узнали. */\n chatId: string;\n flowId: string;\n}\n\nexport interface WriteCardFieldInput {\n ownerId: string;\n fieldId: string;\n value: unknown;\n chatId: string;\n flowId: string;\n at: string;\n}\n\n/** Попытки попутной задачи: на человека (`chatId: null`) или на разговор. */\nexport interface SideTaskRecord {\n taskId: string;\n chatId: string | null;\n /** Сколько раз агент предложил. */\n asks: number;\n doneAt: string | null;\n}\n\nexport interface NoteSideTaskInput {\n ownerId: string;\n taskId: string;\n /** `null` — попытки на человека; id чата — у задач «заново в каждом разговоре». */\n chatId: string | null;\n status: \"asked\" | \"done\";\n at: string;\n}\n\nexport interface FlowStore {\n /**\n * Хранилище исполняет условия ограды (с 0.81): `setDue(…, { leaseOwner })` пишет, только пока аренда у этой копии;\n * `swapDelivery` — с `expected`, `claim`, `freeAt` и `endedAt` одной атомарной операцией; `leaseDue` не берёт живую аренду;\n * `endChat` не снимает срок и аренду ждущей доставки. Без этого признака `createAgentFlow` не запускается.\n */\n readonly guarded: true;\n /** Закрепить версию за чатом. Чат уже закреплён — вернуть как есть, ничего не менять. */\n pinChat(input: PinFlowChatInput): Promise<FlowChatRecord>;\n getChat(chatId: string): Promise<FlowChatRecord | null>;\n updateChat(chatId: string, patch: FlowChatPatch, at: string): Promise<FlowChatRecord | null>;\n deleteChat(chatId: string): Promise<void>;\n\n /** Новый ключ — в конец; тот же ключ — не дублируется, но `appliedAt` дописывается к ожидающему без него. */\n addPending(chatId: string, change: PendingFlowChange): Promise<void>;\n /** Забрать всё ожидающее, в порядке добавления, и очистить; переходы, применённые в срок (`appliedAt`), остаются. */\n takePending(chatId: string): Promise<PendingFlowChange[]>;\n\n /** `null` — таймеров нет. Снимает аренду. */\n setDue(chatId: string, due: FlowDueInput | null, options?: SetDueOptions): Promise<void>;\n /**\n * Взять в аренду наступившие таймеры, которые никто не держит; ранние — первыми. Стоящие на паузе — пропустить;\n * законченные — тоже, кроме срока повтора доставки (`FLOW_DELIVERY_EXIT`).\n */\n leaseDue(input: LeaseDueInput): Promise<FlowChatRecord[]>;\n releaseLease(chatId: string, owner: string): Promise<void>;\n /** Продлить свою аренду до `now + leaseMs`. `false` — строка уже не наша: взяла другая копия или срок переписан. */\n renewLease(input: RenewLeaseInput): Promise<boolean>;\n\n saveVersion(input: Omit<SavedFlowVersion, \"createdAt\"> & { at: string }): Promise<SaveFlowVersionResult>;\n getVersion(flowId: string, versionId: string): Promise<SavedFlowVersion | null>;\n\n countChatsByState(input: { flowId: string }): Promise<ChatsInState[]>;\n /** Чаты сценария по порядку id: для отбора перед подсчётом и переводом. */\n listFlowChats(input: ListFlowChatsInput): Promise<FlowChatSummary[]>;\n /** Перевести до `limit` чатов на версию `toVersionId`: версия меняется сразу, перевод состояния — ожидающим `migrate`. */\n migrateBatch(input: MigrateBatchInput): Promise<{ moved: string[] }>;\n\n readCard(ownerId: string): Promise<ClientCardEntry[]>;\n writeCardField(input: WriteCardFieldInput): Promise<void>;\n forgetCard(ownerId: string, fieldId: string | \"all\"): Promise<void>;\n\n /** Сколько сообщений агент написал первым за сутки человека (`day` — «ГГГГ-ММ-ДД» в его поясе). */\n countFirstMessages(ownerId: string, day: string): Promise<number>;\n /** Записать ещё одно; вернуть новый счёт. */\n addFirstMessage(ownerId: string, day: string): Promise<number>;\n\n /**\n * Разговор закончен: ставит время и причину, снимает таймер и аренду. Ждёт доставка — её срок и аренда остаются: идущую\n * отправку не перебить, повтор после конца дойдёт. Уже закончен — ничего не меняет.\n */\n endChat(chatId: string, input: { reason: FlowEndReason; at: string }): Promise<FlowChatRecord | null>;\n /**\n * Открыть законченный разговор заново: только если он всё ещё закончен в `endedAt` (что прочитал вызывающий). Снимает\n * конец, ставит `reopenedAt`. Не тот конец, не закончен или чата нет — `null`: из двух копий открывает одна. С 0.68.\n */\n reopenChat(chatId: string, input: { endedAt: string; at: string }): Promise<FlowChatRecord | null>;\n /** `null` — снять паузу. */\n setPause(chatId: string, until: string | null): Promise<FlowChatRecord | null>;\n setWindowClosed(chatId: string, closed: boolean): Promise<void>;\n setDayClosed(chatId: string, day: string | null): Promise<void>;\n setDelivery(chatId: string, delivery: FlowPendingDelivery | null): Promise<void>;\n /** Заменить запись доставки, если она та, что ждали (`SwapDeliveryInput`). `true` — заменил этот вызов. С 0.81. */\n swapDelivery(chatId: string, input: SwapDeliveryInput): Promise<boolean>;\n\n /** Пояс человека (IANA); `null` — неизвестен. Общий для всех его чатов. */\n readTimeZone(ownerId: string): Promise<string | null>;\n writeTimeZone(ownerId: string, timeZone: string, at: string): Promise<void>;\n\n /** Попытки попутных задач человека: на человека и в этом разговоре. По `taskId`. */\n readSideTasks(ownerId: string, chatId: string): Promise<SideTaskRecord[]>;\n /** `asked` — ещё одна попытка, `done` — задача решена. Вернуть запись после изменения. */\n noteSideTask(input: NoteSideTaskInput): Promise<SideTaskRecord>;\n /** «Забудь обо мне»: стереть все попытки попутных задач человека — и на него, и на его разговоры. */\n forgetSideTasks(ownerId: string): Promise<void>;\n\n /** Остановка сервера: SQLite закрывает файл, PostgreSQL дожидается миграции (пул приложения не закрывает). */\n close?(): Promise<void>;\n}\n","import type { StateMap } from \"../engine/migrate-run\";\nimport type { ChatsInState, FlowPendingDelivery, MigrateBatchInput, PendingFlowChange } from \"./flow-store\";\n\n// Общее у трёх хранилищ сценариев: одинаковые ключи, порядок и правила, чтобы договор держался везде.\n\n/** Ожидающий перевод версии. Ключ с временем: перевод v1 → v2 → v1 → v2 не схлопывается в один. */\nexport function migratePending(input: MigrateBatchInput): PendingFlowChange {\n return { kind: \"migrate\", key: `migrate:${input.toVersionId}:${input.at}`, versionId: input.toVersionId, map: input.map, at: input.at };\n}\n\n/** Переход, применённый в срок без хода: «забрать» его не снимает — «Переделать» на той же ветке применит его снова. */\nexport const appliedInTime = (change: PendingFlowChange): boolean => change.kind === \"transition\" && change.appliedAt !== undefined;\n\n/**\n * Ожидающее после `addPending`: новый ключ — в конец; тот же ключ — прежний остаётся, но `appliedAt` нового дописывается,\n * если его не было (переход отложили ночью, а утром применили в срок). Ничего не изменилось — `null`.\n */\nexport function pendingAdded(pending: readonly PendingFlowChange[], change: PendingFlowChange): PendingFlowChange[] | null {\n const index = pending.findIndex((item) => item.key === change.key);\n if (index === -1) return [...pending, change];\n const existing = pending[index]!;\n if (existing.kind !== \"transition\" || change.kind !== \"transition\" || change.appliedAt === undefined || existing.appliedAt !== undefined) return null;\n return pending.map((item, at) => (at === index ? { ...existing, appliedAt: change.appliedAt } : item));\n}\n\n/** Состояние после перевода: удалённое — по карте, остальное как было. Зеркало для подсчёта до следующего хода. */\nexport const mappedState = (stateId: string, map: StateMap): string => (Object.hasOwn(map, stateId) ? map[stateId]! : stateId);\n\nexport const byVersionAndState = (left: ChatsInState, right: ChatsInState) =>\n compareText(left.versionId, right.versionId) || compareText(left.stateId, right.stateId);\n\n/** Порядок строк как у базы (`COLLATE \"C\"` и `BINARY`): по кодам символов, без учёта языка. */\nexport const compareText = (left: string, right: string) => (left < right ? -1 : left > right ? 1 : 0);\n\n/** Время к одному виду: `toISOString` с миллисекундами, как его вернёт база. */\nexport const normalizeTime = (iso: string) => new Date(iso).toISOString();\n\n/** PostgreSQL не хранит символ NUL ни в тексте, ни в JSON: вырезаем его до записи. */\nexport function withoutNul<T>(value: T): T {\n if (value === null || value === undefined) return value;\n if (typeof value === \"string\") return value.replaceAll(\"\\u0000\", \"\") as T;\n if (typeof value !== \"object\") return value;\n return JSON.parse(JSON.stringify(value, (_key, item: unknown) => (typeof item === \"string\" ? item.replaceAll(\"\\u0000\", \"\") : item))) as T;\n}\n\n/** Запись доставки к одному виду: времена — как их вернёт база. */\nexport function normalizedDelivery(delivery: FlowPendingDelivery): FlowPendingDelivery {\n return {\n ...delivery,\n retryAt: normalizeTime(delivery.retryAt),\n ...(delivery.pausedAt !== undefined && { pausedAt: normalizeTime(delivery.pausedAt) }),\n ...(delivery.resumedAt !== undefined && { resumedAt: normalizeTime(delivery.resumedAt) }),\n };\n}\n\n/** Та же запись доставки для `swapDelivery`: обе нет или совпали `messageId`, `attempt` и `token`. */\nexport function sameDelivery(stored: FlowPendingDelivery | null, expected: FlowPendingDelivery | null): boolean {\n if (stored === null || expected === null) return stored === expected;\n return stored.messageId === expected.messageId && stored.attempt === expected.attempt && (stored.token ?? null) === (expected.token ?? null);\n}\n"],"mappings":";AAyEO,IAAM,qBAAqB;;;ACnE3B,SAAS,eAAe,OAA6C;AAC1E,SAAO,EAAE,MAAM,WAAW,KAAK,WAAW,MAAM,WAAW,IAAI,MAAM,EAAE,IAAI,WAAW,MAAM,aAAa,KAAK,MAAM,KAAK,IAAI,MAAM,GAAG;AACxI;AAGO,IAAM,gBAAgB,CAAC,WAAuC,OAAO,SAAS,gBAAgB,OAAO,cAAc;AAMnH,SAAS,aAAa,SAAuC,QAAuD;AACzH,QAAM,QAAQ,QAAQ,UAAU,CAAC,SAAS,KAAK,QAAQ,OAAO,GAAG;AACjE,MAAI,UAAU,GAAI,QAAO,CAAC,GAAG,SAAS,MAAM;AAC5C,QAAM,WAAW,QAAQ,KAAK;AAC9B,MAAI,SAAS,SAAS,gBAAgB,OAAO,SAAS,gBAAgB,OAAO,cAAc,UAAa,SAAS,cAAc,OAAW,QAAO;AACjJ,SAAO,QAAQ,IAAI,CAAC,MAAM,OAAQ,OAAO,QAAQ,EAAE,GAAG,UAAU,WAAW,OAAO,UAAU,IAAI,IAAK;AACvG;AAGO,IAAM,cAAc,CAAC,SAAiB,QAA2B,OAAO,OAAO,KAAK,OAAO,IAAI,IAAI,OAAO,IAAK;AAE/G,IAAM,oBAAoB,CAAC,MAAoB,UACpD,YAAY,KAAK,WAAW,MAAM,SAAS,KAAK,YAAY,KAAK,SAAS,MAAM,OAAO;AAGlF,IAAM,cAAc,CAAC,MAAc,UAAmB,OAAO,QAAQ,KAAK,OAAO,QAAQ,IAAI;AAG7F,IAAM,gBAAgB,CAAC,QAAgB,IAAI,KAAK,GAAG,EAAE,YAAY;AAGjE,SAAS,WAAc,OAAa;AACzC,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,SAAU,QAAO,MAAM,WAAW,MAAU,EAAE;AACnE,MAAI,OAAO,UAAU,SAAU,QAAO;AACtC,SAAO,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC,MAAM,SAAmB,OAAO,SAAS,WAAW,KAAK,WAAW,MAAU,EAAE,IAAI,IAAK,CAAC;AACrI;AAGO,SAAS,mBAAmB,UAAoD;AACrF,SAAO;AAAA,IACL,GAAG;AAAA,IACH,SAAS,cAAc,SAAS,OAAO;AAAA,IACvC,GAAI,SAAS,aAAa,UAAa,EAAE,UAAU,cAAc,SAAS,QAAQ,EAAE;AAAA,IACpF,GAAI,SAAS,cAAc,UAAa,EAAE,WAAW,cAAc,SAAS,SAAS,EAAE;AAAA,EACzF;AACF;AAGO,SAAS,aAAa,QAAoC,UAA+C;AAC9G,MAAI,WAAW,QAAQ,aAAa,KAAM,QAAO,WAAW;AAC5D,SAAO,OAAO,cAAc,SAAS,aAAa,OAAO,YAAY,SAAS,YAAY,OAAO,SAAS,WAAW,SAAS,SAAS;AACzI;","names":[]}