@ykurilov/agent-chat-background 0.0.0-stage → 0.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +376 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/dist/chunk-4RA3JT6K.js +9 -0
- package/dist/chunk-4RA3JT6K.js.map +1 -0
- package/dist/chunk-4XFSUFM6.js +53 -0
- package/dist/chunk-4XFSUFM6.js.map +1 -0
- package/dist/index.js +756 -0
- package/dist/index.js.map +1 -0
- package/dist/labels.js +14 -0
- package/dist/labels.js.map +1 -0
- package/dist/postgres.js +212 -0
- package/dist/postgres.js.map +1 -0
- package/dist/sqlite.js +184 -0
- package/dist/sqlite.js.map +1 -0
- package/dist/types/background-tasks.d.ts +18 -0
- package/dist/types/background-tool.d.ts +16 -0
- package/dist/types/helper-task.d.ts +17 -0
- package/dist/types/index.d.ts +5 -0
- package/dist/types/keyed-lock.d.ts +3 -0
- package/dist/types/labels.d.ts +10 -0
- package/dist/types/postgres.d.ts +9 -0
- package/dist/types/sqlite.d.ts +8 -0
- package/dist/types/store/memory.d.ts +3 -0
- package/dist/types/store/migration-settings.d.ts +4 -0
- package/dist/types/store/postgres-schema.d.ts +406 -0
- package/dist/types/store/postgres.d.ts +12 -0
- package/dist/types/store/sqlite-schema.d.ts +431 -0
- package/dist/types/store/sqlite.d.ts +6 -0
- package/dist/types/store/task-store.d.ts +23 -0
- package/dist/types/task-delivery.d.ts +24 -0
- package/dist/types/task-runner.d.ts +51 -0
- package/dist/types/task-tools.d.ts +13 -0
- package/dist/types/types.d.ts +97 -0
- package/drizzle/postgres/0000_init.sql +28 -0
- package/drizzle/postgres/0001_delivery_key.sql +1 -0
- package/drizzle/postgres/meta/0000_snapshot.json +229 -0
- package/drizzle/postgres/meta/0001_snapshot.json +235 -0
- package/drizzle/postgres/meta/_journal.json +20 -0
- package/drizzle/sqlite/0000_init.sql +28 -0
- package/drizzle/sqlite/0001_delivery_key.sql +1 -0
- package/drizzle/sqlite/meta/0000_snapshot.json +208 -0
- package/drizzle/sqlite/meta/0001_snapshot.json +215 -0
- package/drizzle/sqlite/meta/_journal.json +20 -0
- package/package.json +75 -3
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/background-tool.ts","../src/background-tasks.ts","../src/helper-task.ts","../src/task-delivery.ts","../src/task-runner.ts","../src/keyed-lock.ts","../src/task-tools.ts","../src/store/memory.ts"],"sourcesContent":["import type { AgentToolsModel } from \"@ykurilov/agent-chat-core\";\nimport type { Tool } from \"ai\";\nimport type { NewTask, RunControl, TaskOutcome, TaskRunner } from \"./task-runner\";\nimport type { BackgroundToolDefinition, TaskRecord } from \"./types\";\n\n/** Фоновый инструмент: агент запускает его, ход идёт дальше, итог придёт служебным сообщением. */\nexport function backgroundTool<Input, Output>(definition: BackgroundToolDefinition<Input, Output>): BackgroundToolDefinition {\n return definition as unknown as BackgroundToolDefinition;\n}\n\n/** Поля хода для записи задачи; без номера ответа и канала (вызов вне хода) — пустой номер и веб. */\nexport function turnFields(model: AgentToolsModel): Pick<NewTask, \"ownerId\" | \"chatId\" | \"messageId\" | \"channel\"> {\n return { ownerId: model.ownerId, chatId: model.chatId, messageId: model.messageId ?? \"\", channel: model.channel ?? \"web\" };\n}\n\nexport const TOO_MANY_TASKS = (max: number) =>\n `Слишком много фоновых задач: одновременно идёт не больше ${max}. Дождитесь, пока одна из них закончится, или отмените ненужную (cancel_task).`;\n\n/** Инструмент хода для фонового инструмента: запись задачи и ответ «запущена» сразу, без ожидания итога. */\nexport function backgroundToolForTurn(\n name: string,\n definition: BackgroundToolDefinition,\n model: AgentToolsModel,\n service: { runner(): TaskRunner; maxRunningPerUser: number },\n): Tool {\n return {\n description: definition.description,\n inputSchema: definition.inputSchema,\n ...(definition.needsApproval !== undefined && { needsApproval: definition.needsApproval }),\n ...(definition.metadata !== undefined && { metadata: definition.metadata }),\n async execute(input: unknown, options: { toolCallId: string }) {\n const title = definition.title(input);\n const started = await service\n .runner()\n .start({ ...turnFields(model), toolCallId: options.toolCallId, kind: \"tool\", name, input, title }, service.maxRunningPerUser);\n if (started.status === \"too_many_tasks\") return { status: \"too_many_tasks\", message: TOO_MANY_TASKS(service.maxRunningPerUser) };\n return { status: \"started\", taskId: started.taskId, title };\n },\n } as Tool;\n}\n\n/** Работа фонового инструмента в бегунке: итог — `done`, ошибка — `failed`. */\nexport async function runBackgroundTool(definition: BackgroundToolDefinition, record: TaskRecord, control: RunControl): Promise<TaskOutcome> {\n const result = await definition.run(record.input, {\n ownerId: record.ownerId,\n chatId: record.chatId,\n signal: control.signal,\n reportProgress: control.reportProgress,\n });\n return { status: \"done\", result };\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { BackgroundPort, BackgroundTasks, TaskView } from \"@ykurilov/agent-chat-core\";\nimport type { ToolSet } from \"ai\";\nimport { backgroundToolForTurn, runBackgroundTool } from \"./background-tool\";\nimport { backgroundHelpers, HELPER_TOOL_PREFIX, helperToolForTurn, runHelperTask } from \"./helper-task\";\nimport { createTaskDelivery } from \"./task-delivery\";\nimport { createTaskRunner } from \"./task-runner\";\nimport type { TaskPlan } from \"./task-runner\";\nimport { backgroundInstructions, CANCEL_TASK, CHECK_TASKS, taskToolsForTurn } from \"./task-tools\";\nimport type { BackgroundToolDefinition, TaskRecord, TaskStore } from \"./types\";\n\nconst DEFAULT_MAX_RUNNING_PER_USER = 3;\nconst DEFAULT_TIMEOUT_MS = 30 * 60_000;\nconst DEFAULT_LEASE_MS = 30_000;\n/** Меньше аренды не бывает: продление раз в треть аренды должно успевать даже под нагрузкой. */\nconst MIN_LEASE_MS = 3_000;\n/** Как у имён инструментов у провайдеров моделей. */\nconst TOOL_NAME = /^[a-zA-Z0-9_-]{1,64}$/;\n\nexport interface BackgroundTasksOptions {\n /** `memoryTaskStore()`, `sqliteTaskStore(path)`, `postgresTaskStore(pool)`. */\n store: TaskStore;\n tools?: Record<string, BackgroundToolDefinition>;\n /** Сколько задач человека идёт одновременно; по умолчанию 3. */\n maxRunningPerUser?: number;\n /** Предел времени задачи; по умолчанию 30 минут. */\n timeoutMs?: number;\n /** Аренда задачи; по умолчанию 30 с, продление раз в треть аренды. */\n leaseMs?: number;\n}\n\n/**\n * Фоновые задачи для службы чата: агент запускает долгую работу, разговор идёт дальше, итог приходит служебным\n * сообщением. Задачи переживают перезапуск сервера; несколько копий сервера на одной базе делят их по аренде.\n */\nexport function createBackgroundTasks(options: BackgroundTasksOptions): BackgroundTasks {\n const { store } = options;\n const tools = options.tools ?? {};\n const maxRunningPerUser = options.maxRunningPerUser ?? DEFAULT_MAX_RUNNING_PER_USER;\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const leaseMs = options.leaseMs ?? DEFAULT_LEASE_MS;\n checkOptions(tools, maxRunningPerUser, timeoutMs, leaseMs);\n\n const leaseOwner = randomUUID();\n let port: BackgroundPort | undefined;\n const requirePort = (): BackgroundPort => {\n if (!port) throw new Error(\"Фоновые задачи не подключены к службе чата: передайте их в createChatServer — ядро вызовет attach\");\n return port;\n };\n\n function plan(record: TaskRecord): TaskPlan {\n if (record.kind === \"tool\") {\n const definition = tools[record.name];\n if (!definition) return `Фонового инструмента «${record.name}» больше нет в настройках службы`;\n return {\n work: (task, control) => runBackgroundTool(definition, task, control),\n rerunAfterRestart: definition.rerunAfterRestart === true,\n timeoutMs: definition.timeoutMs ?? timeoutMs,\n };\n }\n const helper = backgroundHelpers(port)[record.name];\n if (!helper) return `Фонового помощника «${record.name}» больше нет в настройках службы`;\n return { work: (task, control) => runHelperTask(requirePort(), helper, task, control), rerunAfterRestart: true, timeoutMs };\n }\n\n const delivery = createTaskDelivery({ store, leaseOwner, port: requirePort });\n const runner = createTaskRunner({ store, leaseOwner, leaseMs, port: requirePort, delivery, plan });\n const service = { store, maxRunningPerUser, runner: () => (requirePort(), runner) };\n\n let timer: ReturnType<typeof setInterval> | undefined;\n let unsubscribe: (() => void) | undefined;\n let recovering = false;\n let renewing: Promise<void> | undefined;\n let sweeping: Promise<void> | undefined;\n\n /**\n * Обход по таймеру: продлить аренды; отдельно — повторить недоставленное и, после `recover`, подобрать брошенные\n * задачи. Продление не ждёт доставки и подбора: на медленной базе они идут дольше аренды.\n */\n function tick(): void {\n renewing ??= runner.renewLeases().finally(() => {\n renewing = undefined;\n });\n sweeping ??= (async () => {\n await delivery.retryPending();\n if (recovering) await runner.recoverExpired().catch((error: unknown) => port?.logError(error));\n })().finally(() => {\n sweeping = undefined;\n });\n }\n\n return {\n instructions: backgroundInstructions(maxRunningPerUser),\n get toolNames() {\n return [...Object.keys(tools), ...Object.keys(backgroundHelpers(port)).map((name) => HELPER_TOOL_PREFIX + name), CHECK_TASKS, CANCEL_TASK];\n },\n tools(model) {\n const set: ToolSet = {};\n for (const [name, definition] of Object.entries(tools)) set[name] = backgroundToolForTurn(name, definition, model, service);\n for (const [name, helper] of Object.entries(backgroundHelpers(port))) set[HELPER_TOOL_PREFIX + name] = helperToolForTurn(name, helper, model, service);\n return { ...set, ...taskToolsForTurn(model, service) };\n },\n attach(attached) {\n if (port) throw new Error(\"Фоновые задачи уже подключены к службе чата: один экземпляр — одна служба\");\n port = attached;\n unsubscribe = attached.events.subscribe((event) => {\n if (event.type === \"turn-finished\") delivery.chatFree(event.ownerId, event.chatId);\n });\n timer = setInterval(tick, Math.floor(leaseMs / 3));\n timer.unref?.();\n },\n async listForChat(ownerId, chatId) {\n return (await store.listForChat(ownerId, chatId)).map(viewOf);\n },\n cancel: (ownerId, chatId, taskId) => service.runner().cancel(ownerId, chatId, taskId, \"human\"),\n answerApproval: (ownerId, chatId, taskId, approvalId, decision) => service.runner().answerApproval(ownerId, chatId, taskId, approvalId, decision),\n chatDeleted: (ownerId, chatId) => service.runner().chatDeleted(ownerId, chatId),\n async recover() {\n requirePort();\n recovering = true;\n await runner.recoverExpired();\n },\n async flush() {\n clearInterval(timer);\n unsubscribe?.();\n recovering = false;\n await Promise.all([renewing, sweeping]);\n await runner.stop();\n await delivery.idle();\n },\n };\n}\n\nfunction checkOptions(tools: Record<string, BackgroundToolDefinition>, maxRunningPerUser: number, timeoutMs: number, leaseMs: number): void {\n if (!Number.isInteger(maxRunningPerUser) || maxRunningPerUser < 1) throw new Error(\"Фоновые задачи: maxRunningPerUser — целое число от 1\");\n if (!(timeoutMs > 0)) throw new Error(\"Фоновые задачи: timeoutMs — больше нуля\");\n if (!(leaseMs >= MIN_LEASE_MS)) throw new Error(`Фоновые задачи: leaseMs — не меньше ${MIN_LEASE_MS} мс`);\n for (const [name, definition] of Object.entries(tools)) {\n if (!TOOL_NAME.test(name)) throw new Error(`Фоновый инструмент «${name}»: имя — латиница, цифры, «_» и «-», до 64 знаков`);\n if (name === CHECK_TASKS || name === CANCEL_TASK || name.startsWith(HELPER_TOOL_PREFIX)) {\n throw new Error(`Фоновый инструмент «${name}»: имя занято инструментами фона или помощников`);\n }\n if (definition.timeoutMs !== undefined && !(definition.timeoutMs > 0)) throw new Error(`Фоновый инструмент «${name}»: timeoutMs — больше нуля`);\n }\n}\n\nfunction viewOf(task: TaskRecord): TaskView {\n return {\n id: task.id,\n title: task.title,\n kind: task.kind,\n name: task.name,\n status: task.status,\n startedAt: task.startedAt ?? task.createdAt,\n ...(task.finishedAt && { finishedAt: task.finishedAt }),\n ...(task.progress && { progress: task.progress }),\n };\n}\n","import type { AgentHelper, AgentToolsModel, BackgroundPort, HelperProgress } from \"@ykurilov/agent-chat-core\";\nimport { jsonSchema } from \"ai\";\nimport type { Tool } from \"ai\";\nimport { TOO_MANY_TASKS, turnFields } from \"./background-tool\";\nimport type { RunControl, TaskOutcome, TaskRunner } from \"./task-runner\";\nimport type { TaskRecord } from \"./types\";\n\nexport const HELPER_TOOL_PREFIX = \"helper_\";\n/** Название задачи помощника — начало задания. */\nconst TITLE_MAX_LENGTH = 80;\n\nconst helperInput = jsonSchema<{ task: string }>({\n type: \"object\",\n properties: { task: { type: \"string\", description: \"Задание помощнику: что сделать и что вернуть\" } },\n required: [\"task\"],\n additionalProperties: false,\n});\n\n/** Фоновые помощники из настроек службы: ядро отдаёт их в порте. */\nexport function backgroundHelpers(port: BackgroundPort | undefined): Record<string, AgentHelper> {\n const helpers = port?.helpers ?? {};\n return Object.fromEntries(Object.entries(helpers).filter(([, helper]) => helper.background === true));\n}\n\n/** Что модель основного агента видит из ответа `helper_<имя>`: как у помощников ядра. */\nfunction helperModelOutput(output: unknown): { type: \"text\"; value: string } {\n const progress = output as Partial<HelperProgress>;\n if (progress?.status === \"background\") {\n return { type: \"text\", value: `Помощник работает в фоне${progress.taskId ? ` (задача ${progress.taskId})` : \"\"}: итог придёт позже.` };\n }\n return { type: \"text\", value: `Помощник не справился: ${progress?.error ?? \"неизвестная ошибка\"}` };\n}\n\n/** `helper_<имя>` фонового помощника: создаёт задачу и сразу отвечает «работает в фоне». */\nexport function helperToolForTurn(\n name: string,\n helper: AgentHelper,\n model: AgentToolsModel,\n service: { runner(): TaskRunner; maxRunningPerUser: number },\n): Tool {\n return {\n description: helper.description,\n inputSchema: helperInput,\n toModelOutput: ({ output }: { output: unknown }) => helperModelOutput(output),\n async execute({ task }: { task: string }, options: { toolCallId: string }): Promise<HelperProgress> {\n const started = await service\n .runner()\n .start(\n { ...turnFields(model), toolCallId: options.toolCallId, kind: \"helper\", name, input: { task }, title: titleOf(task) },\n service.maxRunningPerUser,\n );\n if (started.status === \"too_many_tasks\") {\n const message = TOO_MANY_TASKS(service.maxRunningPerUser);\n return { task, steps: [], text: message, status: \"failed\", error: message };\n }\n return { task, steps: [], text: \"\", status: \"background\", taskId: started.taskId };\n },\n } as unknown as Tool;\n}\n\n/**\n * Работа фонового помощника в бегунке: цикл ядра (`port.runHelper`) с сохранёнными шагами. После каждого шага шаги и ход\n * пишутся в задачу: после перезапуска цикл продолжит с них, траты за старые шаги не повторятся.\n */\nexport async function runHelperTask(port: BackgroundPort, helper: AgentHelper, record: TaskRecord, control: RunControl): Promise<TaskOutcome> {\n const { task } = record.input as { task: string };\n let lastProgress: HelperProgress | undefined = record.progress;\n const model = await port.helperModel(record.name, record.ownerId, record.chatId);\n const result = await port.runHelper({\n name: record.name,\n helper,\n task,\n model,\n ...(record.helperMessages && { priorMessages: record.helperMessages }),\n abortSignal: control.signal,\n async onStep(step) {\n lastProgress = step.progress;\n await control.saveStep({ helperMessages: step.messages, progress: step.progress });\n },\n });\n const progress: HelperProgress = { ...(lastProgress ?? { task, steps: [] }), text: result.text, status: result.status };\n switch (result.status) {\n case \"done\":\n return { status: \"done\", result: result.text, helperMessages: result.messages, progress };\n case \"failed\":\n return { status: \"failed\", error: result.text, helperMessages: result.messages, progress: { ...progress, error: result.text } };\n case \"awaiting_approval\":\n // Пауза: шаги и вопрос человеку сохранены, служебное сообщение покажет подтверждение.\n return { status: \"awaiting_approval\", helperMessages: result.messages, progress: lastProgress ?? progress };\n }\n}\n\nfunction titleOf(task: string): string {\n const line = task.trim().replace(/\\s+/g, \" \");\n return line.length <= TITLE_MAX_LENGTH ? line : `${line.slice(0, TITLE_MAX_LENGTH - 1)}…`;\n}\n","import type { AgentNotice, BackgroundPort } from \"@ykurilov/agent-chat-core\";\nimport { NOTICE_STATUSES } from \"./store/task-store\";\nimport type { TaskRecord, TaskStore } from \"./types\";\n\n/** Предел итога в служебном сообщении: как предел результата инструмента в ядре. */\nconst NOTICE_RESULT_MAX_LENGTH = 50_000;\n\n/** Без пробуждения агента: человек сам отменил или задача ждёт его решения. Доставляются раньше остальных. */\nconst isQuiet = (task: TaskRecord) => task.status === \"canceled\" || task.status === \"awaiting_approval\";\n\nexport interface TaskDelivery {\n /** В чате есть недоставленные итоги, которые держит эта копия: доставить сейчас или, если чат занят, позже. */\n request(ownerId: string, chatId: string): Promise<void>;\n /** Ход чата закончился: если чат ждал доставки — пора. */\n chatFree(ownerId: string, chatId: string): void;\n /** Повтор для всех чатов, где доставка не удалась (обход по таймеру). */\n retryPending(): Promise<void>;\n forget(ownerId: string, chatId: string): void;\n /** Дождаться доставок, которые уже идут. */\n idle(): Promise<void>;\n}\n\n/**\n * Доставка итогов задач в чат служебными сообщениями. Копия доставляет только задачи со своей арендой. Готовые задачи\n * чата уходят пачками с ключом из ключей задач: повтор после сбоя ядро не повторит.\n */\nexport function createTaskDelivery(deps: { store: TaskStore; leaseOwner: string; port(): BackgroundPort }): TaskDelivery {\n const { store, leaseOwner } = deps;\n const pending = new Map<string, { ownerId: string; chatId: string }>();\n const inFlight = new Map<string, Promise<void>>();\n const again = new Set<string>();\n const chatKey = (ownerId: string, chatId: string) => `${ownerId}\\u0000${chatId}`;\n\n /** `wait` — чат занят или доставка не удалась: остаётся в ожидании. */\n async function deliverOnce(ownerId: string, chatId: string): Promise<\"done\" | \"wait\"> {\n const tasks = (await store.listUndelivered(ownerId, chatId)).filter((task) => task.leaseOwner === leaseOwner);\n const delivered: string[] = [];\n for (const group of await batchesOf(ownerId, tasks)) {\n const result = await deps.port().deliverNotices({\n ownerId,\n chatId,\n notices: group.tasks.map(noticeOf),\n wakeAgent: !group.tasks.every(isQuiet),\n deliveryKey: group.key,\n });\n if (result.status === \"busy\") return \"wait\";\n if (result.status === \"chat_gone\") {\n // Чата больше нет: итогам некуда идти, задачи отменены. Задачу, которую тем временем продолжили, ведёт та копия.\n const left = tasks.filter((task) => !delivered.includes(task.id));\n const now = new Date().toISOString();\n for (const task of left) {\n await store.update(\n ownerId,\n task.id,\n { status: \"canceled\", finishedAt: task.finishedAt ?? now, deliveredAt: now, leaseOwner: undefined, leaseUntil: undefined },\n { leaseOwner, statuses: NOTICE_STATUSES },\n );\n }\n return \"done\";\n }\n const ids = group.tasks.map((task) => task.id);\n await store.markDelivered(ids, group.key, new Date().toISOString());\n delivered.push(...ids);\n }\n return \"done\";\n }\n\n /**\n * Пачки доставки, тихие раньше громких. Начатая пачка (ключ сохранён в задачах) повторяется как была: ядро узнает\n * ключ и не запишет её второй раз, даже если готовых задач за это время стало больше. Ключ новой пачки сохраняется\n * в задачах до отправки.\n */\n async function batchesOf(ownerId: string, tasks: TaskRecord[]): Promise<Array<{ key: string; tasks: TaskRecord[] }>> {\n const batches = new Map<string, TaskRecord[]>();\n for (const task of tasks) if (task.deliveryKey) batches.set(task.deliveryKey, [...(batches.get(task.deliveryKey) ?? []), task]);\n const fresh = tasks.filter((task) => !task.deliveryKey);\n for (const group of [fresh.filter(isQuiet), fresh.filter((task) => !isQuiet(task))]) {\n if (group.length === 0) continue;\n const key = group.map(deliveryKeyOf).join(\"+\");\n const kept: TaskRecord[] = [];\n // Задачу, которую тем временем забрала другая копия, эта не доставляет.\n for (const task of group) if (await store.update(ownerId, task.id, { deliveryKey: key }, { leaseOwner })) kept.push(task);\n if (kept.length > 0) batches.set(key, kept);\n }\n const quietFirst = (group: TaskRecord[]) => (group.every(isQuiet) ? 0 : 1);\n return [...batches]\n .map(([key, group]) => ({ key, tasks: group }))\n .sort((a, b) => quietFirst(a.tasks) - quietFirst(b.tasks));\n }\n\n function drain(ownerId: string, chatId: string): Promise<void> {\n const key = chatKey(ownerId, chatId);\n const running = inFlight.get(key);\n if (running) {\n again.add(key);\n return running;\n }\n const work = (async () => {\n try {\n let outcome: \"done\" | \"wait\";\n do {\n again.delete(key);\n outcome = await deliverOnce(ownerId, chatId).catch((error: unknown) => {\n deps.port().logError(error);\n return \"wait\" as const;\n });\n } while (again.has(key));\n if (outcome === \"done\") pending.delete(key);\n } finally {\n inFlight.delete(key);\n }\n })();\n inFlight.set(key, work);\n return work;\n }\n\n return {\n request(ownerId, chatId) {\n pending.set(chatKey(ownerId, chatId), { ownerId, chatId });\n return drain(ownerId, chatId);\n },\n chatFree(ownerId, chatId) {\n if (pending.has(chatKey(ownerId, chatId))) void drain(ownerId, chatId);\n },\n async retryPending() {\n await Promise.all([...pending.values()].map(({ ownerId, chatId }) => drain(ownerId, chatId)));\n },\n forget(ownerId, chatId) {\n pending.delete(chatKey(ownerId, chatId));\n },\n async idle() {\n await Promise.allSettled([...inFlight.values()]);\n },\n };\n}\n\nfunction deliveryKeyOf(task: TaskRecord): string {\n const approvalId = task.status === \"awaiting_approval\" ? task.progress?.pendingApproval?.approvalId : undefined;\n return approvalId ? `task:${task.id}:approval:${approvalId}` : `task:${task.id}`;\n}\n\n/** Служебное сообщение о задаче, как его видят модель и экран. */\nexport function noticeOf(task: TaskRecord): AgentNotice {\n const base = { taskId: task.id, title: task.title };\n switch (task.status) {\n case \"done\":\n return {\n kind: \"task_done\",\n ...base,\n ...(task.result !== undefined && { result: noticeResult(task.result) }),\n // Помощник брал чужой текст: чат с этим итогом живые демонстрации не запускает.\n ...(task.progress?.externalContent && { externalContent: true as const }),\n };\n case \"failed\":\n return { kind: \"task_failed\", ...base, error: task.error ?? \"неизвестная ошибка\" };\n case \"timed_out\":\n return { kind: \"task_timed_out\", ...base, ...(task.error && { error: task.error }) };\n case \"canceled\":\n return { kind: \"task_canceled\", ...base };\n case \"awaiting_approval\": {\n const pendingApproval = task.progress?.pendingApproval;\n return {\n kind: \"task_awaiting_approval\",\n ...base,\n ...(pendingApproval && { approval: { approvalId: pendingApproval.approvalId, toolName: pendingApproval.toolName, input: pendingApproval.input } }),\n };\n }\n default:\n return { kind: \"task_interrupted\", ...base };\n }\n}\n\n/** Итог строкой (объект — JSON), обрезанный пределом: модель получает его в хвосте запроса. */\nfunction noticeResult(result: unknown): string {\n const text = typeof result === \"string\" ? result : JSON.stringify(result);\n if (text.length <= NOTICE_RESULT_MAX_LENGTH) return text;\n const tail = `\\n… (итог обрезан: всего ${text.length} знаков)`;\n return text.slice(0, NOTICE_RESULT_MAX_LENGTH - tail.length) + tail;\n}\n","import { randomUUID } from \"node:crypto\";\nimport type { BackgroundPort, TaskStatus } from \"@ykurilov/agent-chat-core\";\nimport { createKeyedLock } from \"./keyed-lock\";\nimport { isFinishedStatus, isRunningStatus, RUNNING_STATUSES } from \"./store/task-store\";\nimport type { TaskDelivery } from \"./task-delivery\";\nimport type { TaskCondition, TaskPatch, TaskRecord, TaskStore } from \"./types\";\n\n/** Сколько раз задачу запускают заново после падений сервера; дальше — «прервана». */\nconst MAX_ATTEMPTS = 3;\n\n/** Что можно отменить: задача идёт или ждёт решения человека. */\nconst CANCELABLE_STATUSES = [...RUNNING_STATUSES, \"awaiting_approval\"] as const satisfies readonly TaskStatus[];\n\n/** Чем закончился запуск: статус и поля итога. */\nexport type TaskOutcome = TaskPatch & { status: TaskStatus };\n\n/** Что запуск задачи получает от бегунка. */\nexport interface RunControl {\n signal: AbortSignal;\n /** Короткий текст хода в карточке; частые вызовы не копят записи. */\n reportProgress(text: string): void;\n /** Сохранить шаг (помощник); аренду проверяет перед записью, потерянная аренда обрывает запуск. */\n saveStep(patch: TaskPatch): Promise<void>;\n}\n\n/** Как вести задачу; строка — почему её вести нельзя (описания больше нет в настройках). */\nexport type TaskPlan = { work(record: TaskRecord, control: RunControl): Promise<TaskOutcome>; rerunAfterRestart: boolean; timeoutMs: number } | string;\n\n/** Новая задача из вызова инструмента. */\nexport type NewTask = Pick<TaskRecord, \"ownerId\" | \"chatId\" | \"messageId\" | \"toolCallId\" | \"channel\" | \"kind\" | \"name\" | \"input\" | \"title\">;\n\ninterface RunningTask {\n record: TaskRecord;\n controller: AbortController;\n timer?: ReturnType<typeof setTimeout>;\n /** Итог уже пишется (готово, ошибка, таймаут): второй итог не нужен. */\n finishing: boolean;\n /** Запуск остановлен снаружи (отмена, потеря аренды, остановка службы): итог не пишется. */\n stopped: boolean;\n}\n\n/** Аренда, которую держит эта копия: задача идёт здесь или ждёт здесь доставки итога. */\ninterface HeldLease {\n ownerId: string;\n chatId: string;\n run?: RunningTask;\n}\n\nexport interface TaskRunner {\n start(task: NewTask, maxRunningPerUser: number): Promise<{ status: \"started\"; taskId: string } | { status: \"too_many_tasks\" }>;\n cancel(ownerId: string, chatId: string, taskId: string, by: \"human\" | \"agent\"): Promise<\"canceled\" | \"not_found\" | \"already_finished\">;\n answerApproval(ownerId: string, chatId: string, taskId: string, approvalId: string, decision: \"allow\" | \"deny\"): Promise<\"resumed\" | \"not_found\" | \"stale\">;\n chatDeleted(ownerId: string, chatId: string): Promise<void>;\n /** Подобрать задачи, чья аренда истекла: хозяин пропал. */\n recoverExpired(): Promise<void>;\n renewLeases(): Promise<void>;\n /** Остановить запуски без записи итога и отпустить аренды: следующий старт подберёт задачи сразу. */\n stop(): Promise<void>;\n}\n\n/**\n * Бегунок задач. Задача идёт у той копии сервера, что держит аренду; аренда продлевается по таймеру службы. Итог\n * пишется, только если аренда ещё наша. Переходы одной задачи (итог, отмена, ответ на подтверждение) идут по одному.\n */\nexport function createTaskRunner(deps: {\n store: TaskStore;\n leaseOwner: string;\n leaseMs: number;\n port(): BackgroundPort;\n delivery: TaskDelivery;\n plan(record: TaskRecord): TaskPlan;\n}): TaskRunner {\n const { store, leaseOwner: me, leaseMs, delivery } = deps;\n const leases = new Map<string, HeldLease>();\n const taskLock = createKeyedLock();\n const ownerLock = createKeyedLock();\n /** Служба остановлена: брошенные задачи не подбираем, новых запусков нет. */\n let stopped = false;\n let recovering: Promise<void> | undefined;\n const now = () => new Date().toISOString();\n const leaseUntil = (from: string) => new Date(Date.parse(from) + leaseMs).toISOString();\n /** Запись идущей задачи: только пока аренда у этой копии и задача идёт — отмену или чужой итог она не затрёт. */\n const stillMine: TaskCondition = { leaseOwner: me, statuses: RUNNING_STATUSES };\n const tasksChanged = (ownerId: string, chatId: string) => deps.port().events.emit({ type: \"tasks-changed\", ownerId, chatId });\n const logError = (error: unknown) => deps.port().logError(error);\n\n function stopRun(run: RunningTask, reason: string): void {\n run.stopped = true;\n run.finishing = true;\n clearTimeout(run.timer);\n run.controller.abort(new Error(reason));\n }\n\n /** Итог задачи, которую ведёт эта копия: запись, событие, доставка. Задачу забрали или отменили — итог не наш. */\n async function writeOutcome(record: TaskRecord, outcome: TaskOutcome): Promise<void> {\n const at = now();\n const patch: TaskPatch = { ...outcome, ...(isFinishedStatus(outcome.status) && { finishedAt: at }), leaseUntil: leaseUntil(at) };\n if (!(await store.update(record.ownerId, record.id, patch, stillMine))) {\n leases.delete(record.id);\n return;\n }\n leases.set(record.id, { ownerId: record.ownerId, chatId: record.chatId });\n tasksChanged(record.ownerId, record.chatId);\n void delivery.request(record.ownerId, record.chatId);\n }\n\n async function finish(run: RunningTask, outcome: TaskOutcome): Promise<void> {\n if (run.finishing) return;\n run.finishing = true;\n clearTimeout(run.timer);\n if (outcome.status === \"timed_out\") run.controller.abort(new Error(outcome.error));\n await taskLock(run.record.id, async () => {\n if (run.stopped) return;\n try {\n await writeOutcome(run.record, outcome);\n } catch (error) {\n // Аренду не держим: задачу подберёт восстановление.\n leases.delete(run.record.id);\n logError(error);\n }\n });\n }\n\n function progressWriter(run: RunningTask): (text: string) => void {\n let latest: string | undefined;\n let writing = false;\n return (text) => {\n latest = text;\n if (writing) return;\n writing = true;\n void (async () => {\n try {\n while (latest !== undefined && !run.finishing) {\n const next = latest;\n latest = undefined;\n const progress = { task: run.record.title, steps: [], text: next, status: \"running\" as const };\n if (await store.update(run.record.ownerId, run.record.id, { progress }, stillMine)) tasksChanged(run.record.ownerId, run.record.chatId);\n }\n } catch (error) {\n logError(error);\n } finally {\n writing = false;\n }\n })();\n };\n }\n\n /** Запуск задачи, которую эта копия только что взяла (`claim`). */\n function launch(record: TaskRecord, plan: Exclude<TaskPlan, string>): void {\n const run: RunningTask = { record, controller: new AbortController(), finishing: false, stopped: false };\n leases.set(record.id, { ownerId: record.ownerId, chatId: record.chatId, run });\n run.timer = setTimeout(() => void finish(run, { status: \"timed_out\", error: `Задача не уложилась в ${formatDuration(plan.timeoutMs)}` }), plan.timeoutMs);\n run.timer.unref?.();\n const control: RunControl = {\n signal: run.controller.signal,\n reportProgress: progressWriter(run),\n async saveStep(patch) {\n if (run.finishing) return;\n if (!(await store.update(record.ownerId, record.id, { ...patch, leaseUntil: leaseUntil(now()) }, stillMine))) {\n leases.delete(record.id);\n stopRun(run, \"аренда задачи потеряна\");\n throw run.controller.signal.reason;\n }\n tasksChanged(record.ownerId, record.chatId);\n },\n };\n void Promise.resolve()\n .then(() => plan.work(record, control))\n .then(\n (outcome) => finish(run, outcome),\n (error: unknown) => finish(run, { status: \"failed\", error: messageOf(error) }),\n );\n }\n\n /** Задача без запуска получает итог сразу (прервана, описания нет). */\n async function finishWithout(record: TaskRecord, outcome: TaskOutcome): Promise<void> {\n await taskLock(record.id, () => writeOutcome(record, outcome));\n }\n\n /** Обход брошенных задач. На медленной базе он идёт долго: аренда каждой задачи считается от её взятия. */\n async function recoverOnce(): Promise<void> {\n for (const stale of await store.listExpired(now())) {\n if (stopped) return;\n if (leases.has(stale.id)) continue;\n try {\n const at = now();\n const claimed = await store.claim(stale.id, me, leaseUntil(at), at);\n if (claimed) await resume(stale.status, claimed);\n } catch (error) {\n logError(error);\n }\n }\n }\n\n async function resume(prior: TaskStatus, claimed: TaskRecord): Promise<void> {\n if (!isRunningStatus(prior)) {\n // Итог готов, но не доставлен: доставит эта копия.\n leases.set(claimed.id, { ownerId: claimed.ownerId, chatId: claimed.chatId });\n await delivery.request(claimed.ownerId, claimed.chatId);\n return;\n }\n const plan = deps.plan(claimed);\n if (typeof plan === \"string\") return finishWithout(claimed, { status: \"failed\", error: plan });\n if (claimed.attempt > MAX_ATTEMPTS) return finishWithout(claimed, { status: \"interrupted\" });\n // Не начатую задачу запускаем всегда; начатую — если её можно повторить.\n if (prior === \"running\" && !plan.rerunAfterRestart) return finishWithout(claimed, { status: \"interrupted\" });\n launch(claimed, plan);\n tasksChanged(claimed.ownerId, claimed.chatId);\n }\n\n return {\n start(task, maxRunningPerUser) {\n // По одному на человека: одновременные вызовы не обойдут предел.\n return ownerLock(task.ownerId, async () => {\n if ((await store.countActive(task.ownerId)) >= maxRunningPerUser) return { status: \"too_many_tasks\" as const };\n const plan = deps.plan({ ...task, id: \"\", status: \"queued\", attempt: 0, createdAt: \"\" });\n if (typeof plan === \"string\") throw new Error(plan);\n const createdAt = now();\n const id = randomUUID();\n await store.create({ ...task, id, status: \"queued\", attempt: 0, createdAt, leaseOwner: me, leaseUntil: leaseUntil(createdAt) });\n const claimed = await store.claim(id, me, leaseUntil(createdAt), createdAt);\n if (!claimed) throw new Error(`Задача ${id} не взята в работу сразу после создания`);\n launch(claimed, plan);\n tasksChanged(task.ownerId, task.chatId);\n return { status: \"started\" as const, taskId: id };\n });\n },\n\n cancel(ownerId, chatId, taskId, by) {\n return taskLock(taskId, async () => {\n const task = await store.get(ownerId, taskId);\n if (!task || task.chatId !== chatId) return \"not_found\";\n if (isFinishedStatus(task.status)) return \"already_finished\";\n const at = now();\n const canceled: TaskPatch =\n by === \"human\"\n ? // Пометка «отменена» в чате: доставит эта копия, агент не проснётся.\n { status: \"canceled\", finishedAt: at, leaseOwner: me, leaseUntil: leaseUntil(at), deliveredAt: undefined, deliveryKey: undefined }\n : // Отменил сам агент: он и так знает, сообщать нечего.\n { status: \"canceled\", finishedAt: at, deliveredAt: at, leaseOwner: undefined, leaseUntil: undefined, deliveryKey: undefined };\n // Итог мог записаться на другой копии между чтением и записью: тогда задача уже закончена.\n if (!(await store.update(ownerId, taskId, canceled, { statuses: CANCELABLE_STATUSES }))) return \"already_finished\";\n const run = leases.get(taskId)?.run;\n if (run) stopRun(run, \"задача отменена\");\n if (by === \"human\") {\n leases.set(taskId, { ownerId, chatId });\n void delivery.request(ownerId, chatId);\n } else {\n leases.delete(taskId);\n }\n tasksChanged(ownerId, chatId);\n return \"canceled\";\n });\n },\n\n answerApproval(ownerId, chatId, taskId, approvalId, decision) {\n return taskLock(taskId, async () => {\n const task = await store.get(ownerId, taskId);\n const pending = task?.progress?.pendingApproval;\n if (!task || task.chatId !== chatId || task.status !== \"awaiting_approval\" || !pending) return \"not_found\";\n // Ответ на вопрос, который помощник уже не задаёт (опоздал, пока шёл новый шаг), новый вопрос не одобряет.\n if (pending.approvalId !== approvalId) return \"stale\";\n const plan = deps.plan(task);\n if (typeof plan === \"string\") return \"not_found\";\n const { pendingApproval: _answered, ...progress } = task.progress!;\n const response = { type: \"tool-approval-response\" as const, approvalId, approved: decision === \"allow\" };\n // Продолжение по решению человека — не новая попытка: попытки считают только перезапуски после сбоев.\n // Условие по статусу и вопросу: ответ, пришедший сразу на две копии, продолжит задачу один раз, а пока\n // эта копия писала, другая могла ответить и помощник — задать новый вопрос.\n const resumed = await store.update(\n ownerId,\n taskId,\n {\n status: \"running\",\n helperMessages: [...(task.helperMessages ?? []), { role: \"tool\", content: [response] }],\n progress: { ...progress, status: \"running\" },\n deliveredAt: undefined,\n deliveryKey: undefined,\n leaseOwner: me,\n leaseUntil: leaseUntil(now()),\n },\n { statuses: [\"awaiting_approval\"], pendingApprovalId: approvalId },\n );\n if (!resumed) return (await store.get(ownerId, taskId))?.status === \"awaiting_approval\" ? \"stale\" : \"not_found\";\n launch(resumed, plan);\n tasksChanged(ownerId, chatId);\n return \"resumed\";\n });\n },\n\n async chatDeleted(ownerId, chatId) {\n for (const [taskId, lease] of leases) {\n if (lease.ownerId !== ownerId || lease.chatId !== chatId) continue;\n if (lease.run) stopRun(lease.run, \"чат удалён\");\n leases.delete(taskId);\n }\n delivery.forget(ownerId, chatId);\n // Другие копии потеряют продление аренды и сами оборвут свои запуски.\n await store.deleteForChat(ownerId, chatId);\n },\n\n recoverExpired() {\n // Один обход за раз: второй, начатый посреди первого, взял бы ту же задачу (своя аренда берётся снова) и запустил дважды.\n recovering ??= recoverOnce().finally(() => {\n recovering = undefined;\n });\n return recovering;\n },\n\n async renewLeases() {\n const until = leaseUntil(now());\n await Promise.all(\n [...leases].map(async ([taskId, lease]) => {\n let renewed: boolean;\n try {\n renewed = await store.renewLease(taskId, me, until);\n } catch (error) {\n logError(error);\n return;\n }\n // Не продлилась: итог доставлен или задачу забрала другая копия (она её и ведёт).\n if (renewed || leases.get(taskId) !== lease) return;\n leases.delete(taskId);\n if (lease.run) stopRun(lease.run, \"аренда задачи потеряна\");\n }),\n );\n },\n\n async stop() {\n stopped = true;\n const held = [...leases.entries()];\n leases.clear();\n for (const [, lease] of held) if (lease.run) stopRun(lease.run, \"служба остановлена\");\n const at = now();\n await Promise.all(held.map(([taskId]) => store.renewLease(taskId, me, at).catch(logError)));\n },\n };\n}\n\nfunction messageOf(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction formatDuration(ms: number): string {\n return ms >= 60_000 && ms % 60_000 === 0 ? `${ms / 60_000} мин` : `${Math.ceil(ms / 1000)} с`;\n}\n","/** Очередь по ключу: дела с одним ключом идут по одному, с разными — сразу. Ошибка одного дела следующим не мешает. */\nexport type KeyedLock = <T>(key: string, work: () => Promise<T>) => Promise<T>;\n\nexport function createKeyedLock(): KeyedLock {\n const tails = new Map<string, Promise<void>>();\n return (key, work) => {\n const result = (tails.get(key) ?? Promise.resolve()).then(work);\n const tail = result.then(\n () => {},\n () => {},\n );\n tails.set(key, tail);\n void tail.then(() => {\n if (tails.get(key) === tail) tails.delete(key);\n });\n return result;\n };\n}\n","import type { AgentToolsModel } from \"@ykurilov/agent-chat-core\";\nimport { jsonSchema } from \"ai\";\nimport type { Tool } from \"ai\";\nimport type { TaskRunner } from \"./task-runner\";\nimport type { TaskStore } from \"./types\";\n\nexport const CHECK_TASKS = \"check_tasks\";\nexport const CANCEL_TASK = \"cancel_task\";\n\n/** Постоянный блок системной инструкции: один на все ходы, кэш модели не ломает. */\nexport function backgroundInstructions(maxRunningPerUser: number): string {\n return [\n \"Фоновые задачи. Некоторые инструменты работают в фоне: вызов сразу отвечает «started» с taskId, итог придёт позже отдельным служебным сообщением.\",\n \"Не жди итога и не запускай ту же задачу повторно: скажи пользователю, что задача запущена, и продолжай разговор.\",\n `Состояние задач этого чата — ${CHECK_TASKS}. Отменить задачу — ${CANCEL_TASK} с её taskId.`,\n `У пользователя одновременно идёт не больше ${maxRunningPerUser} задач; при ответе «too_many_tasks» предложи дождаться или отменить одну из них.`,\n ].join(\"\\n\");\n}\n\n/** `check_tasks` и `cancel_task` для хода: только задачи этого чата. */\nexport function taskToolsForTurn(model: AgentToolsModel, service: { store: TaskStore; runner(): TaskRunner }): Record<string, Tool> {\n const { ownerId, chatId } = model;\n return {\n [CHECK_TASKS]: {\n description: \"Фоновые задачи этого чата: название, состояние, ход.\",\n inputSchema: jsonSchema<Record<string, never>>({ type: \"object\", properties: {}, additionalProperties: false }),\n async execute() {\n const tasks = await service.store.listForChat(ownerId, chatId);\n return {\n tasks: tasks.map((task) => ({\n taskId: task.id,\n title: task.title,\n status: task.status,\n startedAt: task.startedAt ?? task.createdAt,\n ...(task.finishedAt && { finishedAt: task.finishedAt }),\n ...(task.progress?.text && { progress: task.progress.text }),\n ...(task.error && { error: task.error }),\n })),\n };\n },\n } as Tool,\n [CANCEL_TASK]: {\n description: \"Отменить фоновую задачу этого чата по taskId.\",\n inputSchema: jsonSchema<{ taskId: string }>({\n type: \"object\",\n properties: { taskId: { type: \"string\" } },\n required: [\"taskId\"],\n additionalProperties: false,\n }),\n async execute({ taskId }: { taskId: string }) {\n return { status: await service.runner().cancel(ownerId, chatId, taskId, \"agent\"), taskId };\n },\n } as Tool,\n };\n}\n","import type { TaskRecord, TaskStore } from \"../types\";\nimport { isRunningStatus, meetsCondition, NOTICE_STATUSES, patchEntries, withoutEmptyFields } from \"./task-store\";\n\nconst copy = (record: TaskRecord): TaskRecord => withoutEmptyFields(structuredClone(record));\nconst byCreatedAt = (left: TaskRecord, right: TaskRecord) => left.createdAt.localeCompare(right.createdAt);\n\n/** Задачи в памяти процесса: для разработки и тестов. Перезапуск их теряет, две копии сервера их не делят. */\nexport function memoryTaskStore(): TaskStore {\n const tasks = new Map<string, TaskRecord>();\n\n const owned = (ownerId: string, taskId: string) => {\n const task = tasks.get(taskId);\n return task && task.ownerId === ownerId ? task : undefined;\n };\n const isFree = (task: TaskRecord, leaseOwner: string, now: string) =>\n task.leaseOwner === leaseOwner || task.leaseUntil === undefined || task.leaseUntil < now;\n\n return {\n async create(task) {\n tasks.set(task.id, copy(task));\n },\n async get(ownerId, taskId) {\n const task = owned(ownerId, taskId);\n return task ? copy(task) : null;\n },\n async update(ownerId, taskId, patch, when) {\n const task = owned(ownerId, taskId);\n if (!task || !meetsCondition(task, when)) return null;\n const changed = { ...task } as Record<string, unknown>;\n for (const [key, value] of patchEntries(structuredClone(patch))) changed[key] = value;\n const next = withoutEmptyFields(changed as unknown as TaskRecord);\n tasks.set(taskId, next);\n return copy(next);\n },\n async listForChat(ownerId, chatId) {\n return [...tasks.values()].filter((task) => task.ownerId === ownerId && task.chatId === chatId).sort(byCreatedAt).map(copy);\n },\n async countActive(ownerId) {\n return [...tasks.values()].filter((task) => task.ownerId === ownerId && isRunningStatus(task.status)).length;\n },\n async claim(taskId, leaseOwner, leaseUntil, now) {\n const task = tasks.get(taskId);\n if (!task || task.deliveredAt !== undefined || !isFree(task, leaseOwner, now)) return null;\n const next: TaskRecord = isRunningStatus(task.status)\n ? { ...task, status: \"running\", attempt: task.attempt + 1, startedAt: task.startedAt ?? now, leaseOwner, leaseUntil }\n : { ...task, leaseOwner, leaseUntil };\n tasks.set(taskId, next);\n return copy(next);\n },\n async renewLease(taskId, leaseOwner, leaseUntil) {\n const task = tasks.get(taskId);\n if (!task || task.deliveredAt !== undefined || task.leaseOwner !== leaseOwner) return false;\n tasks.set(taskId, { ...task, leaseUntil });\n return true;\n },\n async listExpired(now) {\n return [...tasks.values()].filter((task) => task.deliveredAt === undefined && (task.leaseUntil === undefined || task.leaseUntil < now)).map(copy);\n },\n async listUndelivered(ownerId, chatId) {\n return [...tasks.values()]\n .filter((task) => task.ownerId === ownerId && task.chatId === chatId && task.deliveredAt === undefined)\n .filter((task) => (NOTICE_STATUSES as readonly string[]).includes(task.status))\n .sort(byCreatedAt)\n .map(copy);\n },\n async markDelivered(taskIds, deliveryKey, at) {\n for (const taskId of taskIds) {\n const task = tasks.get(taskId);\n if (!task || task.deliveryKey !== deliveryKey || !(NOTICE_STATUSES as readonly string[]).includes(task.status)) continue;\n const { leaseOwner: _owner, leaseUntil: _until, ...rest } = task;\n tasks.set(taskId, { ...rest, deliveredAt: at });\n }\n },\n async deleteForChat(ownerId, chatId) {\n for (const [taskId, task] of tasks) if (task.ownerId === ownerId && task.chatId === chatId) tasks.delete(taskId);\n },\n };\n}\n"],"mappings":"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","names":["randomUUID","jsonSchema","randomUUID"]}
|
package/dist/labels.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/labels.ts
|
|
2
|
+
var backgroundStepPhrases = {
|
|
3
|
+
check_tasks: { running: "\u041F\u0440\u043E\u0432\u0435\u0440\u044F\u044E \u0444\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438", done: "\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u043B \u0444\u043E\u043D\u043E\u0432\u044B\u0435 \u0437\u0430\u0434\u0430\u0447\u0438" },
|
|
4
|
+
cancel_task: { running: "\u041E\u0442\u043C\u0435\u043D\u044F\u044E \u0437\u0430\u0434\u0430\u0447\u0443", done: "\u041E\u0442\u043C\u0435\u043D\u0438\u043B \u0437\u0430\u0434\u0430\u0447\u0443" }
|
|
5
|
+
};
|
|
6
|
+
var englishBackgroundStepPhrases = {
|
|
7
|
+
check_tasks: { running: "Checking background tasks", done: "Checked background tasks" },
|
|
8
|
+
cancel_task: { running: "Canceling a task", done: "Canceled a task" }
|
|
9
|
+
};
|
|
10
|
+
export {
|
|
11
|
+
backgroundStepPhrases,
|
|
12
|
+
englishBackgroundStepPhrases
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=labels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/labels.ts"],"sourcesContent":["/** Понятные фразы инструментов фоновых задач для простого вида чата (`stepPhrases` у `AgentChat`). Без импортов: файл безопасно подключать в браузерной сборке. */\nexport const backgroundStepPhrases: Record<string, { running: string; done: string }> = {\n check_tasks: { running: \"Проверяю фоновые задачи\", done: \"Проверил фоновые задачи\" },\n cancel_task: { running: \"Отменяю задачу\", done: \"Отменил задачу\" },\n};\n\n/** Те же фразы по-английски. */\nexport const englishBackgroundStepPhrases: Record<string, { running: string; done: string }> = {\n check_tasks: { running: \"Checking background tasks\", done: \"Checked background tasks\" },\n cancel_task: { running: \"Canceling a task\", done: \"Canceled a task\" },\n};\n"],"mappings":";AACO,IAAM,wBAA2E;AAAA,EACtF,aAAa,EAAE,SAAS,oIAA2B,MAAM,mIAA0B;AAAA,EACnF,aAAa,EAAE,SAAS,mFAAkB,MAAM,kFAAiB;AACnE;AAGO,IAAM,+BAAkF;AAAA,EAC7F,aAAa,EAAE,SAAS,6BAA6B,MAAM,2BAA2B;AAAA,EACtF,aAAa,EAAE,SAAS,oBAAoB,MAAM,kBAAkB;AACtE;","names":[]}
|
package/dist/postgres.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKGROUND_MIGRATIONS_TABLE,
|
|
3
|
+
BACKGROUND_MIGRATION_LOCK_KEY
|
|
4
|
+
} from "./chunk-4RA3JT6K.js";
|
|
5
|
+
import {
|
|
6
|
+
NOTICE_STATUSES,
|
|
7
|
+
RUNNING_STATUSES,
|
|
8
|
+
meetsCondition,
|
|
9
|
+
patchEntries,
|
|
10
|
+
withoutEmptyFields,
|
|
11
|
+
withoutNul
|
|
12
|
+
} from "./chunk-4XFSUFM6.js";
|
|
13
|
+
|
|
14
|
+
// src/postgres.ts
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
|
|
17
|
+
// src/store/postgres.ts
|
|
18
|
+
import { and, asc, eq, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
|
19
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
20
|
+
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
|
21
|
+
|
|
22
|
+
// src/store/postgres-schema.ts
|
|
23
|
+
import { index, integer, jsonb, pgSchema, text, timestamp } from "drizzle-orm/pg-core";
|
|
24
|
+
var agentChatSchema = pgSchema("agent_chat");
|
|
25
|
+
var moment = (name) => timestamp(name, { mode: "date", precision: 3, withTimezone: true });
|
|
26
|
+
var backgroundTasks = agentChatSchema.table(
|
|
27
|
+
"background_tasks",
|
|
28
|
+
{
|
|
29
|
+
id: text("id").primaryKey(),
|
|
30
|
+
ownerId: text("owner_id").notNull(),
|
|
31
|
+
chatId: text("chat_id").notNull(),
|
|
32
|
+
messageId: text("message_id").notNull(),
|
|
33
|
+
toolCallId: text("tool_call_id").notNull(),
|
|
34
|
+
channel: text("channel", { enum: ["web", "telegram", "vk", "max"] }).notNull(),
|
|
35
|
+
kind: text("kind", { enum: ["tool", "helper"] }).notNull(),
|
|
36
|
+
name: text("name").notNull(),
|
|
37
|
+
input: jsonb("input"),
|
|
38
|
+
title: text("title").notNull(),
|
|
39
|
+
status: text("status").$type().notNull(),
|
|
40
|
+
/** Шаги помощника: с них задача продолжается после перезапуска. */
|
|
41
|
+
helperMessages: jsonb("helper_messages").$type(),
|
|
42
|
+
progress: jsonb("progress").$type(),
|
|
43
|
+
result: jsonb("result"),
|
|
44
|
+
error: text("error"),
|
|
45
|
+
leaseOwner: text("lease_owner"),
|
|
46
|
+
leaseUntil: moment("lease_until"),
|
|
47
|
+
attempt: integer("attempt").notNull().default(0),
|
|
48
|
+
createdAt: moment("created_at").notNull(),
|
|
49
|
+
startedAt: moment("started_at"),
|
|
50
|
+
finishedAt: moment("finished_at"),
|
|
51
|
+
deliveredAt: moment("delivered_at"),
|
|
52
|
+
/** Ключ служебного сообщения, с которым задачу начали доставлять. */
|
|
53
|
+
deliveryKey: text("delivery_key")
|
|
54
|
+
},
|
|
55
|
+
(table) => [
|
|
56
|
+
index("background_tasks_status_lease_idx").on(table.status, table.leaseUntil),
|
|
57
|
+
index("background_tasks_owner_status_idx").on(table.ownerId, table.status),
|
|
58
|
+
index("background_tasks_chat_delivered_idx").on(table.chatId, table.deliveredAt)
|
|
59
|
+
]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// src/store/postgres.ts
|
|
63
|
+
var TIME_FIELDS = ["leaseUntil", "createdAt", "startedAt", "finishedAt", "deliveredAt"];
|
|
64
|
+
var toDate = (iso) => iso === null ? null : new Date(iso);
|
|
65
|
+
var toIso = (date) => date === null ? void 0 : date.toISOString();
|
|
66
|
+
function toRecord(row) {
|
|
67
|
+
return withoutEmptyFields({
|
|
68
|
+
...row,
|
|
69
|
+
leaseUntil: toIso(row.leaseUntil),
|
|
70
|
+
createdAt: toIso(row.createdAt),
|
|
71
|
+
startedAt: toIso(row.startedAt),
|
|
72
|
+
finishedAt: toIso(row.finishedAt),
|
|
73
|
+
deliveredAt: toIso(row.deliveredAt)
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function toColumns(patch) {
|
|
77
|
+
const columns = {};
|
|
78
|
+
for (const [key, value] of patchEntries(patch)) {
|
|
79
|
+
columns[key] = TIME_FIELDS.includes(key) ? toDate(value) : withoutNul(value);
|
|
80
|
+
}
|
|
81
|
+
return columns;
|
|
82
|
+
}
|
|
83
|
+
async function migrateBackgroundTasksToLatest(pool, migrationsFolder) {
|
|
84
|
+
const client = await pool.connect();
|
|
85
|
+
try {
|
|
86
|
+
await client.query("select pg_advisory_lock($1)", [BACKGROUND_MIGRATION_LOCK_KEY]);
|
|
87
|
+
try {
|
|
88
|
+
await migrate(drizzle(client), { migrationsFolder, migrationsSchema: "agent_chat", migrationsTable: BACKGROUND_MIGRATIONS_TABLE });
|
|
89
|
+
} finally {
|
|
90
|
+
await client.query("select pg_advisory_unlock($1)", [BACKGROUND_MIGRATION_LOCK_KEY]);
|
|
91
|
+
}
|
|
92
|
+
} finally {
|
|
93
|
+
client.release();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function createPostgresTaskStore(pool, migrationsFolder) {
|
|
97
|
+
const db = drizzle(pool);
|
|
98
|
+
const ready = migrateBackgroundTasksToLatest(pool, migrationsFolder);
|
|
99
|
+
ready.catch(() => {
|
|
100
|
+
});
|
|
101
|
+
const owned = (ownerId, taskId) => and(eq(backgroundTasks.id, taskId), eq(backgroundTasks.ownerId, ownerId));
|
|
102
|
+
const isRunning = inArray(backgroundTasks.status, [...RUNNING_STATUSES]);
|
|
103
|
+
const condition = (when) => and(
|
|
104
|
+
when?.leaseOwner !== void 0 ? eq(backgroundTasks.leaseOwner, when.leaseOwner) : void 0,
|
|
105
|
+
when?.statuses !== void 0 ? inArray(backgroundTasks.status, [...when.statuses]) : void 0,
|
|
106
|
+
when?.pendingApprovalId !== void 0 ? sql`${backgroundTasks.progress}->'pendingApproval'->>'approvalId' = ${when.pendingApprovalId}` : void 0
|
|
107
|
+
);
|
|
108
|
+
async function get(ownerId, taskId) {
|
|
109
|
+
await ready;
|
|
110
|
+
const [row] = await db.select().from(backgroundTasks).where(owned(ownerId, taskId));
|
|
111
|
+
return row ? toRecord(row) : null;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
async create(task) {
|
|
115
|
+
await ready;
|
|
116
|
+
await db.insert(backgroundTasks).values(toColumns(task));
|
|
117
|
+
},
|
|
118
|
+
get,
|
|
119
|
+
async update(ownerId, taskId, patch, when) {
|
|
120
|
+
const columns = toColumns(patch);
|
|
121
|
+
if (Object.keys(columns).length === 0) {
|
|
122
|
+
const task = await get(ownerId, taskId);
|
|
123
|
+
return task && meetsCondition(task, when) ? task : null;
|
|
124
|
+
}
|
|
125
|
+
await ready;
|
|
126
|
+
const [row] = await db.update(backgroundTasks).set(columns).where(and(owned(ownerId, taskId), condition(when))).returning();
|
|
127
|
+
return row ? toRecord(row) : null;
|
|
128
|
+
},
|
|
129
|
+
async listForChat(ownerId, chatId) {
|
|
130
|
+
await ready;
|
|
131
|
+
const rows = await db.select().from(backgroundTasks).where(and(eq(backgroundTasks.ownerId, ownerId), eq(backgroundTasks.chatId, chatId))).orderBy(asc(backgroundTasks.createdAt), asc(backgroundTasks.id));
|
|
132
|
+
return rows.map(toRecord);
|
|
133
|
+
},
|
|
134
|
+
async countActive(ownerId) {
|
|
135
|
+
await ready;
|
|
136
|
+
const [row] = await db.select({ count: sql`count(*)::int` }).from(backgroundTasks).where(and(eq(backgroundTasks.ownerId, ownerId), isRunning));
|
|
137
|
+
return Number(row?.count ?? 0);
|
|
138
|
+
},
|
|
139
|
+
async claim(taskId, leaseOwner, leaseUntil, now) {
|
|
140
|
+
await ready;
|
|
141
|
+
const nowDate = new Date(now);
|
|
142
|
+
const [row] = await db.update(backgroundTasks).set({
|
|
143
|
+
leaseOwner,
|
|
144
|
+
leaseUntil: new Date(leaseUntil),
|
|
145
|
+
status: sql`case when ${isRunning} then 'running' else ${backgroundTasks.status} end`,
|
|
146
|
+
attempt: sql`case when ${isRunning} then ${backgroundTasks.attempt} + 1 else ${backgroundTasks.attempt} end`,
|
|
147
|
+
startedAt: sql`case when ${isRunning} then coalesce(${backgroundTasks.startedAt}, ${nowDate}) else ${backgroundTasks.startedAt} end`
|
|
148
|
+
}).where(
|
|
149
|
+
and(
|
|
150
|
+
eq(backgroundTasks.id, taskId),
|
|
151
|
+
isNull(backgroundTasks.deliveredAt),
|
|
152
|
+
or(eq(backgroundTasks.leaseOwner, leaseOwner), isNull(backgroundTasks.leaseUntil), lt(backgroundTasks.leaseUntil, nowDate))
|
|
153
|
+
)
|
|
154
|
+
).returning();
|
|
155
|
+
return row ? toRecord(row) : null;
|
|
156
|
+
},
|
|
157
|
+
async renewLease(taskId, leaseOwner, leaseUntil) {
|
|
158
|
+
await ready;
|
|
159
|
+
const renewed = await db.update(backgroundTasks).set({ leaseUntil: new Date(leaseUntil) }).where(and(eq(backgroundTasks.id, taskId), eq(backgroundTasks.leaseOwner, leaseOwner), isNull(backgroundTasks.deliveredAt))).returning({ id: backgroundTasks.id });
|
|
160
|
+
return renewed.length > 0;
|
|
161
|
+
},
|
|
162
|
+
async listExpired(now) {
|
|
163
|
+
await ready;
|
|
164
|
+
const rows = await db.select().from(backgroundTasks).where(and(isNull(backgroundTasks.deliveredAt), or(isNull(backgroundTasks.leaseUntil), lt(backgroundTasks.leaseUntil, new Date(now))))).orderBy(asc(backgroundTasks.createdAt));
|
|
165
|
+
return rows.map(toRecord);
|
|
166
|
+
},
|
|
167
|
+
async listUndelivered(ownerId, chatId) {
|
|
168
|
+
await ready;
|
|
169
|
+
const rows = await db.select().from(backgroundTasks).where(
|
|
170
|
+
and(
|
|
171
|
+
eq(backgroundTasks.ownerId, ownerId),
|
|
172
|
+
eq(backgroundTasks.chatId, chatId),
|
|
173
|
+
isNull(backgroundTasks.deliveredAt),
|
|
174
|
+
inArray(backgroundTasks.status, [...NOTICE_STATUSES])
|
|
175
|
+
)
|
|
176
|
+
).orderBy(asc(backgroundTasks.createdAt), asc(backgroundTasks.id));
|
|
177
|
+
return rows.map(toRecord);
|
|
178
|
+
},
|
|
179
|
+
async markDelivered(taskIds, deliveryKey, at) {
|
|
180
|
+
if (taskIds.length === 0) return;
|
|
181
|
+
await ready;
|
|
182
|
+
await db.update(backgroundTasks).set({ deliveredAt: new Date(at), leaseOwner: null, leaseUntil: null }).where(
|
|
183
|
+
and(
|
|
184
|
+
inArray(backgroundTasks.id, [...taskIds]),
|
|
185
|
+
eq(backgroundTasks.deliveryKey, deliveryKey),
|
|
186
|
+
inArray(backgroundTasks.status, [...NOTICE_STATUSES])
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
},
|
|
190
|
+
async deleteForChat(ownerId, chatId) {
|
|
191
|
+
await ready;
|
|
192
|
+
await db.delete(backgroundTasks).where(and(eq(backgroundTasks.ownerId, ownerId), eq(backgroundTasks.chatId, chatId)));
|
|
193
|
+
},
|
|
194
|
+
async close() {
|
|
195
|
+
await ready.catch(() => {
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/postgres.ts
|
|
202
|
+
var MIGRATIONS_FOLDER = fileURLToPath(new URL("../drizzle/postgres", import.meta.url));
|
|
203
|
+
function postgresTaskStore(pool) {
|
|
204
|
+
return createPostgresTaskStore(pool, MIGRATIONS_FOLDER);
|
|
205
|
+
}
|
|
206
|
+
export {
|
|
207
|
+
BACKGROUND_MIGRATIONS_TABLE,
|
|
208
|
+
BACKGROUND_MIGRATION_LOCK_KEY,
|
|
209
|
+
backgroundTasks,
|
|
210
|
+
postgresTaskStore
|
|
211
|
+
};
|
|
212
|
+
//# sourceMappingURL=postgres.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/postgres.ts","../src/store/postgres.ts","../src/store/postgres-schema.ts"],"sourcesContent":["import { fileURLToPath } from \"node:url\";\nimport type { Pool } from \"pg\";\nimport type { TaskStore } from \"./types\";\nimport { createPostgresTaskStore } from \"./store/postgres\";\n\nexport { BACKGROUND_MIGRATIONS_TABLE, BACKGROUND_MIGRATION_LOCK_KEY } from \"./store/migration-settings\";\nexport { backgroundTasks } from \"./store/postgres-schema\";\n\n/** Миграции лежат в пакете рядом с `dist`: `drizzle/postgres`. */\nconst MIGRATIONS_FOLDER = fileURLToPath(new URL(\"../drizzle/postgres\", import.meta.url));\n\n/**\n * Задачи фона в PostgreSQL на пуле приложения (схема `agent_chat`, та же база, что у хранилища чатов).\n * Таблица приводится к последней миграции при первом обращении. Пул закрывает приложение.\n */\nexport function postgresTaskStore(pool: Pool): TaskStore {\n return createPostgresTaskStore(pool, MIGRATIONS_FOLDER);\n}\n","import { and, asc, eq, inArray, isNull, lt, or, sql } from \"drizzle-orm\";\nimport { drizzle } from \"drizzle-orm/node-postgres\";\nimport { migrate } from \"drizzle-orm/node-postgres/migrator\";\nimport type { Pool } from \"pg\";\nimport type { TaskCondition, TaskPatch, TaskRecord, TaskStore } from \"../types\";\nimport { BACKGROUND_MIGRATION_LOCK_KEY, BACKGROUND_MIGRATIONS_TABLE } from \"./migration-settings\";\nimport { backgroundTasks } from \"./postgres-schema\";\nimport { meetsCondition, NOTICE_STATUSES, patchEntries, RUNNING_STATUSES, withoutEmptyFields, withoutNul } from \"./task-store\";\n\ntype Row = typeof backgroundTasks.$inferSelect;\n\nconst TIME_FIELDS = [\"leaseUntil\", \"createdAt\", \"startedAt\", \"finishedAt\", \"deliveredAt\"] as const;\nconst toDate = (iso: string | null) => (iso === null ? null : new Date(iso));\nconst toIso = (date: Date | null) => (date === null ? undefined : date.toISOString());\n\nfunction toRecord(row: Row): TaskRecord {\n return withoutEmptyFields({\n ...row,\n leaseUntil: toIso(row.leaseUntil),\n createdAt: toIso(row.createdAt)!,\n startedAt: toIso(row.startedAt),\n finishedAt: toIso(row.finishedAt),\n deliveredAt: toIso(row.deliveredAt),\n } as TaskRecord);\n}\n\n/** Запись или правка как колонки таблицы: времена — `Date`, без символа NUL. */\nfunction toColumns(patch: TaskPatch | TaskRecord): Record<string, unknown> {\n const columns: Record<string, unknown> = {};\n for (const [key, value] of patchEntries(patch)) {\n columns[key] = (TIME_FIELDS as readonly string[]).includes(key) ? toDate(value as string | null) : withoutNul(value);\n }\n return columns;\n}\n\n/**\n * Приводит таблицу задач к последней миграции. Идёт на одном соединении под блокировкой базы:\n * если несколько серверов стартуют сразу, второй ждёт первого и видит применённые миграции.\n */\nexport async function migrateBackgroundTasksToLatest(pool: Pool, migrationsFolder: string): Promise<void> {\n const client = await pool.connect();\n try {\n await client.query(\"select pg_advisory_lock($1)\", [BACKGROUND_MIGRATION_LOCK_KEY]);\n try {\n await migrate(drizzle(client), { migrationsFolder, migrationsSchema: \"agent_chat\", migrationsTable: BACKGROUND_MIGRATIONS_TABLE });\n } finally {\n await client.query(\"select pg_advisory_unlock($1)\", [BACKGROUND_MIGRATION_LOCK_KEY]);\n }\n } finally {\n client.release();\n }\n}\n\n/**\n * Хранилище задач в PostgreSQL (схема `agent_chat`) на пуле приложения. Миграция идёт при первом обращении,\n * каждая операция её дожидается. Закрытие пул не закрывает: он принадлежит приложению.\n */\nexport function createPostgresTaskStore(pool: Pool, migrationsFolder: string): TaskStore {\n const db = drizzle(pool);\n const ready = migrateBackgroundTasksToLatest(pool, migrationsFolder);\n // Ошибка миграции придёт в каждую операцию; без этого она всплыла бы ещё и как необработанная.\n ready.catch(() => {});\n const owned = (ownerId: string, taskId: string) => and(eq(backgroundTasks.id, taskId), eq(backgroundTasks.ownerId, ownerId));\n const isRunning = inArray(backgroundTasks.status, [...RUNNING_STATUSES]);\n /** Условие записи — в том же `update … where`: проверка и запись одним действием базы. */\n const condition = (when: TaskCondition | undefined) =>\n and(\n when?.leaseOwner !== undefined ? eq(backgroundTasks.leaseOwner, when.leaseOwner) : undefined,\n when?.statuses !== undefined ? inArray(backgroundTasks.status, [...when.statuses]) : undefined,\n when?.pendingApprovalId !== undefined ? sql`${backgroundTasks.progress}->'pendingApproval'->>'approvalId' = ${when.pendingApprovalId}` : undefined,\n );\n\n async function get(ownerId: string, taskId: string): Promise<TaskRecord | null> {\n await ready;\n const [row] = await db.select().from(backgroundTasks).where(owned(ownerId, taskId));\n return row ? toRecord(row) : null;\n }\n\n return {\n async create(task) {\n await ready;\n await db.insert(backgroundTasks).values(toColumns(task) as Row);\n },\n get,\n async update(ownerId, taskId, patch, when) {\n const columns = toColumns(patch);\n if (Object.keys(columns).length === 0) {\n const task = await get(ownerId, taskId);\n return task && meetsCondition(task, when) ? task : null;\n }\n await ready;\n const [row] = await db.update(backgroundTasks).set(columns).where(and(owned(ownerId, taskId), condition(when))).returning();\n return row ? toRecord(row) : null;\n },\n async listForChat(ownerId, chatId) {\n await ready;\n const rows = await db\n .select()\n .from(backgroundTasks)\n .where(and(eq(backgroundTasks.ownerId, ownerId), eq(backgroundTasks.chatId, chatId)))\n .orderBy(asc(backgroundTasks.createdAt), asc(backgroundTasks.id));\n return rows.map(toRecord);\n },\n async countActive(ownerId) {\n await ready;\n const [row] = await db\n .select({ count: sql<number>`count(*)::int` })\n .from(backgroundTasks)\n .where(and(eq(backgroundTasks.ownerId, ownerId), isRunning));\n return Number(row?.count ?? 0);\n },\n async claim(taskId, leaseOwner, leaseUntil, now) {\n await ready;\n const nowDate = new Date(now);\n // Одно условное обновление: вторая копия ждёт строку, перепроверяет условие на новой версии и ничего не берёт.\n const [row] = await db\n .update(backgroundTasks)\n .set({\n leaseOwner,\n leaseUntil: new Date(leaseUntil),\n status: sql`case when ${isRunning} then 'running' else ${backgroundTasks.status} end`,\n attempt: sql`case when ${isRunning} then ${backgroundTasks.attempt} + 1 else ${backgroundTasks.attempt} end`,\n startedAt: sql`case when ${isRunning} then coalesce(${backgroundTasks.startedAt}, ${nowDate}) else ${backgroundTasks.startedAt} end`,\n })\n .where(\n and(\n eq(backgroundTasks.id, taskId),\n isNull(backgroundTasks.deliveredAt),\n or(eq(backgroundTasks.leaseOwner, leaseOwner), isNull(backgroundTasks.leaseUntil), lt(backgroundTasks.leaseUntil, nowDate)),\n ),\n )\n .returning();\n return row ? toRecord(row) : null;\n },\n async renewLease(taskId, leaseOwner, leaseUntil) {\n await ready;\n const renewed = await db\n .update(backgroundTasks)\n .set({ leaseUntil: new Date(leaseUntil) })\n .where(and(eq(backgroundTasks.id, taskId), eq(backgroundTasks.leaseOwner, leaseOwner), isNull(backgroundTasks.deliveredAt)))\n .returning({ id: backgroundTasks.id });\n return renewed.length > 0;\n },\n async listExpired(now) {\n await ready;\n const rows = await db\n .select()\n .from(backgroundTasks)\n .where(and(isNull(backgroundTasks.deliveredAt), or(isNull(backgroundTasks.leaseUntil), lt(backgroundTasks.leaseUntil, new Date(now)))))\n .orderBy(asc(backgroundTasks.createdAt));\n return rows.map(toRecord);\n },\n async listUndelivered(ownerId, chatId) {\n await ready;\n const rows = await db\n .select()\n .from(backgroundTasks)\n .where(\n and(\n eq(backgroundTasks.ownerId, ownerId),\n eq(backgroundTasks.chatId, chatId),\n isNull(backgroundTasks.deliveredAt),\n inArray(backgroundTasks.status, [...NOTICE_STATUSES]),\n ),\n )\n .orderBy(asc(backgroundTasks.createdAt), asc(backgroundTasks.id));\n return rows.map(toRecord);\n },\n async markDelivered(taskIds, deliveryKey, at) {\n if (taskIds.length === 0) return;\n await ready;\n await db\n .update(backgroundTasks)\n .set({ deliveredAt: new Date(at), leaseOwner: null, leaseUntil: null })\n .where(\n and(\n inArray(backgroundTasks.id, [...taskIds]),\n eq(backgroundTasks.deliveryKey, deliveryKey),\n inArray(backgroundTasks.status, [...NOTICE_STATUSES]),\n ),\n );\n },\n async deleteForChat(ownerId, chatId) {\n await ready;\n await db.delete(backgroundTasks).where(and(eq(backgroundTasks.ownerId, ownerId), eq(backgroundTasks.chatId, chatId)));\n },\n async close() {\n // Дожидаемся миграции: пул приложения можно закрывать сразу после.\n await ready.catch(() => {});\n },\n };\n}\n","import type { HelperProgress, TaskStatus } from \"@ykurilov/agent-chat-core\";\nimport type { ModelMessage } from \"ai\";\nimport { index, integer, jsonb, pgSchema, text, timestamp } from \"drizzle-orm/pg-core\";\n\n/** Та же схема базы, что у хранилища чатов: у приложения одна база и одна схема `agent_chat`. */\nconst agentChatSchema = pgSchema(\"agent_chat\");\n\nconst moment = (name: string) => timestamp(name, { mode: \"date\", precision: 3, withTimezone: true });\n\nexport const backgroundTasks = agentChatSchema.table(\n \"background_tasks\",\n {\n id: text(\"id\").primaryKey(),\n ownerId: text(\"owner_id\").notNull(),\n chatId: text(\"chat_id\").notNull(),\n messageId: text(\"message_id\").notNull(),\n toolCallId: text(\"tool_call_id\").notNull(),\n channel: text(\"channel\", { enum: [\"web\", \"telegram\", \"vk\", \"max\"] }).notNull(),\n kind: text(\"kind\", { enum: [\"tool\", \"helper\"] }).notNull(),\n name: text(\"name\").notNull(),\n input: jsonb(\"input\"),\n title: text(\"title\").notNull(),\n status: text(\"status\").$type<TaskStatus>().notNull(),\n /** Шаги помощника: с них задача продолжается после перезапуска. */\n helperMessages: jsonb(\"helper_messages\").$type<ModelMessage[]>(),\n progress: jsonb(\"progress\").$type<HelperProgress>(),\n result: jsonb(\"result\"),\n error: text(\"error\"),\n leaseOwner: text(\"lease_owner\"),\n leaseUntil: moment(\"lease_until\"),\n attempt: integer(\"attempt\").notNull().default(0),\n createdAt: moment(\"created_at\").notNull(),\n startedAt: moment(\"started_at\"),\n finishedAt: moment(\"finished_at\"),\n deliveredAt: moment(\"delivered_at\"),\n /** Ключ служебного сообщения, с которым задачу начали доставлять. */\n deliveryKey: text(\"delivery_key\"),\n },\n (table) => [\n index(\"background_tasks_status_lease_idx\").on(table.status, table.leaseUntil),\n index(\"background_tasks_owner_status_idx\").on(table.ownerId, table.status),\n index(\"background_tasks_chat_delivered_idx\").on(table.chatId, table.deliveredAt),\n ],\n);\n"],"mappings":"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","names":[]}
|
package/dist/sqlite.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BACKGROUND_MIGRATIONS_TABLE
|
|
3
|
+
} from "./chunk-4RA3JT6K.js";
|
|
4
|
+
import {
|
|
5
|
+
NOTICE_STATUSES,
|
|
6
|
+
RUNNING_STATUSES,
|
|
7
|
+
meetsCondition,
|
|
8
|
+
patchEntries,
|
|
9
|
+
withoutEmptyFields
|
|
10
|
+
} from "./chunk-4XFSUFM6.js";
|
|
11
|
+
|
|
12
|
+
// src/sqlite.ts
|
|
13
|
+
import { fileURLToPath } from "url";
|
|
14
|
+
|
|
15
|
+
// src/store/sqlite.ts
|
|
16
|
+
import Database from "better-sqlite3";
|
|
17
|
+
import { and, asc, eq, inArray, isNull, lt, or, sql } from "drizzle-orm";
|
|
18
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
19
|
+
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
|
|
20
|
+
|
|
21
|
+
// src/store/sqlite-schema.ts
|
|
22
|
+
import { index, integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
23
|
+
var backgroundTasks = sqliteTable(
|
|
24
|
+
"background_tasks",
|
|
25
|
+
{
|
|
26
|
+
id: text("id").primaryKey(),
|
|
27
|
+
ownerId: text("owner_id").notNull(),
|
|
28
|
+
chatId: text("chat_id").notNull(),
|
|
29
|
+
messageId: text("message_id").notNull(),
|
|
30
|
+
toolCallId: text("tool_call_id").notNull(),
|
|
31
|
+
channel: text("channel", { enum: ["web", "telegram", "vk", "max"] }).notNull(),
|
|
32
|
+
kind: text("kind", { enum: ["tool", "helper"] }).notNull(),
|
|
33
|
+
name: text("name").notNull(),
|
|
34
|
+
input: text("input", { mode: "json" }),
|
|
35
|
+
title: text("title").notNull(),
|
|
36
|
+
status: text("status").$type().notNull(),
|
|
37
|
+
/** Шаги помощника: с них задача продолжается после перезапуска. */
|
|
38
|
+
helperMessages: text("helper_messages", { mode: "json" }).$type(),
|
|
39
|
+
progress: text("progress", { mode: "json" }).$type(),
|
|
40
|
+
result: text("result", { mode: "json" }),
|
|
41
|
+
error: text("error"),
|
|
42
|
+
leaseOwner: text("lease_owner"),
|
|
43
|
+
leaseUntil: integer("lease_until"),
|
|
44
|
+
attempt: integer("attempt").notNull().default(0),
|
|
45
|
+
createdAt: integer("created_at").notNull(),
|
|
46
|
+
startedAt: integer("started_at"),
|
|
47
|
+
finishedAt: integer("finished_at"),
|
|
48
|
+
deliveredAt: integer("delivered_at"),
|
|
49
|
+
/** Ключ служебного сообщения, с которым задачу начали доставлять. */
|
|
50
|
+
deliveryKey: text("delivery_key")
|
|
51
|
+
},
|
|
52
|
+
(table) => [
|
|
53
|
+
index("background_tasks_status_lease_idx").on(table.status, table.leaseUntil),
|
|
54
|
+
index("background_tasks_owner_status_idx").on(table.ownerId, table.status),
|
|
55
|
+
index("background_tasks_chat_delivered_idx").on(table.chatId, table.deliveredAt)
|
|
56
|
+
]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// src/store/sqlite.ts
|
|
60
|
+
var TIME_FIELDS = ["leaseUntil", "createdAt", "startedAt", "finishedAt", "deliveredAt"];
|
|
61
|
+
var toMs = (iso) => iso === void 0 || iso === null ? null : Date.parse(iso);
|
|
62
|
+
var toIso = (ms) => ms === null ? void 0 : new Date(ms).toISOString();
|
|
63
|
+
function toRecord(row) {
|
|
64
|
+
return withoutEmptyFields({
|
|
65
|
+
...row,
|
|
66
|
+
leaseUntil: toIso(row.leaseUntil),
|
|
67
|
+
createdAt: toIso(row.createdAt),
|
|
68
|
+
startedAt: toIso(row.startedAt),
|
|
69
|
+
finishedAt: toIso(row.finishedAt),
|
|
70
|
+
deliveredAt: toIso(row.deliveredAt)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function toColumns(patch) {
|
|
74
|
+
const columns = {};
|
|
75
|
+
for (const [key, value] of patchEntries(patch)) {
|
|
76
|
+
columns[key] = TIME_FIELDS.includes(key) ? toMs(value) : value;
|
|
77
|
+
}
|
|
78
|
+
return columns;
|
|
79
|
+
}
|
|
80
|
+
function createSqliteTaskStore(filePath, migrationsFolder) {
|
|
81
|
+
const sqlite = new Database(filePath);
|
|
82
|
+
sqlite.pragma("journal_mode = WAL");
|
|
83
|
+
sqlite.pragma("busy_timeout = 5000");
|
|
84
|
+
const db = drizzle(sqlite);
|
|
85
|
+
migrate(db, { migrationsFolder, migrationsTable: BACKGROUND_MIGRATIONS_TABLE });
|
|
86
|
+
const owned = (ownerId, taskId) => and(eq(backgroundTasks.id, taskId), eq(backgroundTasks.ownerId, ownerId));
|
|
87
|
+
const isRunning = inArray(backgroundTasks.status, [...RUNNING_STATUSES]);
|
|
88
|
+
const condition = (when) => and(
|
|
89
|
+
when?.leaseOwner !== void 0 ? eq(backgroundTasks.leaseOwner, when.leaseOwner) : void 0,
|
|
90
|
+
when?.statuses !== void 0 ? inArray(backgroundTasks.status, [...when.statuses]) : void 0,
|
|
91
|
+
when?.pendingApprovalId !== void 0 ? sql`json_extract(${backgroundTasks.progress}, '$.pendingApproval.approvalId') = ${when.pendingApprovalId}` : void 0
|
|
92
|
+
);
|
|
93
|
+
async function get(ownerId, taskId) {
|
|
94
|
+
const row = db.select().from(backgroundTasks).where(owned(ownerId, taskId)).get();
|
|
95
|
+
return row ? toRecord(row) : null;
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
async create(task) {
|
|
99
|
+
db.insert(backgroundTasks).values(toColumns(task)).run();
|
|
100
|
+
},
|
|
101
|
+
get,
|
|
102
|
+
async update(ownerId, taskId, patch, when) {
|
|
103
|
+
const columns = toColumns(patch);
|
|
104
|
+
if (Object.keys(columns).length === 0) {
|
|
105
|
+
const task = await get(ownerId, taskId);
|
|
106
|
+
return task && meetsCondition(task, when) ? task : null;
|
|
107
|
+
}
|
|
108
|
+
const row = db.update(backgroundTasks).set(columns).where(and(owned(ownerId, taskId), condition(when))).returning().get();
|
|
109
|
+
return row ? toRecord(row) : null;
|
|
110
|
+
},
|
|
111
|
+
async listForChat(ownerId, chatId) {
|
|
112
|
+
const rows = db.select().from(backgroundTasks).where(and(eq(backgroundTasks.ownerId, ownerId), eq(backgroundTasks.chatId, chatId))).orderBy(asc(backgroundTasks.createdAt), asc(backgroundTasks.id)).all();
|
|
113
|
+
return rows.map(toRecord);
|
|
114
|
+
},
|
|
115
|
+
async countActive(ownerId) {
|
|
116
|
+
const row = db.select({ count: sql`count(*)` }).from(backgroundTasks).where(and(eq(backgroundTasks.ownerId, ownerId), isRunning)).get();
|
|
117
|
+
return Number(row?.count ?? 0);
|
|
118
|
+
},
|
|
119
|
+
async claim(taskId, leaseOwner, leaseUntil, now) {
|
|
120
|
+
const nowMs = toMs(now);
|
|
121
|
+
const row = db.update(backgroundTasks).set({
|
|
122
|
+
leaseOwner,
|
|
123
|
+
leaseUntil: toMs(leaseUntil),
|
|
124
|
+
status: sql`case when ${isRunning} then 'running' else ${backgroundTasks.status} end`,
|
|
125
|
+
attempt: sql`case when ${isRunning} then ${backgroundTasks.attempt} + 1 else ${backgroundTasks.attempt} end`,
|
|
126
|
+
startedAt: sql`case when ${isRunning} then coalesce(${backgroundTasks.startedAt}, ${nowMs}) else ${backgroundTasks.startedAt} end`
|
|
127
|
+
}).where(
|
|
128
|
+
and(
|
|
129
|
+
eq(backgroundTasks.id, taskId),
|
|
130
|
+
isNull(backgroundTasks.deliveredAt),
|
|
131
|
+
or(eq(backgroundTasks.leaseOwner, leaseOwner), isNull(backgroundTasks.leaseUntil), lt(backgroundTasks.leaseUntil, nowMs))
|
|
132
|
+
)
|
|
133
|
+
).returning().get();
|
|
134
|
+
return row ? toRecord(row) : null;
|
|
135
|
+
},
|
|
136
|
+
async renewLease(taskId, leaseOwner, leaseUntil) {
|
|
137
|
+
const renewed = db.update(backgroundTasks).set({ leaseUntil: toMs(leaseUntil) }).where(and(eq(backgroundTasks.id, taskId), eq(backgroundTasks.leaseOwner, leaseOwner), isNull(backgroundTasks.deliveredAt))).run();
|
|
138
|
+
return renewed.changes > 0;
|
|
139
|
+
},
|
|
140
|
+
async listExpired(now) {
|
|
141
|
+
const rows = db.select().from(backgroundTasks).where(and(isNull(backgroundTasks.deliveredAt), or(isNull(backgroundTasks.leaseUntil), lt(backgroundTasks.leaseUntil, toMs(now))))).orderBy(asc(backgroundTasks.createdAt)).all();
|
|
142
|
+
return rows.map(toRecord);
|
|
143
|
+
},
|
|
144
|
+
async listUndelivered(ownerId, chatId) {
|
|
145
|
+
const rows = db.select().from(backgroundTasks).where(
|
|
146
|
+
and(
|
|
147
|
+
eq(backgroundTasks.ownerId, ownerId),
|
|
148
|
+
eq(backgroundTasks.chatId, chatId),
|
|
149
|
+
isNull(backgroundTasks.deliveredAt),
|
|
150
|
+
inArray(backgroundTasks.status, [...NOTICE_STATUSES])
|
|
151
|
+
)
|
|
152
|
+
).orderBy(asc(backgroundTasks.createdAt), asc(backgroundTasks.id)).all();
|
|
153
|
+
return rows.map(toRecord);
|
|
154
|
+
},
|
|
155
|
+
async markDelivered(taskIds, deliveryKey, at) {
|
|
156
|
+
if (taskIds.length === 0) return;
|
|
157
|
+
db.update(backgroundTasks).set({ deliveredAt: toMs(at), leaseOwner: null, leaseUntil: null }).where(
|
|
158
|
+
and(
|
|
159
|
+
inArray(backgroundTasks.id, [...taskIds]),
|
|
160
|
+
eq(backgroundTasks.deliveryKey, deliveryKey),
|
|
161
|
+
inArray(backgroundTasks.status, [...NOTICE_STATUSES])
|
|
162
|
+
)
|
|
163
|
+
).run();
|
|
164
|
+
},
|
|
165
|
+
async deleteForChat(ownerId, chatId) {
|
|
166
|
+
db.delete(backgroundTasks).where(and(eq(backgroundTasks.ownerId, ownerId), eq(backgroundTasks.chatId, chatId))).run();
|
|
167
|
+
},
|
|
168
|
+
async close() {
|
|
169
|
+
sqlite.close();
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/sqlite.ts
|
|
175
|
+
var MIGRATIONS_FOLDER = fileURLToPath(new URL("../drizzle/sqlite", import.meta.url));
|
|
176
|
+
function sqliteTaskStore(filePath) {
|
|
177
|
+
return createSqliteTaskStore(filePath, MIGRATIONS_FOLDER);
|
|
178
|
+
}
|
|
179
|
+
export {
|
|
180
|
+
BACKGROUND_MIGRATIONS_TABLE,
|
|
181
|
+
backgroundTasks,
|
|
182
|
+
sqliteTaskStore
|
|
183
|
+
};
|
|
184
|
+
//# sourceMappingURL=sqlite.js.map
|