@ykurilov/agent-chat-core 0.64.0 → 0.65.1

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,17 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.65.1
4
+
5
+ ### Patch Changes
6
+
7
+ - ca56c29: Запись идущего ответа: «записать сейчас» (начатый вызов, итог инструмента) пишет только собранное к вызову; промежуточные результаты, пришедшие во время записи, идут окном 500 мс, а не второй записью следом.
8
+
9
+ ## 0.65.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 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.
14
+
3
15
  ## 0.64.0
4
16
 
5
17
  ### 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
  }
@@ -5445,6 +5465,8 @@ function createAnswerSaver(storage, messageId, durations, usage, onTakenOver, ex
5445
5465
  let lastWriteAt = Date.now();
5446
5466
  let lastSignAt = lastWriteAt;
5447
5467
  let unsaved = false;
5468
+ let changes = 0;
5469
+ let savedChanges = 0;
5448
5470
  let closed = false;
5449
5471
  let writing = null;
5450
5472
  let lastFence = 0;
@@ -5464,6 +5486,7 @@ function createAnswerSaver(storage, messageId, durations, usage, onTakenOver, ex
5464
5486
  }
5465
5487
  async function write() {
5466
5488
  unsaved = false;
5489
+ const writtenChanges = changes;
5467
5490
  lastWriteAt = Date.now();
5468
5491
  lastSignAt = lastWriteAt;
5469
5492
  const parts = latest;
@@ -5477,6 +5500,7 @@ function createAnswerSaver(storage, messageId, durations, usage, onTakenOver, ex
5477
5500
  } catch (failure) {
5478
5501
  noteFailure(failure);
5479
5502
  }
5503
+ savedChanges = Math.max(savedChanges, writtenChanges);
5480
5504
  callsWritten(parts);
5481
5505
  }
5482
5506
  async function markAlive() {
@@ -5554,7 +5578,7 @@ function createAnswerSaver(storage, messageId, durations, usage, onTakenOver, ex
5554
5578
  reachedFence = fence;
5555
5579
  reachFence(fence);
5556
5580
  } else {
5557
- unsaved = true;
5581
+ noteChange();
5558
5582
  scheduleWrite();
5559
5583
  }
5560
5584
  }
@@ -5568,13 +5592,18 @@ function createAnswerSaver(storage, messageId, durations, usage, onTakenOver, ex
5568
5592
  if (waiting) waiting.push(chunk);
5569
5593
  else feed.enqueue(chunk);
5570
5594
  }
5595
+ function noteChange() {
5596
+ changes++;
5597
+ unsaved = true;
5598
+ }
5571
5599
  async function writeNow() {
5600
+ const target = changes;
5572
5601
  for (; ; ) {
5573
5602
  clearTimeout(timer);
5574
5603
  timer = void 0;
5575
5604
  if (writing) await writing;
5576
- else if (unsaved && !closed) startWrite();
5577
- else return;
5605
+ else if (savedChanges < target && !closed) startWrite();
5606
+ else return scheduleWrite();
5578
5607
  }
5579
5608
  }
5580
5609
  async function settleAssembly() {
@@ -5612,7 +5641,7 @@ function createAnswerSaver(storage, messageId, durations, usage, onTakenOver, ex
5612
5641
  const held = waiting;
5613
5642
  waiting = null;
5614
5643
  for (const chunk of held) feed.enqueue(chunk);
5615
- unsaved = true;
5644
+ noteChange();
5616
5645
  await writeNow();
5617
5646
  };
5618
5647
  const current = erase().finally(() => {
@@ -5631,7 +5660,7 @@ function createAnswerSaver(storage, messageId, durations, usage, onTakenOver, ex
5631
5660
  clearTimeout(aliveTimer);
5632
5661
  await rewinding?.catch(() => {
5633
5662
  });
5634
- unsaved = true;
5663
+ noteChange();
5635
5664
  await settleAssembly();
5636
5665
  await writeNow();
5637
5666
  },
@@ -9533,9 +9562,10 @@ var CHAT_CAPABILITIES = [
9533
9562
  "toolDetails",
9534
9563
  "steer",
9535
9564
  "modelDetails",
9536
- "errorDetails"
9565
+ "errorDetails",
9566
+ "appNotices"
9537
9567
  ];
9538
- var OFF_BY_DEFAULT = /* @__PURE__ */ new Set(["errorDetails"]);
9568
+ var OFF_BY_DEFAULT = /* @__PURE__ */ new Set(["errorDetails", "appNotices"]);
9539
9569
  function resolveChatCapabilities(given) {
9540
9570
  const resolved = Object.fromEntries(CHAT_CAPABILITIES.map((capability) => [capability, !OFF_BY_DEFAULT.has(capability)]));
9541
9571
  if (given === void 0 || given === null) return resolved;
@@ -9551,6 +9581,7 @@ function resolveChatCapabilities(given) {
9551
9581
  if (!resolved.modelDetails) {
9552
9582
  resolved.modelChoice = false;
9553
9583
  resolved.errorDetails = false;
9584
+ resolved.appNotices = false;
9554
9585
  }
9555
9586
  return resolved;
9556
9587
  }
@@ -12346,10 +12377,17 @@ function createChatService(input) {
12346
12377
  return scope.capabilities;
12347
12378
  }
12348
12379
  let warnedErrorDetailsWithoutModel = false;
12380
+ let warnedAppNoticesWithoutModel = false;
12349
12381
  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.");
12382
+ if (given?.modelDetails !== false) return;
12383
+ if (!warnedErrorDetailsWithoutModel && given.errorDetails === true) {
12384
+ warnedErrorDetailsWithoutModel = true;
12385
+ 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.");
12386
+ }
12387
+ if (!warnedAppNoticesWithoutModel && given.appNotices === true) {
12388
+ warnedAppNoticesWithoutModel = true;
12389
+ 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.");
12390
+ }
12353
12391
  }
12354
12392
  async function askCapabilities(userId, chat) {
12355
12393
  if (!options2.chatCapabilities) return resolveChatCapabilities(void 0);
@@ -12559,6 +12597,7 @@ function createChatService(input) {
12559
12597
  const pending = runningAt < 0 ? [] : branch.slice(runningAt + 1);
12560
12598
  const capabilities = await capabilitiesOf(userId, withClosure(stored, closed), scope);
12561
12599
  const visibility = clientVisibilityOf(capabilities, isScreenTool);
12600
+ const screenNotices = capabilities.appNotices && options2.appNoticeViews ? { appNoticeViews: options2.appNoticeViews } : {};
12562
12601
  const undelivered = undeliveredIdsOf(branch);
12563
12602
  const markedUndelivered = (message) => undelivered.has(message.id) ? { ...message, metadata: { ...message.metadata, undelivered: true } } : message;
12564
12603
  const visible = (list) => visibility.showsEverything ? list : list.map((message) => visibility.visibleMessage(message));
@@ -12570,10 +12609,13 @@ function createChatService(input) {
12570
12609
  messages: visible(
12571
12610
  publicMessages(
12572
12611
  shown.filter((message) => message.id !== runningAnswerId || resumeBases.get(chatId)?.id === runningAnswerId).map((message) => message.id === runningAnswerId ? resumeBases.get(chatId) : markedUndelivered(toAgentChatMessage(message))),
12573
- hiddenStateKey
12612
+ hiddenStateKey,
12613
+ screenNotices
12574
12614
  )
12575
12615
  ),
12576
- ...pending.length > 0 && { pendingMessages: visible(publicMessages(pending.map(toAgentChatMessage), hiddenStateKey)) },
12616
+ ...pending.length > 0 && {
12617
+ pendingMessages: visible(publicMessages(pending.map(toAgentChatMessage), hiddenStateKey, screenNotices))
12618
+ },
12577
12619
  // Ход передаётся новому ответу на поправки: он вот-вот пойдёт, поток подождёт его.
12578
12620
  running: runningId !== null || handOffs.has(chatId),
12579
12621
  detachedAnswer: isDetachedAnswer(answerBeforeCorrections(branch), runningId),
@@ -14421,4 +14463,4 @@ export {
14421
14463
  InvalidWidgetAnswerError,
14422
14464
  createChatService
14423
14465
  };
14424
- //# sourceMappingURL=chunk-EWEEYX6U.js.map
14466
+ //# sourceMappingURL=chunk-4NQEQWGW.js.map