@ykurilov/agent-chat-limits 0.0.0-stage → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/create-spending-limits.ts","../src/limit-state.ts","../src/spending-state.ts","../src/spending-period.ts","../src/window-reading.ts","../src/message-windows.ts","../src/spending-windows.ts"],"sourcesContent":["import { SpendingLimitReachedError, resolveTimeZone } from \"@ykurilov/agent-chat-core\";\nimport type {\n ChatStorage,\n ModelCatalog,\n SpendingLimits,\n SpendingPeriod,\n SpendingRefusalReason,\n SpendingScope,\n SpendingState,\n SpendingStatus,\n SpendingWindowStatus,\n TurnSpendingRules,\n UsageFilter,\n} from \"@ykurilov/agent-chat-core\";\nimport { limitStateStore } from \"./limit-state\";\nimport { giveBackMessageSlots, readMessageWindow, takeMessageSlots } from \"./message-windows\";\nimport type { MessageThresholdCrossing, MessageWindowReading } from \"./message-windows\";\nimport { calendarBounds } from \"./spending-period\";\nimport { normalizeWindows, validLimit } from \"./spending-windows\";\nimport type { SpendingSubject, SpendingWindow, SpendingWindowConfig } from \"./spending-windows\";\nimport { readWindow, windowStateKey } from \"./window-reading\";\nimport type { WindowReading, WindowState } from \"./window-reading\";\n\nexport type { CalendarWindowConfig, RollingWindowConfig, SpendingWindowConfig, TotalWindowConfig, WindowLimit } from \"./spending-windows\";\n\nconst DEFAULT_PERIOD: SpendingPeriod = \"month\";\nconst DEFAULT_WARN_AT = 0.8;\nconst DEFAULT_THRESHOLDS = [0.8, 1];\nconst STATE_SEVERITY: Record<SpendingState, number> = { ok: 0, warning: 1, exhausted: 2 };\n\n/** Бюджеты в долларах. Нет поля — на эту область лимита нет. */\nexport interface SpendingBudget {\n user?: number;\n chat?: number;\n}\n\n/** Правила хода. */\nexport interface TurnRulesConfig {\n /** Запас на старт: новый ход начинается, только если в каждом окне осталось не меньше стольких долларов. */\n minRemainingUsd?: number;\n /**\n * Что делать ходу, который дошёл до остатка окна: `finish` (по умолчанию) — доделать, перерасход окна остаётся в нём\n * или переносится в следующее (`carryOverrun` окна); `stop` — закончить итогом, как на потолке хода.\n */\n overrun?: \"finish\" | \"stop\";\n /** Потолок стоимости одного хода в долларах. */\n maxCostUsd?: number;\n /** Потолок вызовов инструментов за ход. */\n maxToolCalls?: number;\n}\n\n/** Событие порога: расход окна дошёл до доли `threshold` лимита. Одно на порог в каждом окне. */\nexport interface SpendingThresholdEvent {\n windowId: string;\n scope: SpendingScope;\n period: SpendingPeriod;\n hours?: number;\n threshold: number;\n share: number;\n spentUsd: number;\n limitUsd: number;\n /** Начало окна (ISO); `null` — окно на всё время. */\n windowStart: string | null;\n resetsAt: string | null;\n /** Чей расход дошёл до порога: у окна сервиса — человек, на чьём расходе это случилось. */\n userId: string;\n chatId?: string;\n}\n\n/**\n * Событие порога окна сообщений: счёт сообщений дошёл до доли `threshold` лимита. Одно на порог в каждом окне; после\n * сброса окна или в новом окне — снова.\n */\nexport interface MessageThresholdEvent {\n windowId: string;\n scope: SpendingScope;\n period: SpendingPeriod;\n hours?: number;\n threshold: number;\n share: number;\n /** Сообщений в окне вместе с этим. */\n used: number;\n limit: number;\n remaining: number;\n /** Начало окна (ISO); `null` — окно на всё время. */\n windowStart: string | null;\n resetsAt: string | null;\n /** Чьё сообщение дошло до порога: у окна сервиса — человек, на чьём сообщении это случилось. */\n userId: string;\n chatId?: string;\n}\n\n/** Личные настройки человека. */\nexport interface UserSpendingSettings {\n blocked: boolean;\n /** Свои лимиты окон по имени: число долларов (у окна сообщений — сообщений) или `null` — без лимита. */\n limits: Record<string, number | null>;\n}\n\n/** Правка личных настроек: `\"default\"` у окна — снова общий лимит. */\nexport interface UserSpendingSettingsPatch {\n blocked?: boolean;\n limits?: Record<string, number | null | \"default\">;\n}\n\nexport interface SpendingLimitsOptions {\n storage: ChatStorage;\n /**\n * Каталог, по которому сервис чата считает цену каждого хода. Здесь он нужен, чтобы при создании проверить,\n * что у всех пар «модель × провайдер» цена есть. Расход суммируется из записей журнала (доллары на момент хода),\n * поэтому смена цен и удаление модели из каталога прошлый расход не меняют.\n */\n catalog: ModelCatalog;\n /**\n * Окна лимитов: любое число одновременных окон, каждое со своей областью, сроком и лимитом. Ход начинается, только\n * если не исчерпано ни одно. Вместо `budget` и `period`.\n */\n windows?: readonly SpendingWindowConfig[];\n /**\n * Бюджет человека и чата (прежний способ, вместо `windows`). Функция от человека и чата: у разных тарифов разные\n * бюджеты. Зовётся на каждую проверку, так что смена тарифа действует сразу. Ошибка функции отклоняет ход.\n */\n budget?: (input: { userId: string; chatId?: string }) => SpendingBudget | Promise<SpendingBudget>;\n /** Период расхода к `budget`: одинаковый для обеих областей или свой для каждой. По умолчанию календарный месяц. */\n period?: SpendingPeriod | { user?: SpendingPeriod; chat?: SpendingPeriod };\n /**\n * Часовой пояс IANA (`Europe/Moscow`), по которому режутся сутки, недели и месяцы, если у окна нет своего. Та же\n * настройка — запасной пояс сегодняшней даты агента (если не задан `agent.timeZone`). Не задан — по UTC.\n * Не IANA-пояс — ошибка при создании.\n */\n timeZone?: string;\n /** С какой доли лимита показывать предупреждение, от 0 до 1 не включая границы. По умолчанию 0,8. */\n warnAt?: number;\n /** Пороги событий `onThreshold`: доли лимита больше 0 и не больше 1. По умолчанию 80% и 100%. */\n thresholds?: readonly number[];\n /**\n * Расход окна дошёл до порога (письмо, уведомление). Одно событие на порог в каждом окне; проверка — после\n * записи расхода (`afterSpending`). Ход обработчик не ждёт, ошибка уходит в `logError`.\n */\n onThreshold?: (event: SpendingThresholdEvent) => void | Promise<void>;\n /**\n * Счёт окна сообщений дошёл до порога (те же `thresholds`): «осталось N сообщений». Одно событие на порог в каждом\n * окне; проверка — когда сообщение берёт место. Сообщение потом не записалось — событие уже ушло, второго на тот же\n * порог не будет. Ход обработчик не ждёт, ошибка уходит в `logError`. Нет — событий о сообщениях нет.\n */\n onMessageThreshold?: (event: MessageThresholdEvent) => void | Promise<void>;\n /** Правила хода: запас на старт, перерасход, потолки денег и вызовов. */\n turn?: TurnRulesConfig;\n /**\n * Что делать, если у модели каталога нет цены у какого-то провайдера. `reject` (по умолчанию) — ошибка при создании\n * со списком пар; `ignore` — ходы такой модели записываются со стоимостью 0, а при создании в журнал сервера\n * один раз пишется предупреждение со списком пар.\n */\n unpricedModels?: \"reject\" | \"ignore\";\n /**\n * Кто живёт без лимитов (сотрудники, тестовые аккаунты): `true` — ни одно окно, ни денежное, ни сообщений, человека\n * не ограничивает. Зовётся на каждую проверку; ошибка функции отклоняет ход. Блокировку (`blocked`) не снимает.\n * Потолок одного хода (`turnRules`: `maxCostUsd`, `maxToolCalls`) тоже остаётся: это защита от бесконечного хода,\n * а не лимит человека.\n */\n exemptUsers?: (subject: { userId: string; chatId?: string }) => boolean | Promise<boolean>;\n /** Часы; по умолчанию настоящие. Нужны тестам. */\n now?: () => Date;\n /** Журнал сервера. По умолчанию `console.error`. */\n logError?: (message: string) => void;\n}\n\nexport interface ScopeSpending {\n /** Бюджет в долларах; `null` — лимита нет. */\n limitUsd: number | null;\n spentUsd: number;\n /** Сколько осталось; при перерасходе 0. */\n remainingUsd: number | null;\n /** Доля израсходованного (может быть больше 1). */\n share: number | null;\n state: SpendingState;\n period: SpendingPeriod;\n periodStart: string | null;\n resetsAt: string | null;\n}\n\n/** Окно лимита для страницы приложения: доллары и границы. */\nexport interface WindowSpending {\n id: string;\n scope: SpendingScope;\n period: SpendingPeriod;\n hours?: number;\n limitUsd: number;\n /** Расход в окне вместе с перенесённым перерасходом. */\n spentUsd: number;\n remainingUsd: number;\n share: number;\n state: SpendingState;\n /** Начало окна; `null` — на всё время или скользящее ещё не началось. */\n start: string | null;\n resetsAt: string | null;\n /** Перерасход прошлого окна, перенесённый в это. */\n carriedUsd: number;\n}\n\n/** Окно сообщений для страницы приложения: лимит, отправлено, осталось. */\nexport interface MessageWindowSpending {\n id: string;\n scope: SpendingScope;\n period: SpendingPeriod;\n hours?: number;\n limit: number;\n used: number;\n remaining: number;\n share: number;\n state: SpendingState;\n /** Начало окна; `null` — на всё время или скользящее ещё не началось. */\n start: string | null;\n resetsAt: string | null;\n}\n\nexport interface SpendingReport {\n user: ScopeSpending;\n /** Пояс лимитов, если задан: границы периода и дату обновления показывать по нему. */\n timeZone?: string;\n /** Есть, если спрошен чат. */\n chat?: ScopeSpending;\n /** Денежные окна с лимитом (лимиты с окнами). */\n windows?: WindowSpending[];\n /** Окна сообщений с лимитом; есть, только если такие окна заданы. */\n messageWindows?: MessageWindowSpending[];\n /** Человек заблокирован (лимиты с окнами). */\n blocked?: boolean;\n}\n\nexport interface SpendingLimitsService extends SpendingLimits {\n /** Расход и лимиты для страницы приложения («тариф»): доллары и границы периода. */\n getSpending(input: { userId: string; chatId?: string }): Promise<SpendingReport>;\n /** Личные настройки человека: блокировка и свои лимиты окон. Нужны методы состояния хранилища. */\n getUserSettings(userId: string): Promise<UserSpendingSettings>;\n setUserSettings(userId: string, patch: UserSpendingSettingsPatch): Promise<UserSpendingSettings>;\n /**\n * Ручной сброс окна: расход до этого момента в окне не считается, перенос стирается. Окно человека — с `userId`,\n * чата — с `chatId`, сервиса — без них. Скользящее окно закрывается: новое начнётся с первого запроса.\n */\n resetWindow(input: { windowId: string; userId?: string; chatId?: string }): Promise<void>;\n}\n\ninterface PersonState {\n blocked?: boolean;\n limits?: Record<string, number | null>;\n}\n\nexport function createSpendingLimits(options: SpendingLimitsOptions): SpendingLimitsService {\n const { storage } = options;\n const now = options.now ?? (() => new Date());\n const logError = options.logError ?? ((message: string) => console.error(message));\n const warnAt = options.warnAt ?? DEFAULT_WARN_AT;\n if (!(Number.isFinite(warnAt) && warnAt > 0 && warnAt < 1)) {\n throw new Error(`warnAt должен быть числом больше 0 и меньше 1, получено ${String(warnAt)}`);\n }\n const timeZone = checkedTimeZone(options.timeZone);\n const zone = timeZone === undefined ? {} : { timeZone };\n const thresholds = checkedThresholds(options.thresholds);\n const turn = checkedTurnRules(options.turn);\n const store = limitStateStore(storage);\n // Прежний способ (`budget`) отдаёт прежний вид отказа, состояния и отчёта: без окон.\n const legacy = options.windows === undefined;\n if (!legacy && options.budget) throw new Error(\"Лимиты задаются одним способом: windows или budget с period, не обоими\");\n if (legacy && !options.budget) throw new Error(\"Нужны окна лимитов (windows) или бюджет (budget)\");\n const allWindows = legacy ? legacyWindows(options.period, timeZone) : normalizeWindows(options.windows!, timeZone);\n // Деньги и сообщения считаются порознь: проверка хода, запас и пороги смотрят только на деньги.\n const windows = allWindows.filter((window) => window.measure === \"usd\");\n const messageWindows = allWindows.filter((window) => window.measure === \"messages\");\n const needsState = allWindows.find((window) => window.kind === \"rolling\" || window.measure === \"messages\");\n if (!store && needsState) {\n const why = needsState.measure === \"messages\" ? \"считает сообщения\" : \"скользящее\";\n throw new Error(`Окно «${needsState.id}» ${why}: хранилищу нужны методы getSpendingLimitState и saveSpendingLimitState`);\n }\n if (!store && options.onThreshold) throw new Error(\"onThreshold: хранилищу нужны методы getSpendingLimitState и saveSpendingLimitState\");\n const unpriced = findUnpricedModels(options.catalog);\n if (unpriced.length > 0) {\n if ((options.unpricedModels ?? \"reject\") === \"reject\") {\n throw new Error(\n `Нет цены у моделей: ${unpriced.join(\", \")}. Задайте pricing в профиле или подключите лимиты с unpricedModels: \"ignore\" (расход таких моделей не считается).`,\n );\n }\n logError(`Нет цены у моделей: ${unpriced.join(\", \")}. Их ходы записываются со стоимостью 0 и в лимиты не входят`);\n }\n\n function requireStore() {\n if (!store) throw new Error(\"Личные настройки и сброс окон: хранилищу нужны методы getSpendingLimitState и saveSpendingLimitState\");\n return store;\n }\n\n async function budgetFor(subject: SpendingSubject): Promise<SpendingBudget> {\n try {\n const budget = await options.budget!({ userId: subject.userId, ...(subject.chatId !== undefined && { chatId: subject.chatId }) });\n for (const scope of [\"user\", \"chat\"] as const) {\n const amount = budget[scope];\n if (amount !== undefined && !(typeof amount === \"number\" && Number.isFinite(amount) && amount >= 0)) {\n throw new Error(`Бюджет «${scope}» должен быть неотрицательным числом, получено ${String(amount)}`);\n }\n }\n return budget;\n } catch (failure) {\n logError(`Не удалось получить бюджет расходов человека ${subject.userId}: ${failure instanceof Error ? failure.message : \"неизвестная ошибка\"}`);\n throw failure;\n }\n }\n\n async function personOf(userId: string): Promise<PersonState> {\n return (store && (await store.read<PersonState>(`person:${userId}`))) ?? {};\n }\n\n async function isExempt(subject: SpendingSubject): Promise<boolean> {\n if (!options.exemptUsers) return false;\n try {\n return (await options.exemptUsers({ userId: subject.userId, ...(subject.chatId !== undefined && { chatId: subject.chatId }) })) === true;\n } catch (failure) {\n logError(`exemptUsers упал (человек ${subject.userId}): ${failure instanceof Error ? failure.message : \"неизвестная ошибка\"}`);\n throw failure;\n }\n }\n\n /**\n * Лимит каждого окна (и денег, и сообщений) для человека и чата: свой лимит человека, иначе общий. `undefined` —\n * у окна нет лимита; у человека без лимитов (`exemptUsers`) — ни у одного окна.\n */\n async function limitsFor(subject: SpendingSubject, person: PersonState): Promise<Map<string, number | undefined>> {\n if (await isExempt(subject)) return new Map();\n if (legacy) {\n const budget = await budgetFor(subject);\n return new Map([[\"user\", budget.user], [\"chat\", budget.chat]]);\n }\n const entries = await Promise.all(\n allWindows.map(async (window): Promise<[string, number | undefined]> => {\n const own = window.scope === \"service\" ? undefined : person.limits?.[window.id];\n if (own !== undefined) return [window.id, own === null ? undefined : own];\n return [window.id, await window.limit(subject)];\n }),\n );\n return new Map(entries);\n }\n\n async function sumSince(filter: UsageFilter, since: string | null): Promise<number> {\n const sums = await storage.sumUsage({ ...filter, ...(since !== null && { since }) });\n return sums.reduce((total, sum) => total + sum.costUsd, 0);\n }\n\n /** Окна с лимитом (и, для отчёта, без него) у человека и чата. */\n async function readAll(\n subject: SpendingSubject,\n person: PersonState,\n read: { open: boolean; withoutLimit?: (window: SpendingWindow) => boolean; pending?: { user: number; chat: number }; limits?: Map<string, number | undefined> },\n ): Promise<WindowReading[]> {\n const limits = read.limits ?? (await limitsFor(subject, person));\n const moment = now();\n const readings = await Promise.all(\n windows.map((window) => {\n const limitUsd = limits.get(window.id);\n if (limitUsd === undefined && !read.withoutLimit?.(window)) return null;\n const pendingUsd = read.pending && (window.scope === \"chat\" ? read.pending.chat : read.pending.user);\n return readWindow({ window, subject, limitUsd, now: moment, store, sumSince, warnAt, open: read.open, ...(pendingUsd !== undefined && { pendingUsd }) });\n }),\n );\n return readings.filter((reading): reading is WindowReading => reading !== null);\n }\n\n /** Переход окон в новое и открытие скользящих: запись ложится, только если окно никто не сменил раньше. */\n async function saveTransitions(readings: readonly WindowReading[]): Promise<void> {\n if (!store) return;\n await Promise.all(\n readings\n .filter((reading) => reading.nextState !== null)\n .map((reading) =>\n store.update<WindowState>(reading.stateKey, (current) => {\n const currentStart = current === null ? undefined : current.start;\n return currentStart === reading.savedStart ? reading.nextState : null;\n }),\n ),\n );\n }\n\n function refusal(reading: WindowReading, reason: SpendingRefusalReason): SpendingLimitReachedError {\n const { window } = reading;\n return new SpendingLimitReachedError({\n scope: window.scope,\n period: window.period,\n resetsAt: reading.resetsAt,\n ...zone,\n ...(!legacy && { reason, windowId: window.id, ...(window.hours !== undefined && { hours: window.hours }) }),\n });\n }\n\n async function notifyThresholds(reading: WindowReading, subject: SpendingSubject): Promise<void> {\n if (!store || !options.onThreshold || reading.waiting || reading.limitUsd === null || reading.share === null) return;\n const share = reading.share;\n let crossed: number[] = [];\n await store.update<WindowState>(reading.stateKey, (current) => {\n const base = current !== null && current.start === reading.start ? current : { start: reading.start, carriedUsd: reading.carriedUsd, notified: [] };\n crossed = thresholds.filter((threshold) => share >= threshold && !base.notified.includes(threshold));\n return crossed.length === 0 ? null : { ...base, notified: [...base.notified, ...crossed] };\n });\n const { window } = reading;\n for (const threshold of crossed) {\n const event: SpendingThresholdEvent = {\n windowId: window.id,\n scope: window.scope,\n period: window.period,\n ...(window.hours !== undefined && { hours: window.hours }),\n threshold,\n share,\n spentUsd: reading.spentUsd,\n limitUsd: reading.limitUsd,\n windowStart: reading.start,\n resetsAt: reading.resetsAt,\n userId: subject.userId,\n ...(subject.chatId !== undefined && { chatId: subject.chatId }),\n };\n const report = (failure: unknown) =>\n logError(`onThreshold упал (окно ${window.id}, порог ${threshold}): ${failure instanceof Error ? failure.message : String(failure)}`);\n try {\n void Promise.resolve(options.onThreshold(event)).catch(report);\n } catch (failure) {\n report(failure);\n }\n }\n }\n\n function notifyMessageThreshold(crossing: MessageThresholdCrossing, subject: SpendingSubject): void {\n const { window, threshold, used, limit } = crossing;\n const event: MessageThresholdEvent = {\n windowId: window.id,\n scope: window.scope,\n period: window.period,\n ...(window.hours !== undefined && { hours: window.hours }),\n threshold,\n share: limit === 0 ? 1 : used / limit,\n used,\n limit,\n remaining: Math.max(0, limit - used),\n windowStart: crossing.start,\n resetsAt: crossing.resetsAt,\n userId: subject.userId,\n ...(subject.chatId !== undefined && { chatId: subject.chatId }),\n };\n const report = (failure: unknown) =>\n logError(`onMessageThreshold упал (окно ${window.id}, порог ${threshold}): ${failure instanceof Error ? failure.message : String(failure)}`);\n try {\n void Promise.resolve(options.onMessageThreshold!(event)).catch(report);\n } catch (failure) {\n report(failure);\n }\n }\n\n /** Окно, где остаток меньше запаса на старт (самый малый остаток); исчерпанные окна — не про запас. */\n function shortOfReserve(readings: readonly WindowReading[]): WindowReading | null {\n const reserve = turn?.minRemainingUsd;\n if (reserve === undefined || readings.some((reading) => reading.state === \"exhausted\")) return null;\n const short = readings.filter((reading) => remainingOf(reading) < reserve - 1e-9);\n if (short.length === 0) return null;\n return short.reduce((least, reading) => (remainingOf(reading) < remainingOf(least) ? reading : least));\n }\n\n /** Окна сообщений с лимитом у человека и чата. */\n async function readMessages(subject: SpendingSubject, limits: Map<string, number | undefined>): Promise<MessageWindowReading[]> {\n if (messageWindows.length === 0) return [];\n const moment = now();\n const readings = await Promise.all(\n messageWindows.map((window) => {\n const limit = limits.get(window.id);\n return limit === undefined ? null : readMessageWindow({ window, subject, limit, now: moment, store: store!, warnAt });\n }),\n );\n return readings.filter((reading): reading is MessageWindowReading => reading !== null);\n }\n\n function findWindow(windowId: string): SpendingWindow {\n const window = allWindows.find((candidate) => candidate.id === windowId);\n if (!window) throw new Error(`Нет окна лимита «${windowId}»`);\n return window;\n }\n\n return {\n ...zone,\n\n async assertWithinLimit({ userId, chatId, pendingUsd }) {\n const subject = { userId, chatId };\n const person = await personOf(userId);\n if (person.blocked) {\n throw new SpendingLimitReachedError({ scope: \"user\", period: \"none\", resetsAt: null, ...zone, reason: \"blocked\" });\n }\n // Проверка перед ходом открывает скользящие окна; проверка посреди хода (картинки) — нет: окно уже идёт.\n const readings = await readAll(subject, person, { open: pendingUsd === undefined, ...(pendingUsd && { pending: pendingUsd }) });\n const exhausted = readings.filter((reading) => reading.state === \"exhausted\");\n const short = pendingUsd === undefined && exhausted.length === 0 ? shortOfReserve(readings) : null;\n if (exhausted.length > 0 || short) {\n // Окно, которое началось с долгом, записывается и при отказе: иначе долг считался бы заново из прошлого окна,\n // скользящее окно не открывалось бы никогда, а сброс уезжал бы вперёд.\n await saveTransitions(readings.filter((reading) => (reading.nextState?.carriedUsd ?? 0) > 0));\n }\n if (exhausted.length > 0) {\n // Открыть чат можно, только когда сняты все лимиты: называем тот, что снимется позже.\n throw refusal(exhausted.reduce((latest, reading) => (resetsLater(reading, latest) ? reading : latest)), \"limit\");\n }\n if (short) throw refusal(short, \"reserve\");\n await saveTransitions(readings);\n },\n\n async status({ userId, chatId }): Promise<SpendingStatus> {\n const subject = { userId, chatId };\n const person = await personOf(userId);\n const limits = await limitsFor(subject, person);\n const [readings, messageReadings] = await Promise.all([readAll(subject, person, { open: false, limits }), readMessages(subject, limits)]);\n // Пометка денежных окон появляется только рядом с окнами сообщений: прежние настройки — прежний ответ.\n const marked = messageWindows.length > 0;\n const windowStatuses = legacy ? undefined : [...readings.map((reading) => toWindowStatus(reading, marked)), ...messageReadings.map(toMessageWindowStatus)];\n const listed = windowStatuses ? { windows: windowStatuses } : {};\n if (person.blocked) return { state: \"exhausted\", reason: \"blocked\", blocked: true, ...zone, ...listed };\n const judged: Array<WindowReading | MessageWindowReading> = [...readings, ...messageReadings];\n if (judged.length === 0) return { state: \"ok\", ...listed };\n const short = shortOfReserve(readings);\n const worst = short ?? judged.reduce((current, reading) => (isWorse(reading, current) ? reading : current));\n const state = short ? \"exhausted\" : worst.state;\n const { window } = worst;\n return {\n state,\n ...(!legacy && state === \"exhausted\" && { reason: short ? (\"reserve\" as const) : (\"limit\" as const) }),\n scope: window.scope,\n period: window.period,\n share: worst.share!,\n resetsAt: worst.resetsAt,\n ...zone,\n ...(!legacy && { windowId: window.id, ...(window.hours !== undefined && { hours: window.hours }) }),\n ...(\"used\" in worst && { unit: \"messages\" as const, remaining: Math.max(0, worst.limit - worst.used) }),\n ...listed,\n };\n },\n\n async chatDeleted({ userId, chatId }) {\n const keys = allWindows.filter((window) => window.scope === \"chat\").map((window) => windowStateKey(window, { userId, chatId })!);\n await storage.deleteSpendingLimitStates?.(keys);\n },\n\n async takeMessage({ userId, chatId }) {\n if (messageWindows.length === 0) return undefined;\n const subject = { userId, chatId };\n const limits = await limitsFor(subject, await personOf(userId));\n const limited = messageWindows.flatMap((window) => {\n const limit = limits.get(window.id);\n return limit === undefined ? [] : [{ window, limit }];\n });\n if (limited.length === 0) return undefined;\n const { taken, refused, crossed } = await takeMessageSlots({\n windows: limited,\n subject,\n now: now(),\n store: store!,\n thresholds: options.onMessageThreshold ? thresholds : [],\n });\n if (refused.length > 0) {\n // Как у денег: называем окно, которое освободится позже.\n const latest = refused.reduce((current, candidate) => (resetsLater(candidate, current) ? candidate : current));\n const { window } = latest;\n throw new SpendingLimitReachedError({\n scope: window.scope,\n period: window.period,\n resetsAt: latest.resetsAt,\n ...zone,\n reason: \"limit\",\n windowId: window.id,\n ...(window.hours !== undefined && { hours: window.hours }),\n unit: \"messages\",\n });\n }\n for (const crossing of crossed) notifyMessageThreshold(crossing, subject);\n return taken.length > 0 ? () => giveBackMessageSlots(store!, taken) : undefined;\n },\n\n async turnRules({ userId, chatId }): Promise<TurnSpendingRules | undefined> {\n if (!turn) return undefined;\n let maxCostUsd = turn.maxCostUsd;\n if (turn.overrun === \"stop\") {\n const readings = await readAll({ userId, chatId }, await personOf(userId), { open: false });\n for (const reading of readings) {\n if (reading.limitUsd === null) continue;\n maxCostUsd = Math.min(maxCostUsd ?? Infinity, remainingOf(reading));\n }\n }\n const rules: TurnSpendingRules = {\n ...(maxCostUsd !== undefined && Number.isFinite(maxCostUsd) && { maxCostUsd }),\n ...(turn.maxToolCalls !== undefined && { maxToolCalls: turn.maxToolCalls }),\n };\n return Object.keys(rules).length > 0 ? rules : undefined;\n },\n\n async afterSpending({ userId, chatId }) {\n const subject = { userId, chatId };\n const readings = await readAll(subject, await personOf(userId), { open: false });\n await saveTransitions(readings);\n await Promise.all(readings.map((reading) => notifyThresholds(reading, subject)));\n },\n\n async getSpending({ userId, chatId }) {\n const subject = { userId, ...(chatId !== undefined && { chatId }) };\n const person = await personOf(userId);\n const limits = await limitsFor(subject, person);\n // Прежний вид: человек читается всегда, чат — если спрошен, даже без бюджета.\n const readings = await readAll(subject, person, { open: false, limits, withoutLimit: (window) => legacy && (window.scope === \"user\" || chatId !== undefined) });\n const firstOf = (scope: SpendingScope) => readings.find((reading) => reading.window.scope === scope);\n const user = firstOf(\"user\");\n const chat = chatId === undefined ? undefined : firstOf(\"chat\");\n const report: SpendingReport = {\n user: user ? toScopeSpending(user) : await unlimitedUser(userId),\n ...(chat && { chat: toScopeSpending(chat) }),\n ...zone,\n };\n if (legacy) return report;\n const messages = messageWindows.length > 0 && { messageWindows: (await readMessages(subject, limits)).map(toMessageWindowSpending) };\n return { ...report, windows: readings.filter((reading) => reading.limitUsd !== null).map(toWindowSpending), ...messages, blocked: person.blocked === true };\n },\n\n async getUserSettings(userId) {\n const person = store ? await personOf(userId) : {};\n return { blocked: person.blocked === true, limits: { ...person.limits } };\n },\n\n async setUserSettings(userId, patch) {\n const state = requireStore();\n if (legacy && patch.limits && Object.keys(patch.limits).length > 0) {\n throw new Error(\"Свои лимиты человека задаются только для окон (windows); с budget лимит человека — в самой функции budget\");\n }\n for (const [windowId, amount] of Object.entries(patch.limits ?? {})) {\n const window = findWindow(windowId);\n if (window.scope === \"service\") throw new Error(`Окно «${windowId}» — общее для сервиса: своего лимита у человека в нём нет`);\n if (amount !== null && amount !== \"default\") validLimit(amount, window.measure, `Свой лимит окна «${windowId}» (число, null или \"default\")`);\n }\n const { value } = await state.update<PersonState>(`person:${userId}`, (current) => {\n const limits = { ...current?.limits };\n for (const [windowId, amount] of Object.entries(patch.limits ?? {})) {\n if (amount === \"default\") delete limits[windowId];\n else limits[windowId] = amount;\n }\n return { blocked: patch.blocked ?? current?.blocked ?? false, limits };\n });\n return { blocked: value?.blocked === true, limits: { ...value?.limits } };\n },\n\n async resetWindow({ windowId, userId, chatId }) {\n const state = requireStore();\n const window = findWindow(windowId);\n if (window.scope === \"user\" && userId === undefined) throw new Error(`Окно «${windowId}» — окно человека: нужен userId`);\n if (window.scope === \"chat\" && chatId === undefined) throw new Error(`Окно «${windowId}» — окно чата: нужен chatId`);\n const subject = { userId: userId ?? \"\", ...(chatId !== undefined && { chatId }) };\n const key = windowStateKey(window, subject)!;\n const resetAt = now().toISOString();\n await state.update<WindowState>(key, () => {\n // Пороги после сброса шлются снова. Скользящее окно закрывается; календарное и на всё время — считаются с момента сброса.\n if (window.kind === \"rolling\") return { start: null, carriedUsd: 0, notified: [] };\n const reading = currentStart(window, now());\n return { start: reading, carriedUsd: 0, resetAt, notified: [] };\n });\n },\n };\n\n async function unlimitedUser(userId: string): Promise<ScopeSpending> {\n const spentUsd = await sumSince({ ownerId: userId }, null);\n return { limitUsd: null, spentUsd, remainingUsd: null, share: null, state: \"ok\", period: \"none\", periodStart: null, resetsAt: null };\n }\n}\n\nfunction currentStart(window: SpendingWindow, moment: Date): string | null {\n if (window.kind !== \"calendar\") return null;\n return readWindowStart(window, moment);\n}\n\nfunction readWindowStart(window: SpendingWindow, moment: Date): string {\n return calendarBounds(window.unit!, moment, window.timeZone, window.weekStartsOn).start;\n}\n\n/** Прежние области «человек» и «чат» как окна с именами `user` и `chat`. */\nfunction legacyWindows(period: SpendingLimitsOptions[\"period\"], timeZone: string | undefined): SpendingWindow[] {\n const periods = typeof period === \"object\" ? { user: period.user ?? DEFAULT_PERIOD, chat: period.chat ?? DEFAULT_PERIOD } : { user: period ?? DEFAULT_PERIOD, chat: period ?? DEFAULT_PERIOD };\n return ([\"user\", \"chat\"] as const).map((scope) => {\n const scopePeriod = periods[scope];\n const base = { id: scope, scope, carryOverrun: false, measure: \"usd\" as const, weekStartsOn: 1, timeZone: timeZone ?? \"UTC\", limit: async () => undefined };\n if (scopePeriod === \"none\") return { ...base, kind: \"total\", period: \"none\" };\n if (scopePeriod !== \"day\" && scopePeriod !== \"month\" && scopePeriod !== \"week\") {\n throw new Error(`period: ${String(scopePeriod)} — для budget только month, week, day или none; скользящие окна — через windows`);\n }\n return { ...base, kind: \"calendar\", unit: scopePeriod, period: scopePeriod };\n });\n}\n\nfunction checkedTimeZone(timeZone: string | undefined): string | undefined {\n if (timeZone === undefined) return undefined;\n if (resolveTimeZone(timeZone) === undefined) {\n throw new Error(`timeZone: «${String(timeZone)}» — не часовой пояс IANA; нужен, например, «Europe/Moscow»`);\n }\n return timeZone;\n}\n\nfunction checkedThresholds(thresholds: readonly number[] | undefined): number[] {\n const list = [...(thresholds ?? DEFAULT_THRESHOLDS)];\n for (const threshold of list) {\n if (!(Number.isFinite(threshold) && threshold > 0 && threshold <= 1)) {\n throw new Error(`thresholds: доли лимита больше 0 и не больше 1, получено ${String(threshold)}`);\n }\n }\n return [...new Set(list)].sort((a, b) => a - b);\n}\n\nfunction checkedTurnRules(turn: TurnRulesConfig | undefined): TurnRulesConfig | undefined {\n if (!turn) return undefined;\n for (const field of [\"minRemainingUsd\", \"maxCostUsd\"] as const) {\n const amount = turn[field];\n if (amount !== undefined && !(Number.isFinite(amount) && amount >= 0)) throw new Error(`turn.${field}: неотрицательное число долларов, получено ${String(amount)}`);\n }\n if (turn.maxToolCalls !== undefined && !(Number.isInteger(turn.maxToolCalls) && turn.maxToolCalls >= 0)) {\n throw new Error(`turn.maxToolCalls: целое число от 0, получено ${String(turn.maxToolCalls)}`);\n }\n if (turn.overrun !== undefined && turn.overrun !== \"finish\" && turn.overrun !== \"stop\") {\n throw new Error(`turn.overrun: finish или stop, получено ${String(turn.overrun)}`);\n }\n return turn;\n}\n\n/** Пары «модель × провайдер» каталога, у которых нет цены. */\nfunction findUnpricedModels(catalog: ModelCatalog): string[] {\n const missing: string[] = [];\n for (const model of catalog.models) {\n for (const providerId of Object.keys(model.providerModelIds)) {\n if (!model.pricing?.[providerId]) missing.push(`${model.id} × ${providerId}`);\n }\n }\n return missing;\n}\n\nfunction remainingOf(reading: WindowReading): number {\n return reading.limitUsd === null ? Infinity : reading.limitUsd - reading.spentUsd;\n}\n\n/** Обновление позже: без обновления (никогда) — самое позднее. */\nfunction resetsLater(candidate: { resetsAt: string | null }, current: { resetsAt: string | null }): boolean {\n if (candidate.resetsAt === null) return current.resetsAt !== null;\n if (current.resetsAt === null) return false;\n return candidate.resetsAt > current.resetsAt;\n}\n\nfunction isWorse(candidate: { state: SpendingState; share: number | null }, current: { state: SpendingState; share: number | null }): boolean {\n const bySeverity = STATE_SEVERITY[candidate.state] - STATE_SEVERITY[current.state];\n if (bySeverity !== 0) return bySeverity > 0;\n return (candidate.share ?? 0) > (current.share ?? 0);\n}\n\nfunction toWindowStatus(reading: WindowReading, marked: boolean): SpendingWindowStatus {\n const { window } = reading;\n return {\n id: window.id,\n scope: window.scope,\n period: window.period,\n ...(window.hours !== undefined && { hours: window.hours }),\n state: reading.state,\n share: reading.share ?? 0,\n resetsAt: reading.resetsAt,\n ...(marked && { unit: \"usd\" as const }),\n };\n}\n\nfunction toMessageWindowStatus(reading: MessageWindowReading): SpendingWindowStatus {\n const { window } = reading;\n return {\n id: window.id,\n scope: window.scope,\n period: window.period,\n ...(window.hours !== undefined && { hours: window.hours }),\n state: reading.state,\n share: reading.share,\n resetsAt: reading.resetsAt,\n unit: \"messages\",\n limit: reading.limit,\n used: reading.used,\n remaining: Math.max(0, reading.limit - reading.used),\n };\n}\n\nfunction toMessageWindowSpending(reading: MessageWindowReading): MessageWindowSpending {\n const { window } = reading;\n return {\n id: window.id,\n scope: window.scope,\n period: window.period,\n ...(window.hours !== undefined && { hours: window.hours }),\n limit: reading.limit,\n used: reading.used,\n remaining: Math.max(0, reading.limit - reading.used),\n share: reading.share,\n state: reading.state,\n start: reading.start,\n resetsAt: reading.resetsAt,\n };\n}\n\nfunction toWindowSpending(reading: WindowReading): WindowSpending {\n const { window } = reading;\n return {\n id: window.id,\n scope: window.scope,\n period: window.period,\n ...(window.hours !== undefined && { hours: window.hours }),\n limitUsd: reading.limitUsd!,\n spentUsd: reading.spentUsd,\n remainingUsd: Math.max(0, reading.limitUsd! - reading.spentUsd),\n share: reading.share!,\n state: reading.state,\n start: reading.start,\n resetsAt: reading.resetsAt,\n carriedUsd: reading.carriedUsd,\n };\n}\n\nfunction toScopeSpending(reading: WindowReading): ScopeSpending {\n return {\n limitUsd: reading.limitUsd,\n spentUsd: reading.spentUsd,\n remainingUsd: reading.limitUsd === null ? null : Math.max(0, reading.limitUsd - reading.spentUsd),\n share: reading.share,\n state: reading.state,\n period: reading.window.period,\n periodStart: reading.start,\n resetsAt: reading.resetsAt,\n };\n}\n","import type { ChatStorage } from \"@ykurilov/agent-chat-core\";\n\n/**\n * Сколько раз переписывать запись, которую успел изменить другой сервер. Внутри одного процесса правки одного ключа\n * идут по очереди и друг другу не мешают; спорят только процессы, их немного, и случайная пауза их разводит.\n */\nconst UPDATE_ATTEMPTS = 50;\n/** Наибольшая пауза перед повтором, мс: растёт с каждой неудачей до этого предела. */\nconst MAX_RETRY_PAUSE_MS = 32;\n\n/** Случайная пауза перед повтором: таймер, а не цикл, — цикл событий не стоит. */\nfunction retryPause(attempt: number): Promise<void> {\n const ceiling = Math.min(MAX_RETRY_PAUSE_MS, 2 ** attempt);\n return new Promise((resolve) => setTimeout(resolve, Math.random() * ceiling));\n}\n\nexport interface LimitStateStore {\n read<T>(key: string): Promise<T | null>;\n /**\n * Правка записи по версии: `change` получает текущее значение и возвращает новое или `null` — править нечего.\n * Правки одного ключа в этом процессе идут по очереди; запись успел изменить другой процесс — читает заново и\n * повторяет после короткой случайной паузы. Возвращает записанное (или прочитанное, если правки нет)\n * и было ли записано.\n */\n update<T>(key: string, change: (current: T | null) => T | null): Promise<{ value: T | null; written: boolean }>;\n}\n\n/** Состояние лимитов в хранилище чата: JSON по ключу. Нет методов у хранилища — `null`. */\nexport function limitStateStore(storage: ChatStorage): LimitStateStore | null {\n const get = storage.getSpendingLimitState?.bind(storage);\n const save = storage.saveSpendingLimitState?.bind(storage);\n if (!get || !save) return null;\n // Хвост очереди правок по каждому ключу. Общее окно (сервиса, человека) правят все ходы разом: без очереди они\n // мешали бы друг другу и тратили повторы.\n const queues = new Map<string, Promise<unknown>>();\n function inTurn<R>(key: string, work: () => Promise<R>): Promise<R> {\n const result = (queues.get(key) ?? Promise.resolve()).then(work);\n const tail = result.catch(() => undefined);\n queues.set(key, tail);\n void tail.then(() => {\n if (queues.get(key) === tail) queues.delete(key);\n });\n return result;\n }\n return {\n async read<T>(key: string) {\n const record = await get(key);\n return record ? (JSON.parse(record.value) as T) : null;\n },\n update<T>(key: string, change: (current: T | null) => T | null) {\n return inTurn(key, async () => {\n for (let attempt = 0; attempt < UPDATE_ATTEMPTS; attempt++) {\n if (attempt > 0) await retryPause(attempt);\n const record = await get(key);\n const current = record ? (JSON.parse(record.value) as T) : null;\n const next = change(current);\n if (next === null) return { value: current, written: false };\n if (await save(key, JSON.stringify(next), record?.version ?? null)) return { value: next, written: true };\n }\n throw new Error(`Состояние лимитов «${key}»: запись всё время меняют другие ходы`);\n });\n },\n };\n}\n","import type { SpendingState } from \"@ykurilov/agent-chat-core\";\n\n/** Доллары считаются дробями: сравнение идёт с точностью до миллиардной доли, чтобы 0,1 + 0,2 равнялось 0,3. */\nconst PRECISION = 1e9;\nconst round = (amount: number) => Math.round(amount * PRECISION) / PRECISION;\n\nexport interface ScopeState {\n state: SpendingState;\n /** Доля израсходованного от бюджета; при нулевом бюджете — 1. */\n share: number;\n}\n\nexport function spendingState(input: { spentUsd: number; limitUsd: number; warnAt: number }): ScopeState {\n const spent = round(input.spentUsd);\n const limit = round(input.limitUsd);\n const share = limit > 0 ? round(spent / limit) : 1;\n if (spent >= limit) return { state: \"exhausted\", share };\n if (share >= input.warnAt) return { state: \"warning\", share };\n return { state: \"ok\", share };\n}\n","import type { SpendingPeriod } from \"@ykurilov/agent-chat-core\";\n\nexport interface PeriodBounds {\n /** Начало периода (ISO, UTC); `null` — период без начала. */\n start: string | null;\n /** Когда расход обнулится (ISO, UTC); `null` — никогда. */\n resetsAt: string | null;\n}\n\n/**\n * Период, в который попадает момент `now`: месяц с 1-го числа, сутки с полуночи. Полночь — по часовому поясу\n * `timeZone` (IANA, уже проверенный), без него — по UTC. Сутки при переходе на летнее время длятся 23 или 25 часов.\n */\nexport function periodBounds(period: SpendingPeriod, now: Date, timeZone = \"UTC\"): PeriodBounds {\n if (period === \"none\" || period === \"rolling\") return { start: null, resetsAt: null };\n const { start, end } = calendarBounds(period, now, timeZone);\n return { start, resetsAt: end };\n}\n\nexport type CalendarUnit = \"day\" | \"week\" | \"month\";\n\n/**\n * Календарное окно, в которое попадает момент: сутки с полуночи, неделя с полуночи дня `weekStartsOn`\n * (1 — понедельник … 7 — воскресенье), месяц с 1-го числа. Полночь — по поясу `timeZone` (IANA, уже проверенный).\n */\nexport function calendarBounds(unit: CalendarUnit, now: Date, timeZone = \"UTC\", weekStartsOn = 1): { start: string; end: string } {\n const { year, month, day } = localDate(now, timeZone);\n if (unit === \"month\") return { start: localMidnight(year, month, 1, timeZone), end: localMidnight(year, month + 1, 1, timeZone) };\n if (unit === \"day\") return { start: localMidnight(year, month, day, timeZone), end: localMidnight(year, month, day + 1, timeZone) };\n // День недели этой даты: 1 — понедельник … 7 — воскресенье.\n const weekday = new Date(Date.UTC(year, month, day)).getUTCDay() || 7;\n const startDay = day - ((weekday - weekStartsOn + 7) % 7);\n return { start: localMidnight(year, month, startDay, timeZone), end: localMidnight(year, month, startDay + 7, timeZone) };\n}\n\nconst formatters = new Map<string, Intl.DateTimeFormat>();\n\n/** Часы и календарь пояса: один форматтер на пояс, создавать его дорого. */\nfunction formatterOf(timeZone: string): Intl.DateTimeFormat {\n let formatter = formatters.get(timeZone);\n if (!formatter) {\n formatter = new Intl.DateTimeFormat(\"en-US\", {\n timeZone,\n hourCycle: \"h23\",\n year: \"numeric\",\n month: \"numeric\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"numeric\",\n second: \"numeric\",\n });\n formatters.set(timeZone, formatter);\n }\n return formatter;\n}\n\n/** Дата и время на часах пояса в момент `moment`; месяц с нуля, как у `Date`. */\nfunction wallClock(moment: Date, timeZone: string) {\n const parts = Object.fromEntries(formatterOf(timeZone).formatToParts(moment).map((part) => [part.type, Number(part.value)]));\n return { year: parts.year!, month: parts.month! - 1, day: parts.day!, hour: parts.hour!, minute: parts.minute!, second: parts.second! };\n}\n\nfunction localDate(moment: Date, timeZone: string) {\n const { year, month, day } = wallClock(moment, timeZone);\n return { year, month, day };\n}\n\n/** На сколько часы пояса впереди UTC в момент `epochMs`, мс. */\nfunction offsetMs(epochMs: number, timeZone: string): number {\n const wall = wallClock(new Date(epochMs), timeZone);\n const wallAsUtc = Date.UTC(wall.year, wall.month, wall.day, wall.hour, wall.minute, wall.second);\n return wallAsUtc - Math.floor(epochMs / 1000) * 1000;\n}\n\nconst DAY_MS = 86_400_000;\n\n/**\n * Момент, когда на часах пояса начинается эта дата (переполнение дня и месяца — как у `Date.UTC`). Смещение пояса\n * спрашиваем у Intl за сутки до и через сутки после: в день перехода на летнее время оно меняется, и полночь\n * считается по тому смещению, что действует в неё. Полночи нет (часы перевели с 00:00 на 01:00) — первый момент суток.\n */\nfunction localMidnight(year: number, month: number, day: number, timeZone: string): string {\n const wallMidnight = Date.UTC(year, month, day);\n const target = new Date(wallMidnight);\n const candidates = [offsetMs(wallMidnight - DAY_MS, timeZone), offsetMs(wallMidnight + DAY_MS, timeZone)]\n .map((offset) => wallMidnight - offset)\n .filter((moment) => {\n const wall = wallClock(new Date(moment), timeZone);\n return wall.year === target.getUTCFullYear() && wall.month === target.getUTCMonth() && wall.day === target.getUTCDate();\n });\n return new Date(candidates.length > 0 ? Math.min(...candidates) : wallMidnight - offsetMs(wallMidnight, timeZone)).toISOString();\n}\n","import type { SpendingState, UsageFilter } from \"@ykurilov/agent-chat-core\";\nimport type { LimitStateStore } from \"./limit-state\";\nimport { calendarBounds } from \"./spending-period\";\nimport { spendingState } from \"./spending-state\";\nimport type { SpendingSubject, SpendingWindow } from \"./spending-windows\";\n\nconst HOUR_MS = 3_600_000;\n\n/**\n * Состояние окна в хранилище. `start` — начало окна, к которому относится запись (календарное — его начало,\n * скользящее — первый запрос, на всё время — `null`). Перешли в новое окно — перенос, сброс и пороги прошлого не действуют.\n */\nexport interface WindowState {\n start: string | null;\n /** Перерасход прошлого окна, перенесённый в это. */\n carriedUsd: number;\n /** Ручной сброс: расход до этого момента в окне не считается. */\n resetAt?: string;\n /** Пороги, о которых в этом окне уже сообщено. */\n notified: number[];\n}\n\nexport interface WindowReading {\n window: SpendingWindow;\n /** Ключ состояния окна этого человека, чата или сервиса. */\n stateKey: string;\n /** Лимит в долларах; `null` — лимита нет (читается только для отчёта). */\n limitUsd: number | null;\n /** Начало окна; `null` — окно на всё время или скользящее ещё не началось. */\n start: string | null;\n resetsAt: string | null;\n /** Скользящее окно ещё не началось: ждёт первого запроса. */\n waiting: boolean;\n /** Расход в окне вместе с переносом и заказанным, но не записанным. */\n spentUsd: number;\n carriedUsd: number;\n state: SpendingState;\n share: number | null;\n /** Состояние, которое стоит записать: окно сменилось или скользящее открыто этим запросом. */\n nextState: WindowState | null;\n /** Начало окна в прочитанной записи: новая запись ложится, только если её никто не сменил. */\n savedStart: string | null | undefined;\n}\n\nexport interface ReadWindowInput {\n window: SpendingWindow;\n subject: SpendingSubject;\n limitUsd: number | undefined;\n now: Date;\n store: LimitStateStore | null;\n sumSince: (filter: UsageFilter, since: string | null) => Promise<number>;\n warnAt: number;\n /** Скользящее окно, которое не идёт, открывается этим запросом (проверка перед ходом). */\n open: boolean;\n pendingUsd?: number;\n}\n\n/** Ключ состояния окна; у окна чата без чата — `null` (окно не читается). */\nexport function windowStateKey(window: SpendingWindow, subject: SpendingSubject): string | null {\n if (window.scope === \"service\") return `window:service:${window.id}`;\n if (window.scope === \"user\") return `window:user:${subject.userId}:${window.id}`;\n return subject.chatId === undefined ? null : `window:chat:${subject.chatId}:${window.id}`;\n}\n\nexport function usageFilterOf(window: SpendingWindow, subject: SpendingSubject): UsageFilter {\n if (window.scope === \"service\") return {};\n if (window.scope === \"user\") return { ownerId: subject.userId };\n return { ownerId: subject.userId, chatId: subject.chatId! };\n}\n\n/** Где сейчас окно: начало, конец и идёт ли оно. */\nexport function currentSpan(window: SpendingWindow, saved: WindowState | null, now: Date, open: boolean) {\n if (window.kind === \"total\") return { start: null, end: null, active: true };\n if (window.kind === \"calendar\") {\n const bounds = calendarBounds(window.unit!, now, window.timeZone, window.weekStartsOn);\n return { start: bounds.start, end: bounds.end, active: true };\n }\n const hoursMs = window.hours! * HOUR_MS;\n if (saved?.start && now.getTime() < Date.parse(saved.start) + hoursMs) {\n return { start: saved.start, end: new Date(Date.parse(saved.start) + hoursMs).toISOString(), active: true };\n }\n if (open) return { start: now.toISOString(), end: new Date(now.getTime() + hoursMs).toISOString(), active: true };\n return { start: null, end: null, active: false };\n}\n\nexport async function readWindow(input: ReadWindowInput): Promise<WindowReading | null> {\n const { window, subject, now, store } = input;\n const stateKey = windowStateKey(window, subject);\n if (stateKey === null) return null;\n const filter = usageFilterOf(window, subject);\n const saved = store ? await store.read<WindowState>(stateKey) : null;\n const span = currentSpan(window, saved, now, input.open);\n const sameWindow = saved !== null && span.active && saved.start === span.start;\n const limitUsd = input.limitUsd ?? null;\n let carriedUsd = 0;\n if (sameWindow) carriedUsd = saved.carriedUsd;\n else if (window.carryOverrun && limitUsd !== null) carriedUsd = await carryInto({ ...input, filter, saved, spanStart: span.start, limitUsd });\n const since = latest(span.start, sameWindow ? saved.resetAt : undefined);\n const recorded = span.active ? await input.sumSince(filter, since) : 0;\n const spentUsd = recorded + carriedUsd + Math.max(0, input.pendingUsd ?? 0);\n const judged = limitUsd === null ? { state: \"ok\" as const, share: null } : spendingState({ spentUsd, limitUsd, warnAt: input.warnAt });\n const changed = !sameWindow && span.active && store !== null && (window.kind === \"rolling\" || carriedUsd > 0 || saved !== null);\n return {\n window,\n stateKey,\n limitUsd,\n start: span.start,\n resetsAt: span.end,\n waiting: !span.active,\n spentUsd,\n carriedUsd,\n state: judged.state,\n share: judged.share,\n nextState: changed ? { start: span.start, carriedUsd, notified: [] } : null,\n savedStart: saved === null ? undefined : saved.start,\n };\n}\n\n/**\n * Перерасход прошлого окна, который переходит в новое: расход прошлого окна (с его переносом и после его сброса)\n * сверх лимита. Календарное окно — только соседнее прошлое; скользящее — последнее записанное.\n */\nasync function carryInto(\n input: ReadWindowInput & { filter: UsageFilter; saved: WindowState | null; spanStart: string | null; limitUsd: number },\n): Promise<number> {\n const { window, saved, filter, limitUsd } = input;\n if (window.kind === \"total\") return 0;\n if (window.kind === \"rolling\") {\n if (!saved?.start) return 0;\n const end = new Date(Date.parse(saved.start) + window.hours! * HOUR_MS).toISOString();\n const spent = (await spentBetween(input, filter, latest(saved.start, saved.resetAt), end)) + saved.carriedUsd;\n return Math.max(0, spent - limitUsd);\n }\n const start = input.spanStart!;\n const previous = calendarBounds(window.unit!, new Date(Date.parse(start) - 1), window.timeZone, window.weekStartsOn);\n const known = saved !== null && saved.start === previous.start;\n const since = latest(previous.start, known ? saved.resetAt : undefined);\n const spent = (await spentBetween(input, filter, since, start)) + (known ? saved.carriedUsd : 0);\n return Math.max(0, spent - limitUsd);\n}\n\nasync function spentBetween(input: ReadWindowInput, filter: UsageFilter, since: string | null, until: string): Promise<number> {\n if (since !== null && since >= until) return 0;\n const [fromStart, fromEnd] = await Promise.all([input.sumSince(filter, since), input.sumSince(filter, until)]);\n return fromStart - fromEnd;\n}\n\n/** Более поздний из моментов ISO; `null` и `undefined` — «без начала». */\nfunction latest(start: string | null, other: string | undefined): string | null {\n if (other === undefined) return start;\n if (start === null) return other;\n return other > start ? other : start;\n}\n","import type { SpendingState } from \"@ykurilov/agent-chat-core\";\nimport type { LimitStateStore } from \"./limit-state\";\nimport { spendingState } from \"./spending-state\";\nimport type { SpendingSubject, SpendingWindow } from \"./spending-windows\";\nimport { currentSpan, windowStateKey } from \"./window-reading\";\nimport type { WindowState } from \"./window-reading\";\n\n/**\n * Состояние окна сообщений: то же, что у денег, и число сообщений в окне с началом `start`. `count` — метка этого\n * счёта: новое окно и ручной сброс начинают новый счёт с новой меткой, и старое место в него уже не вернуть.\n */\ntype MessageWindowState = WindowState & { messages?: number; count?: string };\n\n/** Окно сообщений человека, чата или сервиса сейчас. */\nexport interface MessageWindowReading {\n window: SpendingWindow;\n limit: number;\n /** Сообщений в окне; скользящее, которое ещё не началось, — 0. */\n used: number;\n /** Начало окна; `null` — окно на всё время или скользящее ещё не началось. */\n start: string | null;\n resetsAt: string | null;\n state: SpendingState;\n share: number;\n}\n\n/** Место, взятое в окне: отдать его назад можно, только пока счёт тот же — окно не сменилось и не сброшено. */\nexport interface TakenMessageSlot {\n stateKey: string;\n start: string | null;\n /** Метка счёта, в который взято место. */\n count: string;\n /** Это место открыло скользящее окно: вернули последнее — окно снова закрыто, как будто сообщения не было. */\n openedWindow: boolean;\n /** Пороги, которые это место отметило пройденными. */\n crossed: number[];\n}\n\n/** Сообщение дошло до порога окна: о нём скажет `onMessageThreshold`. */\nexport interface MessageThresholdCrossing {\n window: SpendingWindow;\n threshold: number;\n used: number;\n limit: number;\n start: string | null;\n resetsAt: string | null;\n}\n\n/** Окно, в котором места не нашлось, и когда оно обновится. */\nexport interface MessageRefusal {\n window: SpendingWindow;\n resetsAt: string | null;\n}\n\n/** Окно сообщений и его лимит для этого человека. */\nexport interface LimitedMessageWindow {\n window: SpendingWindow;\n limit: number;\n}\n\n/** Сколько сообщений в окне: запись другого окна (прошлые сутки, кончившееся скользящее) не считается. */\nfunction usedIn(saved: MessageWindowState | null, start: string | null): number {\n return saved !== null && saved.start === start ? (saved.messages ?? 0) : 0;\n}\n\n/** Окно сообщений для статуса и отчёта: только чтение, скользящее не открывается. */\nexport async function readMessageWindow(input: {\n window: SpendingWindow;\n subject: SpendingSubject;\n limit: number;\n now: Date;\n store: LimitStateStore;\n warnAt: number;\n}): Promise<MessageWindowReading | null> {\n const { window, limit } = input;\n const stateKey = windowStateKey(window, input.subject);\n if (stateKey === null) return null;\n const saved = await input.store.read<MessageWindowState>(stateKey);\n const span = currentSpan(window, saved, input.now, false);\n const used = span.active ? usedIn(saved, span.start) : 0;\n const judged = spendingState({ spentUsd: used, limitUsd: limit, warnAt: input.warnAt });\n return { window, limit, used, start: span.start, resetsAt: span.end, state: judged.state, share: judged.share };\n}\n\n/**\n * Место под одно сообщение в каждом окне. Проверка и `+1` — одна условная запись по версии: два сообщения на последнее\n * место не пройдут оба. Первое же окно без места останавливает взятие; остальные окна только читаются, чтобы назвать\n * все исчерпанные. Есть отказы — взятое уже отдано назад. Пороги `thresholds`, до которых дошёл счёт, отмечаются той же\n * записью и возвращаются в `crossed`: каждый порог окна — один раз.\n */\nexport async function takeMessageSlots(input: {\n windows: readonly LimitedMessageWindow[];\n subject: SpendingSubject;\n now: Date;\n store: LimitStateStore;\n thresholds: readonly number[];\n}): Promise<{ taken: TakenMessageSlot[]; refused: MessageRefusal[]; crossed: MessageThresholdCrossing[] }> {\n const { store, now } = input;\n const taken: TakenMessageSlot[] = [];\n const refused: MessageRefusal[] = [];\n const crossed: MessageThresholdCrossing[] = [];\n try {\n for (const { window, limit } of input.windows) {\n const stateKey = windowStateKey(window, input.subject);\n if (stateKey === null) continue;\n if (refused.length > 0) {\n const saved = await store.read<MessageWindowState>(stateKey);\n const span = currentSpan(window, saved, now, true);\n if (usedIn(saved, span.start) >= limit) refused.push({ window, resetsAt: span.end });\n continue;\n }\n let outcome: { end: string | null; full: boolean; used: number; slot?: TakenMessageSlot } = { end: null, full: false, used: 0 };\n await store.update<MessageWindowState>(stateKey, (current) => {\n // Скользящее окно, которое не идёт, открывается этим сообщением.\n const span = currentSpan(window, current, now, true);\n const used = usedIn(current, span.start);\n outcome = { end: span.end, full: used >= limit, used: used + 1 };\n if (outcome.full) return null;\n const sameWindow = current !== null && current.start === span.start;\n const base: MessageWindowState = sameWindow ? current : { start: span.start, carriedUsd: 0, notified: [] };\n const count = (sameWindow && current.count) || crypto.randomUUID();\n const share = limit === 0 ? 1 : (used + 1) / limit;\n const reached = input.thresholds.filter((threshold) => share >= threshold && !base.notified.includes(threshold));\n outcome.slot = { stateKey, start: span.start, count, openedWindow: window.kind === \"rolling\" && !sameWindow, crossed: reached };\n return { ...base, start: span.start, messages: used + 1, count, notified: [...base.notified, ...reached] };\n });\n if (outcome.full) {\n refused.push({ window, resetsAt: outcome.end });\n continue;\n }\n const slot = outcome.slot!;\n taken.push(slot);\n for (const threshold of slot.crossed) crossed.push({ window, threshold, used: outcome.used, limit, start: slot.start, resetsAt: outcome.end });\n }\n } catch (failure) {\n await giveBackMessageSlots(store, taken, { unmarkThresholds: true });\n throw failure;\n }\n if (refused.length > 0) {\n // Сообщение не прошло: о порогах никто не узнал, они снова не пройдены.\n await giveBackMessageSlots(store, taken, { unmarkThresholds: true });\n return { taken: [], refused, crossed: [] };\n }\n return { taken, refused, crossed };\n}\n\n/**\n * Отдать взятые места: окно сменилось или сброшено — отдавать нечего. Скользящее окно, которое открыло это место,\n * без сообщений снова закрывается: следующее сообщение откроет его от своего времени. `unmarkThresholds` — о порогах,\n * которые отметило место, никто не узнал: они снова не пройдены. Без него отметки остаются: событие уже ушло, второго\n * на тот же порог не будет.\n */\nexport async function giveBackMessageSlots(\n store: LimitStateStore,\n taken: readonly TakenMessageSlot[],\n options: { unmarkThresholds?: boolean } = {},\n): Promise<void> {\n await Promise.all(\n taken.map((slot) =>\n store.update<MessageWindowState>(slot.stateKey, (current) => {\n if (current === null || current.start !== slot.start || current.count !== slot.count || !current.messages) return null;\n const messages = current.messages - 1;\n if (slot.openedWindow && messages === 0) return { carriedUsd: 0, notified: [], start: null };\n const notified = options.unmarkThresholds ? current.notified.filter((threshold) => !slot.crossed.includes(threshold)) : current.notified;\n return { ...current, messages, notified };\n }),\n ),\n );\n}\n","import { resolveTimeZone } from \"@ykurilov/agent-chat-core\";\nimport type { SpendingPeriod, SpendingScope } from \"@ykurilov/agent-chat-core\";\nimport type { CalendarUnit } from \"./spending-period\";\n\n/** Чей лимит считается: человек и чат хода. `chatId` нет — окна чата не читаются. */\nexport interface SpendingSubject {\n userId: string;\n chatId?: string;\n}\n\n/**\n * Лимит окна в долларах или сообщениях: число или функция от человека и чата (у тарифов разные лимиты). Функция\n * зовётся на каждую проверку, так что смена тарифа действует сразу; `undefined` или `null` — у этого человека окна нет.\n */\nexport type WindowLimit = number | ((subject: SpendingSubject) => number | null | undefined | Promise<number | null | undefined>);\n\ninterface WindowBase {\n /** Имя окна: по нему личные лимиты, ручной сброс, события порогов и экран. Уникальное. */\n id: string;\n /** Чей расход: человека (все его чаты), одного чата или всего сервиса (все люди вместе). */\n scope: SpendingScope;\n /** Лимит в долларах. У окна ровно один лимит: `limitUsd` или `limitMessages`. */\n limitUsd?: WindowLimit;\n /**\n * Лимит в сообщениях человека (целое число): считаются отправка и правка, переделка, повтор, служебные и ответы на\n * подтверждения — нет. Счёт ведётся в состоянии лимитов, удаление чата его не сбрасывает; нужны методы состояния\n * хранилища. Ход такого окна не останавливает: место берётся одно на сообщение до его записи.\n */\n limitMessages?: WindowLimit;\n /** Перерасход окна (ход доделан сверх лимита) переносится в начало следующего окна. По умолчанию нет. Только у денег. */\n carryOverrun?: boolean;\n}\n\n/** Календарное окно: сутки, неделя или месяц по часовому поясу. */\nexport interface CalendarWindowConfig extends WindowBase {\n kind: \"calendar\";\n unit: CalendarUnit;\n /** Пояс IANA; нет — пояс лимитов (`timeZone`), без него — UTC. */\n timeZone?: string;\n /** С какого дня начинается неделя: 1 — понедельник (по умолчанию) … 7 — воскресенье. */\n weekStartsOn?: number;\n}\n\n/** Скользящее окно: `hours` часов от первого запроса после конца прошлого окна. Нужны методы состояния хранилища. */\nexport interface RollingWindowConfig extends WindowBase {\n kind: \"rolling\";\n hours: number;\n}\n\n/** Окно на всё время: расход не обнуляется (сбросить можно вручную). */\nexport interface TotalWindowConfig extends WindowBase {\n kind: \"total\";\n}\n\nexport type SpendingWindowConfig = CalendarWindowConfig | RollingWindowConfig | TotalWindowConfig;\n\n/** Окно после проверки настроек: пояс и день недели подставлены, лимит — всегда функция. */\nexport interface SpendingWindow {\n id: string;\n scope: SpendingScope;\n kind: \"calendar\" | \"rolling\" | \"total\";\n unit?: CalendarUnit;\n timeZone: string;\n weekStartsOn: number;\n hours?: number;\n carryOverrun: boolean;\n period: SpendingPeriod;\n /** Чем меряется окно: доллары или сообщения человека. */\n measure: WindowMeasure;\n limit: (subject: SpendingSubject) => Promise<number | undefined>;\n}\n\nexport type WindowMeasure = \"usd\" | \"messages\";\n\nconst SCOPES: readonly SpendingScope[] = [\"user\", \"chat\", \"service\"];\nconst UNITS: readonly CalendarUnit[] = [\"day\", \"week\", \"month\"];\n\n/** Проверяет настройки окон и приводит их к одному виду. Ошибка — при создании лимитов, а не на первом ходе. */\nexport function normalizeWindows(configs: readonly SpendingWindowConfig[], defaultTimeZone: string | undefined): SpendingWindow[] {\n const seen = new Set<string>();\n return configs.map((config) => {\n const where = `Окно лимита «${String(config.id)}»`;\n if (typeof config.id !== \"string\" || config.id === \"\") throw new Error(\"Окно лимита: нужно непустое имя id\");\n if (seen.has(config.id)) throw new Error(`${where}: имя повторяется`);\n seen.add(config.id);\n if (!SCOPES.includes(config.scope)) throw new Error(`${where}: scope — user, chat или service, получено ${String(config.scope)}`);\n const hasUsd = config.limitUsd !== undefined;\n if (hasUsd === (config.limitMessages !== undefined)) throw new Error(`${where}: нужен ровно один лимит — limitUsd или limitMessages`);\n const measure: WindowMeasure = hasUsd ? \"usd\" : \"messages\";\n if (measure === \"messages\" && config.carryOverrun === true) throw new Error(`${where}: carryOverrun бывает только у окна денег (limitUsd)`);\n const limit = checkedLimit(hasUsd ? config.limitUsd! : config.limitMessages!, measure, where);\n const base = { id: config.id, scope: config.scope, carryOverrun: config.carryOverrun === true, measure, limit, weekStartsOn: 1 };\n if (config.kind === \"rolling\") {\n if (!(Number.isFinite(config.hours) && config.hours > 0)) throw new Error(`${where}: hours — число часов больше 0, получено ${String(config.hours)}`);\n return { ...base, kind: \"rolling\", hours: config.hours, timeZone: \"UTC\", period: \"rolling\" };\n }\n if (config.kind === \"total\") return { ...base, kind: \"total\", timeZone: \"UTC\", period: \"none\" };\n if (config.kind !== \"calendar\") throw new Error(`${where}: kind — calendar, rolling или total, получено ${String((config as { kind: unknown }).kind)}`);\n if (!UNITS.includes(config.unit)) throw new Error(`${where}: unit — day, week или month, получено ${String(config.unit)}`);\n const weekStartsOn = config.weekStartsOn ?? 1;\n if (!(Number.isInteger(weekStartsOn) && weekStartsOn >= 1 && weekStartsOn <= 7)) {\n throw new Error(`${where}: weekStartsOn — от 1 (понедельник) до 7 (воскресенье), получено ${String(weekStartsOn)}`);\n }\n const timeZone = config.timeZone ?? defaultTimeZone ?? \"UTC\";\n if (resolveTimeZone(timeZone) === undefined) throw new Error(`${where}: «${timeZone}» — не часовой пояс IANA`);\n return { ...base, kind: \"calendar\", unit: config.unit, timeZone, weekStartsOn, period: config.unit };\n });\n}\n\nfunction checkedLimit(limit: WindowLimit, measure: WindowMeasure, where: string): SpendingWindow[\"limit\"] {\n if (typeof limit === \"function\") {\n return async (subject) => {\n const amount = await limit(subject);\n if (amount === undefined || amount === null) return undefined;\n return validLimit(amount, measure, where);\n };\n }\n const amount = validLimit(limit, measure, where);\n return async () => amount;\n}\n\n/** Лимит окна: доллары — неотрицательное число, сообщения — целое неотрицательное. */\nexport function validLimit(amount: unknown, measure: WindowMeasure, where: string): number {\n if (measure === \"messages\") {\n if (typeof amount === \"number\" && Number.isInteger(amount) && amount >= 0) return amount;\n throw new Error(`${where}: limitMessages — целое неотрицательное число сообщений, получено ${String(amount)}`);\n }\n if (typeof amount === \"number\" && Number.isFinite(amount) && amount >= 0) return amount;\n throw new Error(`${where}: limitUsd — неотрицательное число долларов, получено ${String(amount)}`);\n}\n"],"mappings":"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","names":["resolveTimeZone","spent","amount","currentStart","latest","resolveTimeZone"]}
@@ -0,0 +1,221 @@
1
+ import type { ChatStorage, ModelCatalog, SpendingLimits, SpendingPeriod, SpendingScope, SpendingState } from "@ykurilov/agent-chat-core";
2
+ import type { SpendingWindowConfig } from "./spending-windows.js";
3
+ export type { CalendarWindowConfig, RollingWindowConfig, SpendingWindowConfig, TotalWindowConfig, WindowLimit } from "./spending-windows.js";
4
+ /** Бюджеты в долларах. Нет поля — на эту область лимита нет. */
5
+ export interface SpendingBudget {
6
+ user?: number;
7
+ chat?: number;
8
+ }
9
+ /** Правила хода. */
10
+ export interface TurnRulesConfig {
11
+ /** Запас на старт: новый ход начинается, только если в каждом окне осталось не меньше стольких долларов. */
12
+ minRemainingUsd?: number;
13
+ /**
14
+ * Что делать ходу, который дошёл до остатка окна: `finish` (по умолчанию) — доделать, перерасход окна остаётся в нём
15
+ * или переносится в следующее (`carryOverrun` окна); `stop` — закончить итогом, как на потолке хода.
16
+ */
17
+ overrun?: "finish" | "stop";
18
+ /** Потолок стоимости одного хода в долларах. */
19
+ maxCostUsd?: number;
20
+ /** Потолок вызовов инструментов за ход. */
21
+ maxToolCalls?: number;
22
+ }
23
+ /** Событие порога: расход окна дошёл до доли `threshold` лимита. Одно на порог в каждом окне. */
24
+ export interface SpendingThresholdEvent {
25
+ windowId: string;
26
+ scope: SpendingScope;
27
+ period: SpendingPeriod;
28
+ hours?: number;
29
+ threshold: number;
30
+ share: number;
31
+ spentUsd: number;
32
+ limitUsd: number;
33
+ /** Начало окна (ISO); `null` — окно на всё время. */
34
+ windowStart: string | null;
35
+ resetsAt: string | null;
36
+ /** Чей расход дошёл до порога: у окна сервиса — человек, на чьём расходе это случилось. */
37
+ userId: string;
38
+ chatId?: string;
39
+ }
40
+ /**
41
+ * Событие порога окна сообщений: счёт сообщений дошёл до доли `threshold` лимита. Одно на порог в каждом окне; после
42
+ * сброса окна или в новом окне — снова.
43
+ */
44
+ export interface MessageThresholdEvent {
45
+ windowId: string;
46
+ scope: SpendingScope;
47
+ period: SpendingPeriod;
48
+ hours?: number;
49
+ threshold: number;
50
+ share: number;
51
+ /** Сообщений в окне вместе с этим. */
52
+ used: number;
53
+ limit: number;
54
+ remaining: number;
55
+ /** Начало окна (ISO); `null` — окно на всё время. */
56
+ windowStart: string | null;
57
+ resetsAt: string | null;
58
+ /** Чьё сообщение дошло до порога: у окна сервиса — человек, на чьём сообщении это случилось. */
59
+ userId: string;
60
+ chatId?: string;
61
+ }
62
+ /** Личные настройки человека. */
63
+ export interface UserSpendingSettings {
64
+ blocked: boolean;
65
+ /** Свои лимиты окон по имени: число долларов (у окна сообщений — сообщений) или `null` — без лимита. */
66
+ limits: Record<string, number | null>;
67
+ }
68
+ /** Правка личных настроек: `"default"` у окна — снова общий лимит. */
69
+ export interface UserSpendingSettingsPatch {
70
+ blocked?: boolean;
71
+ limits?: Record<string, number | null | "default">;
72
+ }
73
+ export interface SpendingLimitsOptions {
74
+ storage: ChatStorage;
75
+ /**
76
+ * Каталог, по которому сервис чата считает цену каждого хода. Здесь он нужен, чтобы при создании проверить,
77
+ * что у всех пар «модель × провайдер» цена есть. Расход суммируется из записей журнала (доллары на момент хода),
78
+ * поэтому смена цен и удаление модели из каталога прошлый расход не меняют.
79
+ */
80
+ catalog: ModelCatalog;
81
+ /**
82
+ * Окна лимитов: любое число одновременных окон, каждое со своей областью, сроком и лимитом. Ход начинается, только
83
+ * если не исчерпано ни одно. Вместо `budget` и `period`.
84
+ */
85
+ windows?: readonly SpendingWindowConfig[];
86
+ /**
87
+ * Бюджет человека и чата (прежний способ, вместо `windows`). Функция от человека и чата: у разных тарифов разные
88
+ * бюджеты. Зовётся на каждую проверку, так что смена тарифа действует сразу. Ошибка функции отклоняет ход.
89
+ */
90
+ budget?: (input: {
91
+ userId: string;
92
+ chatId?: string;
93
+ }) => SpendingBudget | Promise<SpendingBudget>;
94
+ /** Период расхода к `budget`: одинаковый для обеих областей или свой для каждой. По умолчанию календарный месяц. */
95
+ period?: SpendingPeriod | {
96
+ user?: SpendingPeriod;
97
+ chat?: SpendingPeriod;
98
+ };
99
+ /**
100
+ * Часовой пояс IANA (`Europe/Moscow`), по которому режутся сутки, недели и месяцы, если у окна нет своего. Та же
101
+ * настройка — запасной пояс сегодняшней даты агента (если не задан `agent.timeZone`). Не задан — по UTC.
102
+ * Не IANA-пояс — ошибка при создании.
103
+ */
104
+ timeZone?: string;
105
+ /** С какой доли лимита показывать предупреждение, от 0 до 1 не включая границы. По умолчанию 0,8. */
106
+ warnAt?: number;
107
+ /** Пороги событий `onThreshold`: доли лимита больше 0 и не больше 1. По умолчанию 80% и 100%. */
108
+ thresholds?: readonly number[];
109
+ /**
110
+ * Расход окна дошёл до порога (письмо, уведомление). Одно событие на порог в каждом окне; проверка — после
111
+ * записи расхода (`afterSpending`). Ход обработчик не ждёт, ошибка уходит в `logError`.
112
+ */
113
+ onThreshold?: (event: SpendingThresholdEvent) => void | Promise<void>;
114
+ /**
115
+ * Счёт окна сообщений дошёл до порога (те же `thresholds`): «осталось N сообщений». Одно событие на порог в каждом
116
+ * окне; проверка — когда сообщение берёт место. Сообщение потом не записалось — событие уже ушло, второго на тот же
117
+ * порог не будет. Ход обработчик не ждёт, ошибка уходит в `logError`. Нет — событий о сообщениях нет.
118
+ */
119
+ onMessageThreshold?: (event: MessageThresholdEvent) => void | Promise<void>;
120
+ /** Правила хода: запас на старт, перерасход, потолки денег и вызовов. */
121
+ turn?: TurnRulesConfig;
122
+ /**
123
+ * Что делать, если у модели каталога нет цены у какого-то провайдера. `reject` (по умолчанию) — ошибка при создании
124
+ * со списком пар; `ignore` — ходы такой модели записываются со стоимостью 0, а при создании в журнал сервера
125
+ * один раз пишется предупреждение со списком пар.
126
+ */
127
+ unpricedModels?: "reject" | "ignore";
128
+ /**
129
+ * Кто живёт без лимитов (сотрудники, тестовые аккаунты): `true` — ни одно окно, ни денежное, ни сообщений, человека
130
+ * не ограничивает. Зовётся на каждую проверку; ошибка функции отклоняет ход. Блокировку (`blocked`) не снимает.
131
+ * Потолок одного хода (`turnRules`: `maxCostUsd`, `maxToolCalls`) тоже остаётся: это защита от бесконечного хода,
132
+ * а не лимит человека.
133
+ */
134
+ exemptUsers?: (subject: {
135
+ userId: string;
136
+ chatId?: string;
137
+ }) => boolean | Promise<boolean>;
138
+ /** Часы; по умолчанию настоящие. Нужны тестам. */
139
+ now?: () => Date;
140
+ /** Журнал сервера. По умолчанию `console.error`. */
141
+ logError?: (message: string) => void;
142
+ }
143
+ export interface ScopeSpending {
144
+ /** Бюджет в долларах; `null` — лимита нет. */
145
+ limitUsd: number | null;
146
+ spentUsd: number;
147
+ /** Сколько осталось; при перерасходе 0. */
148
+ remainingUsd: number | null;
149
+ /** Доля израсходованного (может быть больше 1). */
150
+ share: number | null;
151
+ state: SpendingState;
152
+ period: SpendingPeriod;
153
+ periodStart: string | null;
154
+ resetsAt: string | null;
155
+ }
156
+ /** Окно лимита для страницы приложения: доллары и границы. */
157
+ export interface WindowSpending {
158
+ id: string;
159
+ scope: SpendingScope;
160
+ period: SpendingPeriod;
161
+ hours?: number;
162
+ limitUsd: number;
163
+ /** Расход в окне вместе с перенесённым перерасходом. */
164
+ spentUsd: number;
165
+ remainingUsd: number;
166
+ share: number;
167
+ state: SpendingState;
168
+ /** Начало окна; `null` — на всё время или скользящее ещё не началось. */
169
+ start: string | null;
170
+ resetsAt: string | null;
171
+ /** Перерасход прошлого окна, перенесённый в это. */
172
+ carriedUsd: number;
173
+ }
174
+ /** Окно сообщений для страницы приложения: лимит, отправлено, осталось. */
175
+ export interface MessageWindowSpending {
176
+ id: string;
177
+ scope: SpendingScope;
178
+ period: SpendingPeriod;
179
+ hours?: number;
180
+ limit: number;
181
+ used: number;
182
+ remaining: number;
183
+ share: number;
184
+ state: SpendingState;
185
+ /** Начало окна; `null` — на всё время или скользящее ещё не началось. */
186
+ start: string | null;
187
+ resetsAt: string | null;
188
+ }
189
+ export interface SpendingReport {
190
+ user: ScopeSpending;
191
+ /** Пояс лимитов, если задан: границы периода и дату обновления показывать по нему. */
192
+ timeZone?: string;
193
+ /** Есть, если спрошен чат. */
194
+ chat?: ScopeSpending;
195
+ /** Денежные окна с лимитом (лимиты с окнами). */
196
+ windows?: WindowSpending[];
197
+ /** Окна сообщений с лимитом; есть, только если такие окна заданы. */
198
+ messageWindows?: MessageWindowSpending[];
199
+ /** Человек заблокирован (лимиты с окнами). */
200
+ blocked?: boolean;
201
+ }
202
+ export interface SpendingLimitsService extends SpendingLimits {
203
+ /** Расход и лимиты для страницы приложения («тариф»): доллары и границы периода. */
204
+ getSpending(input: {
205
+ userId: string;
206
+ chatId?: string;
207
+ }): Promise<SpendingReport>;
208
+ /** Личные настройки человека: блокировка и свои лимиты окон. Нужны методы состояния хранилища. */
209
+ getUserSettings(userId: string): Promise<UserSpendingSettings>;
210
+ setUserSettings(userId: string, patch: UserSpendingSettingsPatch): Promise<UserSpendingSettings>;
211
+ /**
212
+ * Ручной сброс окна: расход до этого момента в окне не считается, перенос стирается. Окно человека — с `userId`,
213
+ * чата — с `chatId`, сервиса — без них. Скользящее окно закрывается: новое начнётся с первого запроса.
214
+ */
215
+ resetWindow(input: {
216
+ windowId: string;
217
+ userId?: string;
218
+ chatId?: string;
219
+ }): Promise<void>;
220
+ }
221
+ export declare function createSpendingLimits(options: SpendingLimitsOptions): SpendingLimitsService;
@@ -0,0 +1,2 @@
1
+ export { createSpendingLimits } from "./create-spending-limits.js";
2
+ export type { CalendarWindowConfig, MessageWindowSpending, RollingWindowConfig, ScopeSpending, SpendingBudget, SpendingLimitsOptions, SpendingLimitsService, SpendingReport, SpendingThresholdEvent, MessageThresholdEvent, SpendingWindowConfig, TotalWindowConfig, TurnRulesConfig, UserSpendingSettings, UserSpendingSettingsPatch, WindowLimit, WindowSpending, } from "./create-spending-limits.js";
@@ -0,0 +1,16 @@
1
+ import type { ChatStorage } from "@ykurilov/agent-chat-core";
2
+ export interface LimitStateStore {
3
+ read<T>(key: string): Promise<T | null>;
4
+ /**
5
+ * Правка записи по версии: `change` получает текущее значение и возвращает новое или `null` — править нечего.
6
+ * Правки одного ключа в этом процессе идут по очереди; запись успел изменить другой процесс — читает заново и
7
+ * повторяет после короткой случайной паузы. Возвращает записанное (или прочитанное, если правки нет)
8
+ * и было ли записано.
9
+ */
10
+ update<T>(key: string, change: (current: T | null) => T | null): Promise<{
11
+ value: T | null;
12
+ written: boolean;
13
+ }>;
14
+ }
15
+ /** Состояние лимитов в хранилище чата: JSON по ключу. Нет методов у хранилища — `null`. */
16
+ export declare function limitStateStore(storage: ChatStorage): LimitStateStore | null;
@@ -0,0 +1,80 @@
1
+ import type { SpendingState } from "@ykurilov/agent-chat-core";
2
+ import type { LimitStateStore } from "./limit-state.js";
3
+ import type { SpendingSubject, SpendingWindow } from "./spending-windows.js";
4
+ /** Окно сообщений человека, чата или сервиса сейчас. */
5
+ export interface MessageWindowReading {
6
+ window: SpendingWindow;
7
+ limit: number;
8
+ /** Сообщений в окне; скользящее, которое ещё не началось, — 0. */
9
+ used: number;
10
+ /** Начало окна; `null` — окно на всё время или скользящее ещё не началось. */
11
+ start: string | null;
12
+ resetsAt: string | null;
13
+ state: SpendingState;
14
+ share: number;
15
+ }
16
+ /** Место, взятое в окне: отдать его назад можно, только пока счёт тот же — окно не сменилось и не сброшено. */
17
+ export interface TakenMessageSlot {
18
+ stateKey: string;
19
+ start: string | null;
20
+ /** Метка счёта, в который взято место. */
21
+ count: string;
22
+ /** Это место открыло скользящее окно: вернули последнее — окно снова закрыто, как будто сообщения не было. */
23
+ openedWindow: boolean;
24
+ /** Пороги, которые это место отметило пройденными. */
25
+ crossed: number[];
26
+ }
27
+ /** Сообщение дошло до порога окна: о нём скажет `onMessageThreshold`. */
28
+ export interface MessageThresholdCrossing {
29
+ window: SpendingWindow;
30
+ threshold: number;
31
+ used: number;
32
+ limit: number;
33
+ start: string | null;
34
+ resetsAt: string | null;
35
+ }
36
+ /** Окно, в котором места не нашлось, и когда оно обновится. */
37
+ export interface MessageRefusal {
38
+ window: SpendingWindow;
39
+ resetsAt: string | null;
40
+ }
41
+ /** Окно сообщений и его лимит для этого человека. */
42
+ export interface LimitedMessageWindow {
43
+ window: SpendingWindow;
44
+ limit: number;
45
+ }
46
+ /** Окно сообщений для статуса и отчёта: только чтение, скользящее не открывается. */
47
+ export declare function readMessageWindow(input: {
48
+ window: SpendingWindow;
49
+ subject: SpendingSubject;
50
+ limit: number;
51
+ now: Date;
52
+ store: LimitStateStore;
53
+ warnAt: number;
54
+ }): Promise<MessageWindowReading | null>;
55
+ /**
56
+ * Место под одно сообщение в каждом окне. Проверка и `+1` — одна условная запись по версии: два сообщения на последнее
57
+ * место не пройдут оба. Первое же окно без места останавливает взятие; остальные окна только читаются, чтобы назвать
58
+ * все исчерпанные. Есть отказы — взятое уже отдано назад. Пороги `thresholds`, до которых дошёл счёт, отмечаются той же
59
+ * записью и возвращаются в `crossed`: каждый порог окна — один раз.
60
+ */
61
+ export declare function takeMessageSlots(input: {
62
+ windows: readonly LimitedMessageWindow[];
63
+ subject: SpendingSubject;
64
+ now: Date;
65
+ store: LimitStateStore;
66
+ thresholds: readonly number[];
67
+ }): Promise<{
68
+ taken: TakenMessageSlot[];
69
+ refused: MessageRefusal[];
70
+ crossed: MessageThresholdCrossing[];
71
+ }>;
72
+ /**
73
+ * Отдать взятые места: окно сменилось или сброшено — отдавать нечего. Скользящее окно, которое открыло это место,
74
+ * без сообщений снова закрывается: следующее сообщение откроет его от своего времени. `unmarkThresholds` — о порогах,
75
+ * которые отметило место, никто не узнал: они снова не пройдены. Без него отметки остаются: событие уже ушло, второго
76
+ * на тот же порог не будет.
77
+ */
78
+ export declare function giveBackMessageSlots(store: LimitStateStore, taken: readonly TakenMessageSlot[], options?: {
79
+ unmarkThresholds?: boolean;
80
+ }): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import type { SpendingPeriod } from "@ykurilov/agent-chat-core";
2
+ export interface PeriodBounds {
3
+ /** Начало периода (ISO, UTC); `null` — период без начала. */
4
+ start: string | null;
5
+ /** Когда расход обнулится (ISO, UTC); `null` — никогда. */
6
+ resetsAt: string | null;
7
+ }
8
+ /**
9
+ * Период, в который попадает момент `now`: месяц с 1-го числа, сутки с полуночи. Полночь — по часовому поясу
10
+ * `timeZone` (IANA, уже проверенный), без него — по UTC. Сутки при переходе на летнее время длятся 23 или 25 часов.
11
+ */
12
+ export declare function periodBounds(period: SpendingPeriod, now: Date, timeZone?: string): PeriodBounds;
13
+ export type CalendarUnit = "day" | "week" | "month";
14
+ /**
15
+ * Календарное окно, в которое попадает момент: сутки с полуночи, неделя с полуночи дня `weekStartsOn`
16
+ * (1 — понедельник … 7 — воскресенье), месяц с 1-го числа. Полночь — по поясу `timeZone` (IANA, уже проверенный).
17
+ */
18
+ export declare function calendarBounds(unit: CalendarUnit, now: Date, timeZone?: string, weekStartsOn?: number): {
19
+ start: string;
20
+ end: string;
21
+ };
@@ -0,0 +1,11 @@
1
+ import type { SpendingState } from "@ykurilov/agent-chat-core";
2
+ export interface ScopeState {
3
+ state: SpendingState;
4
+ /** Доля израсходованного от бюджета; при нулевом бюджете — 1. */
5
+ share: number;
6
+ }
7
+ export declare function spendingState(input: {
8
+ spentUsd: number;
9
+ limitUsd: number;
10
+ warnAt: number;
11
+ }): ScopeState;