@ykurilov/agent-chat-core 0.78.0 → 0.79.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,17 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.79.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 88886de: Новые контракты приложения: HTTP `newChatGreeting` после `newChatData`, проверенное `createChat.title`, текущий вопрос `contextBlocks.userMessage` для ожидаемой асинхронной классификации, direct DeepSeek `low/high/max` и опциональные устойчивые номера фото.
8
+
9
+ `createAttachments({ imageNumbers: true })` сохраняет серверный номер вместе с принятой картинкой; `service.getChatImages(userId, chatId)` возвращает соответствие номер/attachmentId полной активной ветки. Номера не меняются при повторе файла, перезапуске, компакции и переключении ветки; скрытые ветки не отдают свои номера другим файлам. Старые файлы без меток не заполняются задним числом, без настройки запрос модели прежний. Одновременные процессы используют существующую распределённую аренду чата.
10
+
11
+ Приветствие вызывается только при HTTP-создании, не при GET/reload; повтор POST создаёт другой чат. `userMessage` равен null у служебного хода, approval/resume сохраняет исходный вопрос. Данные и название не попадают автоматически в системные инструкции. `importChat.chat.data` уже доступно в 0.77.0; тестовая модель — существующий AI SDK `ai/test`, новый тестовый пакет не нужен. Полные принятые contextBlocks после компакции передаются без обрезания, в прежнем порядке.
12
+
13
+ Как обновить: все SDK-пакеты одной версией; новые настройки включать явно. SQL-схемы не меняются. [Точные примеры и ограничения](https://github.com/ykurilov/agent-chat-sdk/blob/main/docs/app-chat-contracts.md).
14
+
3
15
  ## 0.78.0
4
16
 
5
17
  No changes in this release.
package/README.md CHANGED
@@ -158,3 +158,5 @@ docs/getting-started.md, «Значки шагов и заголовки раз
158
158
  Вид расхода (`source`) — штатный (`BUILT_IN_USAGE_SOURCES`: ответ, сжатие, память, голос `voice_input`/`voice_polish` и другие)
159
159
  или свой вид приложения строкой, например `service.recordUsage({ …, source: "font_download" })`, без приведения типа. Лимиты
160
160
  считают все виды вместе; отчёт по одному виду — `sumUsage({ source })`.
161
+
162
+ Контракты интеграции приложения: [HTTP-приветствие, текущий вопрос, номера фото, метаданные и тесты](https://github.com/ykurilov/agent-chat-sdk/blob/main/docs/app-chat-contracts.md).
@@ -1618,7 +1618,7 @@ function screenDataOf(name, data, views) {
1618
1618
  }
1619
1619
  }
1620
1620
  function isModelOnlyPart(part) {
1621
- return part.type === FROZEN_CONTEXT_PART_TYPE || part.type === END_TURN_PART_TYPE;
1621
+ return part.type === FROZEN_CONTEXT_PART_TYPE || part.type === END_TURN_PART_TYPE || part.type === "data-chat-image-number";
1622
1622
  }
1623
1623
 
1624
1624
  // src/thought-titles/thought-title-parts.ts
@@ -7003,6 +7003,8 @@ async function freezeTurnContext(input, turn, base, place) {
7003
7003
  const { memory, ownerId } = input;
7004
7004
  if (memory && ownerId && !input.group) sources.memory = () => memoryBlocks(memory, { userId: ownerId, chatId: input.chatId });
7005
7005
  if (input.contextBlocks) {
7006
+ const question2 = turn.history.findLast((message) => message.role === "user" && !isStateMessage(message));
7007
+ const human = question2 && isHumanMessage(question2) ? question2 : void 0;
7006
7008
  sources.app = appBlocksSource({
7007
7009
  contextBlocks: input.contextBlocks,
7008
7010
  timeoutMs: input.contextBlocksTimeoutMs ?? DEFAULT_CONTEXT_BLOCKS_TIMEOUT_MS,
@@ -7012,6 +7014,7 @@ async function freezeTurnContext(input, turn, base, place) {
7012
7014
  chatId: input.chatId,
7013
7015
  ownerId: input.ownerId ?? "",
7014
7016
  messageId: input.messageId,
7017
+ userMessage: human ? { id: human.id, text: human.parts.filter((part) => part.type === "text").map((part) => part.text).join("\n") } : null,
7015
7018
  chat: structuredClone({ ...input.chatData && { data: input.chatData } }),
7016
7019
  state: structuredClone(turn.state),
7017
7020
  ...input.subject && { subject: structuredClone(input.subject) },
@@ -8936,11 +8939,16 @@ function createDeepSeekProvider(connectionOptions = {}) {
8936
8939
  createLanguageModel: (providerModelId, apiKey, options2) => createDeepSeek({ apiKey, fetch: options2?.fetch, ...connection })(providerModelId),
8937
8940
  // DeepSeek кэширует начало запроса сам.
8938
8941
  requestOptions: () => ({}),
8939
- // Рассуждения у DeepSeek только включаются и выключаются полем `thinking`; силы у них нет.
8942
+ // Direct Chat Completions: thinking отдельно от reasoning_effort (официальные docs, 24.10.2026).
8943
+ // Старые off/on сохраняют прежние байты; выбор силы включается профилем приложения.
8940
8944
  reasoningRequest: (level) => {
8941
8945
  if (level === "off") return { providerOptions: { deepseek: { thinking: { type: "disabled" } } } };
8942
8946
  if (level === "on") return { providerOptions: { deepseek: { thinking: { type: "enabled" } } } };
8943
- throw new Error(`DeepSeek \u043D\u0435 \u0437\u0430\u0434\u0430\u0451\u0442 \u0441\u0438\u043B\u0443 \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u0439 (\u0443\u0440\u043E\u0432\u0435\u043D\u044C \xAB${level}\xBB), \u0442\u043E\u043B\u044C\u043A\u043E \xABoff\xBB \u0438 \xABon\xBB`);
8947
+ const effort = level === "minimal" ? "low" : level === "medium" ? "high" : level;
8948
+ if (effort === "low" || effort === "high" || effort === "max") {
8949
+ return { providerOptions: { deepseek: { thinking: { type: "enabled" }, reasoningEffort: effort } } };
8950
+ }
8951
+ throw new Error(`DeepSeek: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u0443\u0440\u043E\u0432\u0435\u043D\u044C \u0440\u0430\u0441\u0441\u0443\u0436\u0434\u0435\u043D\u0438\u0439 \xAB${level}\xBB`);
8944
8952
  },
8945
8953
  // Обрезки хвоста (`min_p`) в API DeepSeek нет (api-docs.deepseek.com: только temperature и top_p), а пакет провайдера
8946
8954
  // незнакомые поля не шлёт: `samplingRequest` нет, `sampling.minP` модели сюда не уходит.
@@ -10548,6 +10556,52 @@ function voiceChangeNote(set) {
10548
10556
  var DEFAULT_VOICE_MODE_HINT = voiceModeHint();
10549
10557
  var DEFAULT_TEXT_MODE_NOTE = "[\u0421\u043B\u0443\u0436\u0435\u0431\u043D\u0430\u044F \u043F\u043E\u043C\u0435\u0442\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F, \u0447\u0435\u043B\u043E\u0432\u0435\u043A \u0435\u0451 \u043D\u0435 \u043F\u0438\u0441\u0430\u043B \u0438 \u043D\u0435 \u0432\u0438\u0434\u0438\u0442: \u0440\u0430\u0437\u0433\u043E\u0432\u043E\u0440 \u0441\u043D\u043E\u0432\u0430 \u0438\u0434\u0451\u0442 \u0442\u0435\u043A\u0441\u0442\u043E\u043C, \u043E\u0442\u0432\u0435\u0442 \u043D\u0435 \u043E\u0437\u0432\u0443\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044F \u2014 \u043E\u0431\u044B\u0447\u043D\u044B\u0439 \u043F\u0438\u0441\u044C\u043C\u0435\u043D\u043D\u044B\u0439 \u0441\u0442\u0438\u043B\u044C \u0441\u043D\u043E\u0432\u0430 \u0443\u043C\u0435\u0441\u0442\u0435\u043D. \u0422\u0435\u0433\u0438 \u0433\u043E\u043B\u043E\u0441\u0430 \u0432 \u043A\u0432\u0430\u0434\u0440\u0430\u0442\u043D\u044B\u0445 \u0441\u043A\u043E\u0431\u043A\u0430\u0445 ([laughs], [sighs] \u0438 \u043F\u043E\u0434\u043E\u0431\u043D\u044B\u0435) \u0431\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0441\u0442\u0430\u0432\u044C, \u0434\u0430\u0436\u0435 \u0435\u0441\u043B\u0438 \u043E\u043D\u0438 \u0435\u0441\u0442\u044C \u0432 \u043F\u0440\u0435\u0436\u043D\u0438\u0445 \u043E\u0442\u0432\u0435\u0442\u0430\u0445 \u0432\u044B\u0448\u0435. \u041D\u0435 \u0443\u043F\u043E\u043C\u0438\u043D\u0430\u0439 \u044D\u0442\u0443 \u043F\u043E\u043C\u0435\u0442\u043A\u0443.]";
10550
10558
 
10559
+ // src/attachments/chat-image-numbers.ts
10560
+ var CHAT_IMAGE_NUMBER_PART = "data-chat-image-number";
10561
+ var PREFIX = "attachment:";
10562
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10563
+ function imageId(part) {
10564
+ if (part.type !== "file" || !part.mediaType.startsWith("image/") || !part.url.startsWith(PREFIX)) return void 0;
10565
+ const id = part.url.slice(PREFIX.length);
10566
+ return UUID.test(id) ? id : void 0;
10567
+ }
10568
+ function chatImageReferences(messages) {
10569
+ const byId = /* @__PURE__ */ new Map();
10570
+ const byNumber = /* @__PURE__ */ new Map();
10571
+ for (const message of messages) {
10572
+ if (!isHumanMessage(message)) continue;
10573
+ const files = new Set(message.parts.map(imageId).filter((id) => id !== void 0));
10574
+ for (const part of message.parts) {
10575
+ if (part.type !== CHAT_IMAGE_NUMBER_PART) continue;
10576
+ const data = part.data;
10577
+ if (!data || typeof data.attachmentId !== "string" || !files.has(data.attachmentId) || !Number.isSafeInteger(data.number) || data.number < 1) continue;
10578
+ const number = data.number;
10579
+ const existing = byId.get(data.attachmentId);
10580
+ if (existing && existing.number !== number || byNumber.has(number) && byNumber.get(number) !== data.attachmentId) throw new Error("\u041F\u043E\u0432\u0440\u0435\u0436\u0434\u0435\u043D\u044B \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u044B\u0435 \u043D\u043E\u043C\u0435\u0440\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0447\u0430\u0442\u0430");
10581
+ byNumber.set(number, data.attachmentId);
10582
+ if (!existing) byId.set(data.attachmentId, { number, attachmentId: data.attachmentId, messageId: message.id });
10583
+ }
10584
+ }
10585
+ return [...byId.values()].sort((a, b) => a.number - b.number);
10586
+ }
10587
+ function numberedImageParts(history, parts) {
10588
+ if (!parts.some((part) => imageId(part) !== void 0)) return parts;
10589
+ const references = chatImageReferences(history);
10590
+ const numbers = new Map(references.map((ref) => [ref.attachmentId, ref.number]));
10591
+ let max = references.at(-1)?.number ?? 0;
10592
+ return parts.flatMap((part) => {
10593
+ const id = imageId(part);
10594
+ if (!id) return [part];
10595
+ let number = numbers.get(id);
10596
+ if (number === void 0) {
10597
+ if (max === Number.MAX_SAFE_INTEGER) throw new Error("\u0418\u0441\u0447\u0435\u0440\u043F\u0430\u043D\u044B \u043D\u043E\u043C\u0435\u0440\u0430 \u043A\u0430\u0440\u0442\u0438\u043D\u043E\u043A \u0447\u0430\u0442\u0430");
10598
+ number = ++max;
10599
+ numbers.set(id, number);
10600
+ }
10601
+ return [part, { type: CHAT_IMAGE_NUMBER_PART, data: { attachmentId: id, number } }];
10602
+ });
10603
+ }
10604
+
10551
10605
  // src/subject-chats/subject-chats.ts
10552
10606
  var SUBJECT_KEY_MAX_LENGTH = 200;
10553
10607
  var SUBJECT_DATA_MAX_BYTES = CHAT_DATA_MAX_BYTES;
@@ -10675,7 +10729,7 @@ function createSubjectChats(deps) {
10675
10729
  chatId,
10676
10730
  parentId: messages.at(-1)?.id ?? null,
10677
10731
  role: message.role,
10678
- parts,
10732
+ parts: deps.attachments?.imageNumbers && message.role === "user" ? numberedImageParts(messages, parts) : parts,
10679
10733
  createdAt: isoDate(message.createdAt)
10680
10734
  })
10681
10735
  );
@@ -10752,7 +10806,7 @@ function assertGreeting(greeting) {
10752
10806
  throw new TypeError(`\u0422\u0435\u043A\u0441\u0442 \u0438\u043B\u0438 \u0443\u043A\u0430\u0437\u0430\u043D\u0438\u0435 \u043F\u0440\u0438\u0432\u0435\u0442\u0441\u0442\u0432\u0438\u044F \u2014 \u0441\u0442\u0440\u043E\u043A\u0430 \u043E\u0442 1 \u0434\u043E ${GREETING_MAX_LENGTH} \u0437\u043D\u0430\u043A\u043E\u0432.`);
10753
10807
  }
10754
10808
  }
10755
- var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10809
+ var UUID2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10756
10810
  var ISO_DATE_WITH_ZONE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d{1,9})?)?(Z|[+-]\d{2}:\d{2})$/;
10757
10811
  function isValidDate(value) {
10758
10812
  return typeof value === "string" && ISO_DATE_WITH_ZONE.test(value) && !Number.isNaN(Date.parse(value));
@@ -10761,7 +10815,7 @@ function isoDate(value) {
10761
10815
  return new Date(value).toISOString();
10762
10816
  }
10763
10817
  function assertImport({ chat, messages }) {
10764
- if (chat.id !== void 0 && (typeof chat.id !== "string" || !UUID.test(chat.id))) throw new TypeError("id \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0435\u043C\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u2014 UUID.");
10818
+ if (chat.id !== void 0 && (typeof chat.id !== "string" || !UUID2.test(chat.id))) throw new TypeError("id \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0435\u043C\u043E\u0433\u043E \u0447\u0430\u0442\u0430 \u2014 UUID.");
10765
10819
  if (chat.subjectKey !== void 0) assertSubjectKey(chat.subjectKey);
10766
10820
  if (chat.subjectData !== void 0) assertSubjectData(chat.subjectData);
10767
10821
  if (chat.data !== void 0) assertChatData(chat.data);
@@ -13417,6 +13471,10 @@ function createChatService(input) {
13417
13471
  function takeTurnSlot(userId, chatId) {
13418
13472
  startingSlots.set(chatId, turnSlots.take(userId));
13419
13473
  }
13474
+ async function filePartsWithNumbers(chatId, parts) {
13475
+ if (!options2.attachments?.imageNumbers || !parts.some((part) => part.mediaType.startsWith("image/"))) return parts;
13476
+ return numberedImageParts(await storage.listMessages(chatId), parts);
13477
+ }
13420
13478
  async function acceptFiles(userId, chatId, files) {
13421
13479
  return files.length > 0 ? options2.attachments.acceptUserParts({ userId, chatId, parts: files }) : [];
13422
13480
  }
@@ -13791,7 +13849,7 @@ function createChatService(input) {
13791
13849
  parentId: branch.at(-1)?.id ?? null,
13792
13850
  role: "user",
13793
13851
  parts: [
13794
- ...fileParts,
13852
+ ...await filePartsWithNumbers(chatId, fileParts),
13795
13853
  ...text.trim().length > 0 ? [{ type: "text", text }] : [],
13796
13854
  ...sendOptions?.voiceMode === true ? [voiceModePart(await currentVoice(userId), void 0, uniqueVoiceSounds(sendOptions.voiceSounds ?? []))] : [],
13797
13855
  ...[channelPart(sendOptions?.channel ?? "web", sendOptions?.group)].filter((part) => part !== void 0),
@@ -14041,7 +14099,7 @@ function createChatService(input) {
14041
14099
  chatId,
14042
14100
  parentId,
14043
14101
  role: "user",
14044
- parts: [...fileParts, ...text.trim().length > 0 ? [{ type: "text", text }] : [], steerPart()]
14102
+ parts: [...await filePartsWithNumbers(chatId, fileParts), ...text.trim().length > 0 ? [{ type: "text", text }] : [], steerPart()]
14045
14103
  });
14046
14104
  await confirmFiles(correction.id, fileParts);
14047
14105
  await storage.insertMessage(correction);
@@ -14122,6 +14180,7 @@ function createChatService(input) {
14122
14180
  liveDemos: input.agent.liveDemos === true,
14123
14181
  checksReplies: replyChecks.checks.length > 0,
14124
14182
  async createChat(userId, createOptions) {
14183
+ const title = createOptions?.title === void 0 ? void 0 : normalizeChatTitle(createOptions.title);
14125
14184
  if (createOptions?.data !== void 0) {
14126
14185
  assertChatData(createOptions.data);
14127
14186
  if (!storage.updateChatData) throw new Error("\u0414\u0430\u043D\u043D\u044B\u0435 \u0447\u0430\u0442\u0430 \u0442\u0440\u0435\u0431\u0443\u044E\u0442 \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0430 \u0441 updateChatData: \u0445\u0440\u0430\u043D\u0438\u043B\u0438\u0449\u0435 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0435\u0433\u043E \u043D\u0435 \u0443\u043C\u0435\u0435\u0442.");
@@ -14133,6 +14192,7 @@ function createChatService(input) {
14133
14192
  ownerId: userId,
14134
14193
  ...await choiceForNewChat(userId),
14135
14194
  ...createOptions?.origin && { origin: createOptions.origin },
14195
+ ...title !== void 0 && { title },
14136
14196
  ...createOptions?.data !== void 0 && { data: createOptions.data }
14137
14197
  },
14138
14198
  createOptions?.greeting
@@ -14289,6 +14349,10 @@ function createChatService(input) {
14289
14349
  const chat = await ownChat(userId, chatId);
14290
14350
  return structuredClone(chatDataOfInput(chat) ?? {});
14291
14351
  },
14352
+ async getChatImages(userId, chatId) {
14353
+ await ownChat(userId, chatId);
14354
+ return chatImageReferences(await currentBranch(chatId));
14355
+ },
14292
14356
  async listChats(userId, listOptions) {
14293
14357
  if (listOptions?.data !== void 0) {
14294
14358
  assertChatDataFilter(listOptions.data);
@@ -14496,7 +14560,7 @@ function createChatService(input) {
14496
14560
  parentId: edited2.parentId,
14497
14561
  role: "user",
14498
14562
  parts: [
14499
- ...fileParts,
14563
+ ...await filePartsWithNumbers(chatId, fileParts),
14500
14564
  ...text.trim().length > 0 ? [{ type: "text", text }] : [],
14501
14565
  ...editOptions?.voiceMode === true ? [
14502
14566
  voiceModePart(
@@ -15212,6 +15276,8 @@ export {
15212
15276
  voiceChangeNote,
15213
15277
  DEFAULT_VOICE_MODE_HINT,
15214
15278
  DEFAULT_TEXT_MODE_NOTE,
15279
+ CHAT_IMAGE_NUMBER_PART,
15280
+ chatImageReferences,
15215
15281
  SUBJECT_KEY_MAX_LENGTH,
15216
15282
  SUBJECT_DATA_MAX_BYTES,
15217
15283
  GREETING_MAX_LENGTH,
@@ -15237,4 +15303,4 @@ export {
15237
15303
  InvalidWidgetAnswerError,
15238
15304
  createChatService
15239
15305
  };
15240
- //# sourceMappingURL=chunk-BQTUOC3T.js.map
15306
+ //# sourceMappingURL=chunk-AJSRGBVM.js.map