@ykurilov/agent-chat-core 0.64.0 → 0.65.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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.65.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 814ee7f: Пометка «Агент написал первым — человек молчал 1 ч 7 мин» перед ответом, который агент написал сам. Ядро: право чата `appNotices` (по умолчанию выключено) — служебные пометки приложения (`kind: "app"`) уходят в историю без текста для модели и только через белый список `appNoticeViews` (имя → безопасные данные для экрана; без функции пометка не уходит); `modelDetails: false` выключает и `appNotices`. React: `noticeLines` у `AgentChat` (имя пометки → строка), договор скина 27 — необязательный `ServiceLine`. Сценарии: в данных пометки `silentMs` (от последнего сообщения человека) и `exit`, проекция для экрана регистрируется в `withChat`, готовые строки `flowNoticeLines()` с подписями `russianFlowNoticeLabels`/`englishFlowNoticeLabels` (`FlowLabels.notices`); `FlowTestChat` передаёт их в `renderChat(chatId, { noticeLines })`. Тандем рисует `ServiceLine`. Как подключить — CHANGES.md, раздел 0.65.0.
8
+
3
9
  ## 0.64.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -82,7 +82,7 @@ import type { ChatStorage } from "@ykurilov/agent-chat-core";
82
82
  - Данные чата без записи: `service.getChatData(userId, chatId)` — копия, проверки владения как у `updateChatData`.
83
83
  - Проверка ответа виджета на сервере: `checkAnswer(answer, ctx)` у своего виджета и общая `agent.widgets.checkAnswer(call, answer, ctx)` (для всех виджетов без своей). Зовётся после схемы под замком чата. Исход: `undefined` — принять, `{ answer }` — принять с заменой, `{ refuse: "текст" }` — отказать (`WidgetAnswerRefusedError`, сервер — 409 `widget_answer_refused`, виджет остаётся открытым). `ctx` — `ownerId`, `chatId`, `messageId`, `toolName`, `toolCallId`, `input`, `subject?`, `chatData?`, `state` (записи ложатся в ответ вместе с результатом виджета, хук `onStateChanged` — с причиной `widgetAnswer`), `emitAppEvent` (событие приложения вместе с ответом виджета), `signal` (отмена по сроку или «Стопу»: приложение слушает его само). Срок — `agent.widgets.checkAnswerTimeoutMs` (5 000 мс, заметно меньше `requestTimeoutMs` страницы). Исключение — 500, ничего не записано, в журнале только имя ошибки; срок вышел — `WidgetAnswerCheckTimeoutError`, сервер — 504 `widget_check_timeout`. Чат закрыли во время проверки — `ChatClosedError`, ответ не пишется.
84
84
  - Набор виджетов по чату: `agent.widgets.enabledFor({ chat: { id, ownerId, data?, subject? } })` → имена включённых. Выключенных модель не видит (нет в инструментах запроса), их прошлые вызовы идут модели текстом. Синхронная, те же данные — тот же набор; смена набора сбрасывает кэш модели. Неизвестное имя — строка в журнал, исключение — ошибка хода. Подробно — [docs/widgets.md](../../docs/widgets.md).
85
- - Права человека в чате: `chatCapabilities({ userId, chat })` возвращает запреты (`edit`, `regenerate`, `retry`, `branches`, `modelChoice`, `answerEditing`, `chatList`, `compact`, `attachments`, `voice`, `reasoning`, `toolDetails`, `steer`, `modelDetails`, `errorDetails`; по умолчанию всё `true`, кроме `errorDetails`; `modelDetails: false` выключает и `modelChoice`, и `errorDetails`). Неизвестное право или не булево значение — `TypeError`. Запрещённое действие — `ActionForbiddenError(action)` до любой записи и до `beforeTurn`. За один вызов службы права спрашиваются не больше раза. История несёт `capabilities`. Вручную — `service.assertCapability(userId, chatId, capability)`.
85
+ - Права человека в чате: `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)`.
86
86
  - Скрытие частей ответа: `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` (видит ли модель чата картинки). Права — на того, кто открыл поток.
