@ykurilov/agent-chat-core 0.76.0 → 0.77.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.77.0
4
+
5
+ ### Patch Changes
6
+
7
+ - f4c13da: Сценарий может сохранить подтверждённую судьёй веху отдельно от перехода и потребовать её вместе с актуальным вопросом человека. Например, публикация своего приложения больше не забывается после шести реплик, а старый интерес к курсу не запускает переход при поздней публикации.
8
+
9
+ Серверные `confirmations` и `exitGuards` включаются по желанию. Квитанция привязана к человеку, чату, сценарию, ветке и revision критерия; модельные факты её не устанавливают. Поля `flow.confirmations`, `flow.confirmationCheck` и `flow.checkedTransition` защищены от записи и удаления через `model.state`, включая замену родительского `flow`. `revokeConfirmation` позволяет backend исправить атрибуцию. Положительная проверка и решение сохраняются вместе до перехода с защитой от устаревших записей и повторов. Стабильные инструкции и инструменты модели не меняются.
10
+
11
+ `previewMigration` и опция `requiresConfirmation` у `migrateChats` оставляют разговоры без подходящей квитанции на прежней версии. Старый путь или поле карточки не считаются доказательством. Увеличения контекста судьи, автоматической проверки владения доменом и изменений JSON редактора нет.
12
+
13
+ Подключение, отмена, миграция и ограничения: [docs/flow-confirmations.md](../../docs/flow-confirmations.md). Без новых опций поведение прежнее; SQL-миграций нет. Приложение обрабатывает `busy`/`stale` при отмене и само определяет доверенный маршрут исправления атрибуции.
14
+
3
15
  ## 0.76.0
4
16
 
5
17
  No changes in this release.
@@ -258,6 +258,13 @@ function createAppStateWriter(context) {
258
258
  return keys;
259
259
  }
260
260
  function commit(record, next) {
261
+ if (!context.serverFlowChecks) {
262
+ for (const field of ["confirmations", "confirmationCheck", "checkedTransition"]) {
263
+ if (JSON.stringify(valueAt(current, ["flow", field])) !== JSON.stringify(valueAt(next, ["flow", field]))) {
264
+ throw new TypeError(`state: flow.${field} \u2014 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043D\u043E\u0439 \u043F\u0440\u043E\u0432\u0435\u0440\u043A\u0438 SDK; \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 \u043C\u043E\u0434\u0435\u043B\u0438 \u043D\u0435 \u043C\u0435\u043D\u044F\u0435\u0442 \u0438 \u043D\u0435 \u0443\u0434\u0430\u043B\u044F\u0435\u0442 \u0435\u0433\u043E`);
265
+ }
266
+ }
267
+ }
261
268
  context.write(record);
262
269
  current = next;
263
270
  }
@@ -10022,6 +10029,15 @@ async function callWithRetries2(handler, input, timeoutMs, attempts) {
10022
10029
  }
10023
10030
  }
10024
10031
 
10032
+ // src/app-state/flow-state-commit.ts
10033
+ var ports = /* @__PURE__ */ new WeakMap();
10034
+ function registerFlowStateCommit(service, commit) {
10035
+ ports.set(service, commit);
10036
+ }
10037
+ function flowStateCommitter(service) {
10038
+ return ports.get(service);
10039
+ }
10040
+
10025
10041
  // src/agent-run/reply-check-types.ts
10026
10042
  var DEFAULT_REPLY_CHECKS_WEB = "replace";
