@ykurilov/agent-chat-core
v0.81.0
Published
Ядро чата с агентом. Работает только на сервере: ходит к провайдерам моделей, ведёт ход агента с инструментами, хранит переписку, останавливает и повторяет ответы.
Readme
@ykurilov/agent-chat-core
Ядро чата с агентом. Работает только на сервере: ходит к провайдерам моделей, ведёт ход агента с инструментами, хранит переписку, останавливает и повторяет ответы.
Обычно напрямую нужны только каталог моделей и, если нужна своя база, договор хранилища.
HTTP-адреса даёт @ykurilov/agent-chat-server.
import { extendCatalog, standardCatalog } from "@ykurilov/agent-chat-core";
import type { ChatStorage } from "@ykurilov/agent-chat-core";standardCatalog— стандартные модели и провайдеры. Модели:deepseek-4.1-flash,glm-5.3-flash,gpt-6-luna,gemini-3.8-flash,claude-haiku-5.5(через OpenRouter, кэш на час). Провайдеры и ключи:deepseek(DEEPSEEK_API_KEY),fireworks(FIREWORKS_API_KEY),together(TOGETHER_API_KEY),openrouter(OPENROUTER_API_KEY, сортировка по скорости). Кто какую модель отдаёт — в docs/getting-started.md, шаг 3.extendCatalog(base, { addModels, removeModels, addProviders, removeProviders, pinModelVersions, connectProviders })— свой набор моделей: убрать, добавить, закрепить версию модели у провайдера. Порядок: убрать → добавить → закрепить.Провайдеры через прокси (сервер в России):
connectProviders: { openrouter: { baseURL, headers } }вextendCatalogилиcreateOpenRouterProvider,createDeepSeekProvider,createFireworksProvider,createTogetherProviderс{ baseURL, headers }. Через прокси идёт всё: ход, память, сжатие, сверка расхода OpenRouter. Значения заголовков вычищаются из ошибок и журналов (environmentSecrets); короче 8 знаков иAuthorization— ошибка при сборке. Прокси должен пробрасывать заголовок ответаx-generation-id, иначе расход OpenRouter останется оценкой (fr-api-route.yeycourses.comего пробрасывает).Маршруты OpenRouter:
createOpenRouterProvider({ id: "or-deepseek", only: ["deepseek"], allowFallbacks: false })— отдельный провайдер каталога с теми же моделями, чтоopenrouter(ещёorder,requireParameters,displayName).Часовой кэш Anthropic через OpenRouter: срок
ttl: "1h"на всех метках кэша запроса (инструкция, последнее сообщение, конец шага). Кто главнее:createOpenRouterProvider({ cacheTtlByModel: { "claude-haiku-5.5": "5m" } })→cacheTtlпровайдера →cacheTtlпрофиля модели (уclaude-haiku-5.5—"1h") → 5 минут (метки без срока, запрос прежний). Запись в часовой кэш дороже (у Anthropic 2× цены входа вместо 1.25×), чтение то же; выгодно, когда между ходами больше 5 минут. Цена записи по сроку:cacheWritePerMillion(5 минут) иcacheWrite1hPerMillion(час) вpricingпрофиля.const catalog = extendCatalog(standardCatalog, { connectProviders: { openrouter: { baseURL: "https://fr-api-route.yeycourses.com/openrouter/api/v1", headers: { "X-Proxy-Token": process.env.PROXY_TOKEN! }, }, }, });Профиль модели описывает уровни рассуждений (
reasoning.levels,defaultLevel,encrypted); провайдер переводит уровень в запрос (reasoningRequest). Выбор модели и уровня хранится в чате (Chat.reasoning);defaultChoiceзадаёт выбор для нового чата человека, который ещё ничего не выбирал; дальше новый чат берёт последний выбор человека (хранилище: необязательныеgetUserModelChoiceиsaveUserModelChoice, без них — всегдаdefaultChoice). Смена модели посреди чата: рассуждения прежней модели в запрос не уходят.agent.tools— набор инструментов или функция(model) => ToolSetот выбранной модели. Функция зовётся раз на ход и получаетmodel.ownerId(кто спрашивает),model.chatIdиmodel.place— где человек в приложении ({ description, data? }илиnull, приходит с запросом; в историю не пишется). Модель видит описание места хвостом последнего входа хода (сообщения человека; после ответа на виджет или подтверждения — заметкой сразу за этим ответом), кэш не страдает; предел описания —agent.placeMaxLength(1000 по умолчанию).notifyDataChanged({ tag, details? })вmodelфункцииagent.tools: инструмент сообщает странице, что изменил данные (метка 1–100 знаков,details— JSON до 4 КБ; неверная форма —TypeError). Сообщение уходит той вкладке, где идёт ответ, сразу; в историю не пишется. Тесты, собирающиеAgentToolsModelвручную, добавляют это поле.agent.browserTools— описания инструментов, которые исполняет страница человека (tool({ description, inputSchema })безexecute; имя не должно совпадать с инструментом приложения или памяти).agent.browserToolTimeoutMs— срок ответа вкладки: 30 с по умолчанию, от 1 с до 2³¹−1 мс. Результат до 16 КБ, ошибка обрезается до 1000 знаков, пустой результат — «готово». Служба чата получила методanswerBrowserCall: свои реализацииChatServiceдобавляют его; повторный, чужой и поздний ответ —BrowserCallNotPendingError(видnot_pending). Ожидание живёт в памяти процесса: работает на одном сервере.Инструмент с
needsApproval: true(или функцией от входных данных) не выполняется без решения человека: ход останавливается со статусом ответаawaiting_approval, после решения продолжается в том же сообщении.answerApprovalсервиса чата (createChatService) принимает решение; «всегда в этом чате» помнится по истории чата и предлагается только дляneedsApproval: true(у функции кнопки нет). Хранилище обязано уметьgetMessage. Сбой или «Стоп» после «Разрешить» закрывают вызов как прервавшийся; ответ на карточку проверяет лимит расходов.ChatStorage— интерфейс хранилища;createInMemoryChatStorage()— хранилище в памяти для тестов приложений (отдельного пакета нет: оно часть ядра, ничего лишнего не тянет). Настоящие хранилища — пакеты@ykurilov/agent-chat-storage-sqliteи@ykurilov/agent-chat-storage-postgres.Ветки ответов:
ChatStorageдописывает выбор варианта (insertBranchSelection,listBranchSelections);createChatServiceумеетselectBranch,regenerate,editMessage,getBranchForks. Сообщения, сжатие и поиск работают по текущей ветке (loadCurrentBranch).Поправка на лету:
service.steer(userId, chatId, message, { page? })→{ status: "queued" | "started", messageId }. Сообщение пишется сразу (отметкаdata-agent-steer, модели не уходит), агент берёт его на ближайшей границе шага: ответ закрывается, ход продолжается новым ответом на поправки (поток —subscribe). Поправки подряд уходят модели одним сообщением человека. Хода нет — обычный ход (started); ход на другой копии —ChatBusyError. Проверки — как уsendMessage. Ждущие поправки —pendingMessagesв истории;isCorrection,STEER_PART_TYPE— для своей ленты.@ykurilov/agent-chat-core/storage-contract— общие тестыdescribeChatStorageContractдля своей реализации хранилища (нуженvitest). Их проходят память, SQLite и PostgreSQL.Список чатов:
chat.listChats(userId),chat.renameChat(userId, chatId, title),chat.deleteChat(userId, chatId). Хранилище для этого реализуетlistChats,renameChat,deleteChat. Название чата берётся из первого сообщения (deriveChatTitle, до 60 знаков), вручную — до 100 (normalizeChatTitle,InvalidChatTitleError). Удаление настоящее: идущий ответ и сжатие останавливаются, затем уходят файлы вложений, сообщения, записи сжатия и чат.
Агент внутри продукта
Всё по желанию; без этих настроек запрос к модели не меняется. Подробно и с примером урока — docs/in-product-agent.md.
beforeTurn({ userId, chat, action, text?, messageId? })— приложение отказывает в ходе:{ code, message }→TurnRefusedError, ничего не записано. Уregenerate,editиretryещёabandonedAnswerIds(ответы агента, которые уйдут с ветки) иpreviousAnswerId(заменяемый ответ): откатить побочные дела брошенных ответов; ход не начался —turnNotStartedполучает те же поля. Вручную —service.assertTurnAllowed(userId, chatId, action, text?).turnNotStarted({ userId, chat, action, messageId?, text?, reason })— ход не начался, хотяbeforeTurnпропустил (busy,too_many_turns,spending_limit,duplicate_message,invalid,chat_closed,no_turn,error): снять резерв приложения. Каждый пропуск закрывается ровно одним —afterTurnили этим хуком.afterTurn(turn)— итог хода (TurnResult) после каждого хода, с повторами (afterTurnAttempts, 3) и сроком (afterTurnTimeoutMs, 30 000); переживает перезапуск. Пишите идемпотентно поmessageId+status. Вернуть{ events }— события в конце ответа.stopReason(user/timeout/shutdown) — кто остановил ход;lengthCut: true— ответ упёрся вagent.maxOutputTokens;stepLimit: true— ход упёрся вagent.maxSteps(статусcomplete, в журнале строка[предел шагов]).agent.turnTimeoutMs— общий срок хода (как «Стоп» по сроку),agent.maxOutputTokens— предел токенов ответа в каждом запросе; оба — число или{ default, byModel }.service.flush({ waitForAnswersMs })— штатная остановка: ждёт идущие ответы до срока, остальные закрывает как «Стоп», ждётafterTurn.- События приложения
{ type, data? }: из инструментаmodel.emitAppEvent(event), вне ходаservice.publishAppEvent({ ownerId, chatId, event }). Модели не уходят. - Состояние чата на ветке: инструмент пишет
model.state.set(path, value)/unset(path), читаетget(path?)(путь через точку; ключи без точек, массивы целиком; значения — простой JSON). Запись ложится частью ответаdata-app-state, модели не уходит. Состояние — свёртка записей текущей ветки из хранилища: правка, «Переделать» и выбор ветки меняют его сами, сжатие и «Повторить» не теряют.service.getChatState(userId, chatId)→{ values, atMessageId }(читает только связи и записи состояния; своему хранилищу — необязательныйlistMessagePartsOfTypes).onStateChanged({ ownerId, chatId, reason, state, previousState, atMessageId })— состояние сменилось после хода или смены ветки (reason:turn,editMessage,regenerate,retry,selectBranch,editAnswer,widgetAnswer— записиcheckAnswerвиджета), с повторами (onStateChangedAttempts, 3) и сроком (onStateChangedTimeoutMs, 30 000), по очереди внутри чата, событияstate-changedиstate-changed-failed. Доставка — только пока сервер жив: сверка копии — поgetChatState. Ответ пишется в базу сразу после итога каждого инструмента (промежуточные результаты — окном 500 мс). Запись до 16 КБ, состояние до 256 КБ (AppStateTooLargeError); после конца хода запись не принимается; верхние ключи на$— служебные ключи SDK.privateStateKeys— закрытые верхние ключи приложения: не уходят в историю, поток иstate-changedнаблюдения;getChatState,onStateChanged, события и модель их видят. Закрытое значение не возвращайте в выходе инструмента и тексте ответа. Для своих частей:foldAppState,loadBranchAppState,createAppStateWriter,appStatePart. - Снимок перед каждым шагом:
agent.stepContext: ({ state, stepNumber, chat, subject, … }) => string | null— текст ложится сообщением человека в самый конец запроса шага, после результатов инструментов, в историю не пишется. Снимок на шаге один, всё до него от шага к шагу те же байты (кэш модели).null— шаг без снимка, бросок — без снимка и строка в журнал. Функция видит те жеchannel,audience,staff, что инструменты (в группе личного не показывайте), данные чата — копиями. Срокagent.stepContextTimeoutMs(по умолчанию 5 с): не ответила — шаг без снимка,abortSignalотменён. ТипыStepContext,StepContextInput. - Застывающий контекст: память, сегодняшняя дата, место на экране и блоки приложения вписываются в вопрос человека один раз за ход и дальше не меняются ни на байт. Следующий ход побайтно начинается с последнего шага прошлого (с рассуждениями и инструментами), поэтому всё прошлое читается из кэша модели. В новый вопрос идут только изменившиеся блоки и строка
[Больше не действует: «ключ»]про пропавшие. Источник может вернуть{ key, hidden: true }— блок есть, но не показан ради места: вместо «больше не действует» модель, видевшая его, получит[Не показано ради места, но по-прежнему действует: «ключ»]. Набор хода хранится служебной частью ответаdata-frozen-context(FROZEN_CONTEXT_PART_TYPE): в поток, историю,afterTurnи поиск по истории она не уходит. Продолжение после подтверждения, автопродолжение, запасной провайдер и «Повторить» берут блоки из ответа и источники заново не опрашивают. Память строже остальных блоков: читается один раз на чат — в первом ходе, после сжатия истории, при смене начала истории (правка первого сообщения, «Переделать» первого ответа), в первом личном ходе после группового и после сбоя чтения — и в идущем чате не перечитывается и не досылается (ни изменения, ни новые записи, ни «Больше не действует»);snapshot()иblocks()в остальных ходах не зовутся. Своя долгая память может отдавать блоки по записям:LongTermMemory.blocks?({ userId, chatId }); без него — один блок изsnapshot(). - Блоки приложения в вопросе хода:
agent.contextBlocks: ({ chatId, ownerId, chat, state, subject, channel, audience, … }) => [{ key, text, private? }] | null— список скиллов, роль, правила. Зовётся один раз в начале хода; текст вписывается в вопрос после памяти и даты и дальше не меняется ни на байт (кэш модели), ядро его не оборачивает. На следующих ходах в вопрос идут только изменённые блоки целиком и строка[Больше не действует: «ключ»]про убранные. Ключиmemory,memory:*,today,screen_placeзаняты ядром: такой блок отбрасывается со строкой в журнал.private: true— в групповом ходе блок не показывается. Срокagent.contextBlocksTimeoutMs(по умолчанию 5 с; 0 илиInfinity— без срока, иначе от 1000): не ответила или бросила — блоки приложения берутся из прошлого ответа, в журнал строка[блоки контекста] …,abortSignalотменён; «Стоп» — без строки. Блоки хранятся служебной частью ответа: в потоке, истории,afterTurnи поиске по истории её нет. ТипыContextBlock,ContextBlocksInput,ContextBlocksSource,HiddenContextBlock,ContextBlockEntry. agent.contextBlocksRefresh: "history-start"включает снимок приложения до смены начала видимой истории, как у памяти. Без настройки и при"turn"источник по-прежнему читается каждый новый ход. Снимок хранится в frozen context ветки; своего кэша или SQL-миграций не требуется. Изменения вне переписки передаются черезprepareTurn.notices, права проверяются сервером. Триггеры, ошибки и пример.- Конец хода сразу после инструмента:
executeвозвращаетendTurn(output, { text? })— модель следующего шага не делает, готовыйtextидёт в ответ обычной текстовой частью без запроса к провайдеру и без расхода;outputмодель и запись видят как обычно. ТипEndTurnOptions. СendTurn(output, { requireText: true })остановка принимается только при непустом пользовательском text-part текущего ответа; без него результат обычный и модель продолжает. Проверка в памяти потока, без чтения БД/flush. Пробелы и чисто форматные невидимые Unicode, reasoning, tool output, ошибки и прежняя история не считаются; Markdown/HTML по смыслу не разбирается. Явный непустойoptions.textсохраняет приоритет и прежний путь готового текста. Это не автоподтверждение и не гарантия ответа при отмене, лимите или сбое. Полная семантика и переход. - Данные чата без записи:
service.getChatData(userId, chatId)— копия, проверки владения как уupdateChatData. - Проверка ответа виджета на сервере:
checkAnswer(answer, ctx)у своего виджета и общаяagent.widgets.checkAnswer(call, answer, ctx)(для всех виджетов без своей). Зовётся после схемы под замком чата. Исход:undefined— принять,{ answer }— принять с заменой,{ refuse: "текст" }— отказать (WidgetAnswerRefusedError, сервер — 409widget_answer_refused, виджет остаётся открытым).ctx—ownerId,chatId,messageId,toolName,toolCallId,input,subject?,chatData?,state(записи ложатся в ответ вместе с результатом виджета, хукonStateChanged— с причинойwidgetAnswer),emitAppEvent(событие приложения вместе с ответом виджета),signal(отмена по сроку или «Стопу»: приложение слушает его само). Срок —agent.widgets.checkAnswerTimeoutMs(5 000 мс, заметно меньшеrequestTimeoutMsстраницы). Исключение — 500, ничего не записано, в журнале только имя ошибки; срок вышел —WidgetAnswerCheckTimeoutError, сервер — 504widget_check_timeout. Чат закрыли во время проверки —ChatClosedError, ответ не пишется. - Набор виджетов по чату:
agent.widgets.enabledFor({ chat: { id, ownerId, data?, subject? } })→ имена включённых. Выключенных модель не видит (нет в инструментах запроса), их прошлые вызовы идут модели текстом. Синхронная, те же данные — тот же набор; смена набора сбрасывает кэш модели. Неизвестное имя — строка в журнал, исключение — ошибка хода. Подробно — docs/widgets.md. - Права человека в чате:
chatCapabilities({ userId, chat })возвращает запреты (edit,regenerate,retry,branches,modelChoice,answerEditing,chatList,compact,attachments,voice,reasoning,toolDetails,steer,modelDetails,errorDetails,appNotices; по умолчанию всёtrue, кромеerrorDetailsиappNotices— служебных пометок приложения на экране, без текста для модели и только через белый списокappNoticeViews(имя пометки → безопасные данные для экрана; без функции пометка не уходит);modelDetails: falseвыключаетmodelChoice,errorDetailsиappNotices). Неизвестное право или не булево значение —TypeError. Запрещённое действие —ActionForbiddenError(action)до любой записи и доbeforeTurn. За один вызов службы права спрашиваются не больше раза. История несётcapabilities. Вручную —service.assertCapability(userId, chatId, capability). - Скрытие частей ответа:
reasoning: false— мысли модели,toolDetails: false— служебные инструменты не уходят в историю, поток и повтор кусков после переподключения; хранилище, модель иafterTurnполучают всё. Виджеты, браузерные инструменты, вызовы с запросом подтверждения и инструменты сmetadata: { showOnScreen: true }в описании видны всегда.modelDetails: falseубирает у чата историиownerId,modelId,providerId,reasoning, у сообщений —metadata.modelId,providerId,usage, у частей и кусков —providerMetadata/callProviderMetadata/resultProviderMetadata, уdata-step-reset— провайдера; отметки сжатия идут без текста сводки (summary: "",summaryTokens: 0) в истории иgetCompactions. Подробности сбоя провайдера (metadata.error.details:providerId,modelId,statusCode,reason— одна строка до 300 знаков без секретов, параметров адресов,org-…/proj_…и e-mail,resetsAt— ISO, только будущее не дальше двух лет) хранятся всегда, а в историю и поток идут только приerrorDetails: true(например, админу); без права — толькоerror.kind. Ответ модерации вreasonможет повторять текст человека — отдавайте право только тому, кому этот текст можно видеть. Меню моделей — опцияmodelMenu({ userId }):false—getModelMenu(userId)бросаетActionForbiddenError("modelDetails"). История несётmodelAcceptsImages(видит ли модель чата картинки). Права — на того, кто открыл поток. - Закрытый чат:
service.closeChat({ ownerId, chatId, code, message })иreopenChat({ ownerId, chatId })(событияchat-closed,chat-reopened), из инструмента —model.closeChat({ code, message }). Закрытие — запись$closedна ветке: из инструмента — в ответе сразу при вызове, из кода — служебной записью в конце ветки (идёт ход — после него). Правка, «Переделать», выбор ветки и правка ответа до точки закрытия открывают чат; закрыть навсегда — правами (chatCapabilitiesвидитchat.closed). Закрытие побеждает паузу: ждущие подтверждения и виджеты закрываются отказом, как при «Стопе». В закрытом чате отправка, «Повторить», подтверждения и виджеты —ChatClosedError; служебные сообщения пишутся без хода.listChatsотдаётclosed— копию закрытия текущей ветки (setChatClosed); права и история берут закрытие с ветки, не записавшуюся копию дописывает следующее открытие истории. Закрытие чатов 0.27 переносится в запись лениво, при первой правке или смене ветки. beforeCreateChat({ userId, origin? })— можно ли человеку завести новый чат (createChat):{ code, message }→ChatCreationRefusedError. Чаты объектов и импорт хук не спрашивают. У чатаclosed?: { code, message }, хранилище — необязательныйsetChatClosed(chatId, closed | null).- Чат объекта:
service.openSubjectChat({ ownerId, key, data?, title?, greeting? })→{ chat, created }, один чат на ключ у человека. Инструменты получаютmodel.subject, итог хода —turn.subject.listChats(userId, { subjects })— по умолчанию без чатов объектов. - Приветствие:
createChat(userId, { greeting: { text } })— первое сообщение от агента без вызова модели. - Импорт:
service.importChat({ ownerId, chat, messages })→{ chatId }; без хода, памяти, расхода и лимитов.chat.stateиchat.closedпереносят состояние чата и закрытие:getChatStateвидит их сразу. staffOfChatиmodel.staff— для песочницы: приmodel.staffинструменты не пишут в данныеownerId.- Своё хранилище:
findChatBySubject,importChat,setAfterTurnState,listPendingAfterTurn— только для включённых возможностей; общие тесты с флагамиsubjectChats,importChat,afterTurnState.
Заголовки раздумий
С 0.69. thoughtTitles у createChatService — короткий заголовок раздумья вместо «Думал 3 сек» («Продумываю правила
игры»). Заголовок провайдера (сводка GPT через OpenRouter) берётся как есть, если он на языке человека; иначе — пересказ
дешёвой моделью model в фоне, ход не ждёт. Хранится в части рассуждения (providerMetadata.agentChat.thoughtTitle),
экран получает его в metadata.thoughtTitles (и без права reasoning: на месте раздумья — пустая часть, текста нет);
в запрос основной модели не идёт. Расход — thought_title. Настройки:
enabled (в том числе функция по чату), showWithoutReasoning (по умолчанию да: заголовок виден и без права
reasoning — приложение разрешает показывать суть раздумий; текст раздумья уходит модели заголовков), hiddenNames,
maxLength, maxPerTurn (на ответ целиком), minChars, timeoutMs, language. Подробно —
docs/getting-started.md, «Значки шагов и заголовки раздумий».
Проверка ответа, часы, «не доставлено»
С 0.49. Всё по желанию; без этих настроек ход побайтно прежний.
replyChecksуcreateChatService— проверки готового ответа перед записью: список или{ checks, web }. Готовые — пакет@ykurilov/agent-chat-reply-check(чужие слова, выдуманные ссылки); своя —ReplyCheck:({ text, seenTexts, rewrite, … }) => { text, note? } | void. Веб:web: "replace"(по умолчанию) — печатать и заменить исправленным,"hold"— не печатать до проверки. Мессенджеры получают только проверенный текст. Переписка — расходanswer_correction. С 0.51:{ checks, knownTexts }— тексты приложения вне инструкции, законные для проверок; служебные сообщения «виденным» не считаются (реплики сотрудника с 0.53 — считаются);service.checkReply({ ownerId, chatId, text })— те же проверки над текстом вне хода. Подробно — docs/reply-check.md.now— часы службы: дата в запросе к модели,prepareTurn.now, время сценариев. Тесты подменяют время целиком.beforeTurnкроме отказа может вернуть{ hold: true }— записать сообщение и промолчать (steerотвечает статусомheld), или{ reply: { text } }— ответить готовым текстом без модели. «Записать и промолчать» лимит не тратит.markUndelivered({ ownerId, chatId, messageId })— ответ не дошёл наружу: он остаётся в истории с пометкойmetadata.undelivered, модель его больше не видит, экран показывает «Не доставлено».deliverNoticesполучилtimeZone(пояс человека для даты в инструкции),awaitAnswer(дождаться хода и вернуть готовый проверенный текст вanswer; с 0.53 там жеparts— части ответа для человека: текст, файлы, вызовы инструментов с результатом, без рассуждений и служебных частей,deliverableParts) иusageSource. Ход, где агент пишет первым, пишется в расход какfirst_message. С 0.53 —maxServerTurnsInRow: свой потолок ходов сервера подряд без человека (по умолчанию 5,MAX_SERVER_TURNS_IN_ROW),false— без потолка, частоту держит вызывающий (так ходят касания сценария).appendStaffMessage({ ownerId, chatId, text, staffName?, wakeAgent?, deliveryKey? })(с 0.53) — реплика живого сотрудника в конец ветки. Модель видит её словами сотрудника («Живой сотрудник «Ольга» ответил человеку вместо тебя…»), экран — сообщением сотрудника (StaffMessageскина). Проверки ответа агента её текст знают, аperson_link_viewссылкой человека её не считает. Ход сама не начинает;wakeAgent: true— агент отвечает сразу. Занятый чат —busy, пустой текст —TypeError. В запросе модели реплика идёт в одном сообщении с соседними словами человека (через пустую строку), поэтому пометки «ход оборвался» рядом с ней нет (с 0.57).
Надёжность хода
Всё это работает само; настройки — в agent у createChatService (createChatHandler). Полное описание — в
docs/getting-started.md, раздел «Надёжность хода».
- Временные ошибки провайдера повторяются тихо; перегрузка или лимит до начала ответа — один переход к запасному провайдеру той же модели (
providerFallback).providerFallback.onSilence: "restart-step-on-next"— модель замолчала посреди ответа: шаг стирается (data-step-reset,withoutResetSteps) и начинается заново у следующего провайдера. - «Повторить» после сбоя продолжает оборванный ход с выполненных инструментов, а не начинает заново; обрыв посреди ответа дописывается сам (до двух раз); переполнение окна модели — сжатие и продолжение, ошибка
context_overflowтолько если не помогло. - Пределы:
toolTimeoutMs(120 с на серверный инструмент),modelSilenceTimeout(90 с до первого куска, 120 с между кусками; свой срок провайдера — полеmodelSilenceTimeoutу фабрикcreate…Provider, в том числе для запасного),toolResultMaxLength(50 000 знаков).sequentialToolsпускает инструменты с последствиями по одному, очередь одна на чат. - Расход не теряется: оборванные ответы пишутся оценкой (
estimated), у OpenRouter оценка потом сверяется с настоящей ценой. Дата в запросе — по поясу человека (timeZone), без него —agent.timeZoneили пояс лимитов (limits.timeZone), без них — сервера. - Журнал промахов кэша (
cacheMissLog: falseвыключает), заголовокx-session-idу OpenRouter, чистка секретов из ошибок инструментов (scrubSecrets). - Защита: запись с чужой страницы — 403 (
allowedOrigins, в том числе шаблонhttps://*.example.com), не больше трёх одновременных ответов на человека (maxConcurrentTurnsPerUser).
Своё хранилище для нескольких копий сервера: поле записи alive, markAnswerAlive(messageId) и listSilentAnswerIds({ silentForMs, chatId? }); sumUsage учитывает фильтры messageId и source. Общие тесты describeChatStorageContract всё это проверяют. Вид ошибки context_overflow добавьте в свой switch и словарь надписей.
Ключи провайдеров читаются из окружения сервера. Версия ai закреплена: 7.0.122.
Подключение целиком: docs/getting-started.md.
Журнал изменений и что делать при обновлении: CHANGELOG.md. Все пакеты SDK выходят вместе и с одной версией; обновление — pnpm update "@ykurilov/agent-chat-*" --latest.
Расход и лимиты: ядро пишет токены и стоимость в долларах (costUsd, по ценам модели на момент хода) каждого ответа и фонового сжатия в журнал расхода
(insertUsageRecord, sumUsage, а сумма по списку чатов одним запросом — sumUsageByChat({ chatIds, source? })), цены лежат в профиле модели (pricing), а createChatService({ limits }) проверяет лимит до хода.
Лимиты и суммы считает @ykurilov/agent-chat-limits. Ручное compactNow лимит не проверяет.
Вид расхода (source) — штатный (BUILT_IN_USAGE_SOURCES: ответ, сжатие, память, голос voice_input/voice_polish и другие)
или свой вид приложения строкой, например service.recordUsage({ …, source: "font_download" }), без приведения типа. Лимиты
считают все виды вместе; отчёт по одному виду — sumUsage({ source }).
Контракты интеграции приложения: HTTP-приветствие, текущий вопрос, номера фото, метаданные и тесты.