87
87
  - Закрытый чат: `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 переносится в запись лениво, при первой правке или смене ветки.
88
88
  - `beforeCreateChat({ userId, origin? })` — можно ли человеку завести новый чат (`createChat`): `{ code, message }` → `ChatCreationRefusedError`. Чаты объектов и импорт хук не спрашивают. У чата `closed?: { code, message }`, хранилище — необязательный `setChatClosed(chatId, closed | null)`.
@@ -1545,12 +1545,14 @@ function publicAppStateParts(parts, hidden = isReservedAppStateKey) {
1545
1545
  return data === void 0 ? [] : [{ ...part, data }];
1546
1546
  });
1547
1547
  }
1548
- function publicMessages(messages, hidden = isReservedAppStateKey) {
1548
+ function publicMessages(messages, hidden = isReservedAppStateKey, options2 = {}) {
1549
+ const views = options2.appNoticeViews;
1550
+ const toScreen = views ? (part) => appNoticesForScreen(part, views) : withoutAppNotices;
1549
1551
  const shown = [];
1550
1552
  let waiting = [];
1551
1553
  for (const message of messages) {
1552
1554
  const hasService = message.parts.some((part) => part.type === APP_STATE_PART_TYPE || isModelOnlyPart(part) || hasAppNotice(part));
1553
- const parts = hasService ? publicAppStateParts(message.parts.filter((part) => !isModelOnlyPart(part)).flatMap(withoutAppNotices), hidden) : [...message.parts];
1555
+ const parts = hasService ? publicAppStateParts(message.parts.filter((part) => !isModelOnlyPart(part)).flatMap(toScreen), hidden) : [...message.parts];
1554
1556
  if (message.role === "user" && parts.length === 0) continue;
1555
1557
  const visible = hasService ? { ...message, parts } : message;
1556
1558
  if (isStateMessage(message)) {
@@ -1574,6 +1576,24 @@ function withoutAppNotices(part) {
1574
1576
  const notices = noticesOfPart(part).filter((notice) => notice.kind !== "app");
1575
1577
  return notices.length === 0 ? [] : [{ ...part, data: { ...part.data, notices } }];
1576
1578
  }
1579
+ function appNoticesForScreen(part, views) {
1580
+ if (part.type !== AGENT_NOTICE_PART_TYPE || !hasAppNotice(part)) return [part];
1581
+ const notices = noticesOfPart(part).flatMap((notice) => {
1582
+ if (notice.kind !== "app") return [notice];
1583
+ const data = screenDataOf(notice.name, notice.data, views);
1584
+ return data === void 0 ? [] : [{ kind: "app", name: notice.name, data }];
1585
+ });
1586
+ return notices.length === 0 ? [] : [{ ...part, data: { ...part.data, notices } }];
1587
+ }
1588
+ function screenDataOf(name, data, views) {
1589
+ const view = name !== void 0 && Object.hasOwn(views, name) ? views[name] : void 0;
1590
+ if (!view) return void 0;
1591
+ try {
1592
+ return view(data);
1593
+ } catch {
1594
+ return void 0;
1595
+ }
1596
+ }
1577
1597
  function isModelOnlyPart(part) {
1578
1598
  return part.type === FROZEN_CONTEXT_PART_TYPE || part.type === END_TURN_PART_TYPE;
1579
1599
  }
@@ -9533,9 +9553,10 @@ var CHAT_CAPABILITIES = [
9533
9553
  "toolDetails",
9534
9554
  "steer",
9535
9555
  "modelDetails",
9536
- "errorDetails"
9556
+ "errorDetails",
9557
+ "appNotices"
9537
9558
  ];
9538
- var OFF_BY_DEFAULT = /* @__PURE__ */ new Set(["errorDetails"]);
9559
+ var OFF_BY_DEFAULT = /* @__PURE__ */ new Set(["errorDetails", "appNotices"]);
9539
9560
  function resolveChatCapabilities(given) {
9540
9561
  const resolved = Object.fromEntries(CHAT_CAPABILITIES.map((capability) => [capability, !OFF_BY_DEFAULT.has(capability)]));
9541
9562
  if (given === void 0 || given === null) return resolved;
@@ -9551,6 +9572,7 @@ function resolveChatCapabilities(given) {
9551
9572
  if (!resolved.modelDetails) {
9552
9573
  resolved.modelChoice = false;
9553
9574
  resolved.errorDetails = false;
9575
+ resolved.appNotices = false;
9554
9576
  }
9555
9577
  return resolved;
9556
9578
  }
@@ -12346,10 +12368,17 @@ function createChatService(input) {
12346
12368
  return scope.capabilities;
12347
12369
  }
12348
12370
  let warnedErrorDetailsWithoutModel = false;
12371
+ let warnedAppNoticesWithoutModel = false;
12349
12372
  function warnErrorDetailsWithoutModel(given) {
12350
- if (warnedErrorDetailsWithoutModel || given?.errorDetails !== true || given.modelDetails !== false) return;
12351
- warnedErrorDetailsWithoutModel = true;
12352
- logError("chatCapabilities: errorDetails: true \u043F\u0440\u0438 modelDetails: false \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u2014 \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438 \u0441\u0431\u043E\u044F \u043D\u0430\u0437\u044B\u0432\u0430\u044E\u0442 \u043C\u043E\u0434\u0435\u043B\u044C \u0438 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430.");
12373
+ if (given?.modelDetails !== false) return;
12374
+ if (!warnedErrorDetailsWithoutModel && given.errorDetails === true) {
12375
+ warnedErrorDetailsWithoutModel = true;
12376
+ logError("chatCapabilities: errorDetails: true \u043F\u0440\u0438 modelDetails: false \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u2014 \u043F\u043E\u0434\u0440\u043E\u0431\u043D\u043E\u0441\u0442\u0438 \u0441\u0431\u043E\u044F \u043D\u0430\u0437\u044B\u0432\u0430\u044E\u0442 \u043C\u043E\u0434\u0435\u043B\u044C \u0438 \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440\u0430.");
12377
+ }
12378
+ if (!warnedAppNoticesWithoutModel && given.appNotices === true) {
12379
+ warnedAppNoticesWithoutModel = true;
12380
+ logError("chatCapabilities: appNotices: true \u043F\u0440\u0438 modelDetails: false \u0432\u044B\u043A\u043B\u044E\u0447\u0435\u043D\u043E \u2014 \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0435 \u043F\u043E\u043C\u0435\u0442\u043A\u0438 \u043F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u044E\u0442 \u0440\u0430\u0431\u043E\u0442\u0443 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u044F.");
12381
+ }
12353
12382
  }
12354
12383
  async function askCapabilities(userId, chat) {
12355
12384
  if (!options2.chatCapabilities) return resolveChatCapabilities(void 0);
@@ -12559,6 +12588,7 @@ function createChatService(input) {
12559
12588
  const pending = runningAt < 0 ? [] : branch.slice(runningAt + 1);
12560
12589
  const capabilities = await capabilitiesOf(userId, withClosure(stored, closed), scope);
12561
12590
  const visibility = clientVisibilityOf(capabilities, isScreenTool);
12591
+ const screenNotices = capabilities.appNotices && options2.appNoticeViews ? { appNoticeViews: options2.appNoticeViews } : {};
12562
12592
  const undelivered = undeliveredIdsOf(branch);
12563
12593
  const markedUndelivered = (message) => undelivered.has(message.id) ? { ...message, metadata: { ...message.metadata, undelivered: true } } : message;
12564
12594
  const visible = (list) => visibility.showsEverything ? list : list.map((message) => visibility.visibleMessage(message));
@@ -12570,10 +12600,13 @@ function createChatService(input) {
12570
12600
  messages: visible(
12571
12601
  publicMessages(
12572
12602
  shown.filter((message) => message.id !== runningAnswerId || resumeBases.get(chatId)?.id === runningAnswerId).map((message) => message.id === runningAnswerId ? resumeBases.get(chatId) : markedUndelivered(toAgentChatMessage(message))),
12573
- hiddenStateKey
12603
+ hiddenStateKey,
12604
+ screenNotices
12574
12605
  )
12575
12606
  ),
12576
- ...pending.length > 0 && { pendingMessages: visible(publicMessages(pending.map(toAgentChatMessage), hiddenStateKey)) },
12607
+ ...pending.length > 0 && {
12608
+ pendingMessages: visible(publicMessages(pending.map(toAgentChatMessage), hiddenStateKey, screenNotices))
12609
+ },
12577
12610
  // Ход передаётся новому ответу на поправки: он вот-вот пойдёт, поток подождёт его.
12578
12611
  running: runningId !== null || handOffs.has(chatId),
12579
12612
  detachedAnswer: isDetachedAnswer(answerBeforeCorrections(branch), runningId),
@@ -14421,4 +14454,4 @@ export {
14421
14454
  InvalidWidgetAnswerError,
14422
14455
  createChatService
14423
14456
  };
14424
- //# sourceMappingURL=chunk-EWEEYX6U.js.map
14457
+ //# sourceMappingURL=chunk-SA3DXJYB.js.map