10027
10043
  function resolveReplyChecks(setting) {
@@ -12552,6 +12568,7 @@ function checkedResult(result, state, chatId) {
12552
12568
  const records = [];
12553
12569
  const writer = createAppStateWriter({
12554
12570
  base: state,
12571
+ serverFlowChecks: true,
12555
12572
  write: (record) => records.push(record),
12556
12573
  logError: () => {
12557
12574
  },
@@ -14906,6 +14923,43 @@ function createChatService(input) {
14906
14923
  };
14907
14924
  }
14908
14925
  abandonedBurstReceivers.set(service, receivingAbandonedBurst);
14926
+ registerFlowStateCommit(service, async (input2) => {
14927
+ if (![input2.flowId, input2.revision].every((value) => typeof value === "string" && value.trim().length > 0 && value.length <= 200)) throw new TypeError("flow commit: flowId/revision \u2014 \u043D\u0435\u043F\u0443\u0441\u0442\u044B\u0435 \u0441\u0442\u0440\u043E\u043A\u0438 \u0434\u043E 200 \u0437\u043D\u0430\u043A\u043E\u0432");
14928
+ if (typeof input2.idempotencyKey !== "string" || !input2.idempotencyKey.trim() || input2.idempotencyKey.length > 200) {
14929
+ throw new TypeError("flow commit: idempotencyKey \u2014 \u043D\u0435\u043F\u0443\u0441\u0442\u0430\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0434\u043E 200 \u0437\u043D\u0430\u043A\u043E\u0432");
14930
+ }
14931
+ if (!input2.flow || typeof input2.flow !== "object" || Array.isArray(input2.flow)) throw new TypeError("flow commit: flow \u2014 JSON-\u043E\u0431\u044A\u0435\u043A\u0442");
14932
+ const chat = await storage.getChat(input2.chatId);
14933
+ if (!chat || chat.ownerId !== input2.ownerId || deleting.has(input2.chatId)) return { status: "not_found" };
14934
+ if (closing) return { status: "busy" };
14935
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify([input2.ownerId, input2.chatId, input2.flowId, input2.revision, input2.idempotencyKey])));
14936
+ const operationId = `flow-state:${Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("")}`;
14937
+ try {
14938
+ return await whileStarting(input2.chatId, async () => {
14939
+ const { branch } = await loadTree(input2.chatId);
14940
+ const before = foldAppState(branch);
14941
+ const writer = createAppStateWriter({ base: before, write: () => {
14942
+ }, logError, chatId: input2.chatId, serverFlowChecks: true });
14943
+ writer.set("flow", input2.flow);
14944
+ writer.close();
14945
+ const record = { set: { flow: writer.values().flow } };
14946
+ const earlier = await storage.getMessage(operationId);
14947
+ if (earlier) {
14948
+ if (JSON.stringify(earlier.parts) !== JSON.stringify([appStatePart(record)])) throw new TypeError("flow commit: idempotencyKey \u0443\u0436\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u043D \u0441 \u0434\u0440\u0443\u0433\u0438\u043C payload");
14949
+ return branch.at(-1)?.id === operationId ? { status: "same", atMessageId: operationId } : { status: "stale" };
14950
+ }
14951
+ const head = branch.at(-1)?.id ?? null;
14952
+ if (head !== input2.expectedAtMessageId) return { status: "stale" };
14953
+ const message = stateMessage({ chatId: input2.chatId, parentId: head, record });
14954
+ await storage.insertMessage({ ...message, id: operationId });
14955
+ await branchChanged(input2.ownerId, input2.chatId, "flowCommit", before);
14956
+ return { status: "applied", atMessageId: operationId };
14957
+ });
14958
+ } catch (error) {
14959
+ if (error instanceof ChatBusyError) return { status: "busy" };
14960
+ throw error;
14961
+ }
14962
+ });
14909
14963
  return service;
14910
14964
  function isDetachedAnswer(last, runningId) {
14911
14965
  return runningId === null && last?.status === "streaming" && !liveAnswerIds.has(last.id);
@@ -15135,6 +15189,7 @@ export {
15135
15189
  DEFAULT_AFTER_TURN_ATTEMPTS,
15136
15190
  DEFAULT_STATE_CHANGED_TIMEOUT_MS,
15137
15191
  DEFAULT_STATE_CHANGED_ATTEMPTS,
15192
+ flowStateCommitter,
15138
15193
  DEFAULT_REPLY_CHECKS_WEB,
15139
15194
  resolveReplyChecks,
15140
15195
  THOUGHT_TITLE_USAGE_SOURCE,
@@ -15182,4 +15237,4 @@ export {
15182
15237
  InvalidWidgetAnswerError,
15183
15238
  createChatService
15184
15239
  };
15185
- //# sourceMappingURL=chunk-6KHSUGHE.js.map
15240
+ //# sourceMappingURL=chunk-BQTUOC3T.js.map