@ykurilov/agent-chat-flows 0.63.2 → 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,29 @@
1
1
  # @ykurilov/agent-chat-flows
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
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [814ee7f]
12
+ - @ykurilov/agent-chat-core@0.65.0
13
+ - @ykurilov/agent-chat-react@0.65.0
14
+
15
+ ## 0.64.0
16
+
17
+ ### Minor Changes
18
+
19
+ - 1ea05c0: Тестовый чат: кнопка «К следующему напоминанию» проматывает часы ровно к моменту, когда сценарий сам напишет первым (касание, утреннее, выход по сроку, «срок вышел», «не завершил»; ночь и потолки учтены), и поле «своё время» («15 мин», «1 ч 30 мин», «2 дня»). Сервер: `flow.advanceTestClock({ ownerId, chatId, to: "next_first_message" })` → `{ timeOffset, reason }`, впереди ничего — `FlowTestClockError` с `code: "nothing_scheduled"`. Браузер: `FlowTestChatConnection.advanceToNextFirstMessage`. Разбор длительности — `parseDuration` из `/format`. Скин: необязательные `onNextFirstMessage`, `nothingScheduled`, `onAdvanceText` и пять надписей. Как подключить — CHANGES.md, раздел 0.64.0.
20
+
21
+ ### Patch Changes
22
+
23
+ - Updated dependencies [99cded9]
24
+ - @ykurilov/agent-chat-core@0.64.0
25
+ - @ykurilov/agent-chat-react@0.64.0
26
+
3
27
  ## 0.63.2
4
28
 
5
29
  ### Patch Changes
@@ -276,6 +276,30 @@ function withoutEmpty(object, keep = []) {
276
276
  );
277
277
  }
278
278
 
279
+ // src/format/parse-duration.ts
280
+ var MINUTE_MS = 6e4;
281
+ var HOUR_MS = 60 * MINUTE_MS;
282
+ var DAY_MS = 24 * HOUR_MS;
283
+ var UNIT_MS = Object.fromEntries([
284
+ ...["\u043C", "\u043C\u0438\u043D", "\u043C\u0438\u043D\u0443\u0442\u0430", "\u043C\u0438\u043D\u0443\u0442\u044B", "\u043C\u0438\u043D\u0443\u0442", "\u043C\u0438\u043D\u0443\u0442\u0443", "m", "min", "mins", "minute", "minutes"].map((word) => [word, MINUTE_MS]),
285
+ ...["\u0447", "\u0447\u0430\u0441", "\u0447\u0430\u0441\u0430", "\u0447\u0430\u0441\u043E\u0432", "h", "hr", "hrs", "hour", "hours"].map((word) => [word, HOUR_MS]),
286
+ ...["\u0434", "\u0434\u043D", "\u0434\u0435\u043D\u044C", "\u0434\u043D\u044F", "\u0434\u043D\u0435\u0439", "d", "day", "days"].map((word) => [word, DAY_MS])
287
+ ]);
288
+ var PART = /\s*(\d+(?:[.,]\d+)?)\s*([a-zа-яё]+)\.?\s*(?:(?:,|и(?![a-zа-яё])|and(?![a-zа-яё]))\s*)?/uy;
289
+ function parseDuration(text) {
290
+ const input = text.trim().toLowerCase();
291
+ if (!input) return void 0;
292
+ PART.lastIndex = 0;
293
+ let total = 0;
294
+ while (PART.lastIndex < input.length) {
295
+ const match = PART.exec(input);
296
+ const unit = match ? UNIT_MS[match[2]] : void 0;
297
+ if (!match || unit === void 0) return void 0;
298
+ total += Number(match[1].replace(",", ".")) * unit;
299
+ }
300
+ return total > 0 ? Math.round(total) : void 0;
301
+ }
302
+
279
303
  // src/engine/flow-run.ts
280
304
  var FLOW_STATE_KEY = "flow";
281
305
  var FLOW_PATH_LIMIT = 50;
@@ -454,7 +478,7 @@ function localDay(at, timeZone) {
454
478
 
455
479
  // src/engine/flow-days.ts
456
480
  var DAY_MINUTES = 24 * 60;
457
- var DAY_MS = 864e5;
481
+ var DAY_MS2 = 864e5;
458
482
  var dayStartMinutes = (schedule2) => parseClock(schedule2.dayStart) ?? 0;
459
483
  var nightMinutes = (schedule2) => parseClock(schedule2.nightFrom ?? DEFAULT_FIRST_MESSAGES.quietHours.from) ?? 0;
460
484
  var dateKey = ({ year, month, day }) => `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
@@ -465,7 +489,7 @@ var dateOf = (key) => {
465
489
  var shiftDate = (key, days) => dateKey(nextDate(dateOf(key), days));
466
490
  var daysBetween = (from, to) => {
467
491
  const [a, b] = [dateOf(from), dateOf(to)];
468
- return Math.round((Date.UTC(b.year, b.month - 1, b.day) - Date.UTC(a.year, a.month - 1, a.day)) / DAY_MS);
492
+ return Math.round((Date.UTC(b.year, b.month - 1, b.day) - Date.UTC(a.year, a.month - 1, a.day)) / DAY_MS2);
469
493
  };
470
494
  var later = (a, b) => a > b ? a : b;
471
495
  var eveningNight = (schedule2) => nightMinutes(schedule2) > dayStartMinutes(schedule2);
@@ -559,8 +583,8 @@ function nudgeRings(flow, run, timeZone) {
559
583
  const entryDay = flowDay(run, run.enteredAt, timeZone, schedule2).day;
560
584
  return { ring: own, until: programDayDue(run, entryDay + own.days, timeZone, schedule2), after: common };
561
585
  }
562
- var UNIT_MS = { min: 6e4, h: 36e5, d: 864e5 };
563
- var durationMs = (duration2) => duration2.amount * UNIT_MS[duration2.unit];
586
+ var UNIT_MS2 = { min: 6e4, h: 36e5, d: 864e5 };
587
+ var durationMs = (duration2) => duration2.amount * UNIT_MS2[duration2.unit];
564
588
  var DEFAULT_SKIP_MORNING_MIN = 180;
565
589
  function nextNudge(flow, run, now, timeZone, context = {}) {
566
590
  const rings = nudgesRunning(flow, run, timeZone, context);
@@ -632,7 +656,7 @@ function nextNightReply(flow, run, timeZone, context = {}) {
632
656
 
633
657
  // src/engine/timers.ts
634
658
  var FLOW_DEADLINE_EXIT = "@deadline";
635
- var UNIT_MS2 = { min: 6e4, h: 36e5, d: 864e5 };
659
+ var UNIT_MS3 = { min: 6e4, h: 36e5, d: 864e5 };
636
660
  function actualEntry(run) {
637
661
  const last = run.path.at(-1);
638
662
  return last && last.state === run.state ? last.at : run.enteredAt;
@@ -640,13 +664,13 @@ function actualEntry(run) {
640
664
  function timerDueAt(condition, run, timeZone, schedule2) {
641
665
  switch (condition.from) {
642
666
  case "in_state":
643
- return run.enteredAt + condition.amount * UNIT_MS2[condition.unit];
667
+ return run.enteredAt + condition.amount * UNIT_MS3[condition.unit];
644
668
  case "since_start":
645
- return run.startedAt + condition.amount * UNIT_MS2[condition.unit];
669
+ return run.startedAt + condition.amount * UNIT_MS3[condition.unit];
646
670
  case "silence":
647
671
  if (run.lastAgentAt === null) return null;
648
672
  if (run.lastHumanAt !== null && run.lastHumanAt > run.lastAgentAt) return null;
649
- return run.lastAgentAt + condition.amount * UNIT_MS2[condition.unit];
673
+ return run.lastAgentAt + condition.amount * UNIT_MS3[condition.unit];
650
674
  case "time_of_day": {
651
675
  const minutes = parseClock(condition.at);
652
676
  return minutes === null ? null : nextClockTime(actualEntry(run), minutes, timeZone);
@@ -921,6 +945,38 @@ function withNudgeStep(run, step, own) {
921
945
  return { ...withoutOwnStep(run), nudgeStep: step, ...own && { nudgeStepOwn: true } };
922
946
  }
923
947
 
948
+ // src/engine/first-messages.ts
949
+ function resolveFirstMessages(setting) {
950
+ if (setting === false) return null;
951
+ const rules = {
952
+ quietHours: { ...DEFAULT_FIRST_MESSAGES.quietHours, ...setting?.quietHours },
953
+ maxPerDay: setting?.maxPerDay ?? DEFAULT_FIRST_MESSAGES.maxPerDay,
954
+ ...setting?.canWriteNow ? { canWriteNow: setting.canWriteNow } : {}
955
+ };
956
+ for (const clock2 of [rules.quietHours.from, rules.quietHours.to]) {
957
+ if (parseClock(clock2) === null) throw new Error(`\u0422\u0438\u0445\u0438\u0435 \u0447\u0430\u0441\u044B \u043F\u0438\u0448\u0443\u0442\u0441\u044F \u043A\u0430\u043A \xAB\u0427\u0427:\u041C\u041C\xBB, \u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 \xAB21:00\xBB, \u0430 \u043D\u0435 \xAB${clock2}\xBB.`);
958
+ }
959
+ return rules;
960
+ }
961
+ function isQuiet(quietHours, at, timeZone) {
962
+ const from = parseClock(quietHours.from);
963
+ const to = parseClock(quietHours.to);
964
+ const now = clockMinutes(at, timeZone);
965
+ if (from === to) return false;
966
+ return from < to ? now >= from && now < to : now >= from || now < to;
967
+ }
968
+ function firstAwake(quietHours, at, timeZone) {
969
+ return isQuiet(quietHours, at, timeZone) ? nextClockTime(at, parseClock(quietHours.to), timeZone) : at;
970
+ }
971
+ function firstMessageSlot(rules, input) {
972
+ const { at, timeZone } = input;
973
+ if (input.sentToday >= rules.maxPerDay) {
974
+ return { allowed: false, retryAt: firstAwake(rules.quietHours, startOfNextDay(at, timeZone), timeZone) };
975
+ }
976
+ const awake = firstAwake(rules.quietHours, at, timeZone);
977
+ return awake === at ? { allowed: true } : { allowed: false, retryAt: awake };
978
+ }
979
+
924
980
  // src/engine/side-questions.ts
925
981
  function isFieldStale(field, updatedAt, now) {
926
982
  if (field.refreshAfterMonths === null) return false;
@@ -950,6 +1006,54 @@ function sideQuestion(flow, run, input) {
950
1006
  return unknown ? { fieldId: unknown.id, reason: "unknown" } : null;
951
1007
  }
952
1008
 
1009
+ // src/engine/migrate-run.ts
1010
+ function migrateRun(run, next, map, now) {
1011
+ const exists = (id2) => stateById(next, id2) !== void 0;
1012
+ const renamed = (id2) => !exists(id2) && map[id2] !== void 0 && exists(map[id2]) ? map[id2] : id2;
1013
+ const noteIds = new Set(next.chatNotes.map((field) => field.id));
1014
+ const notes = Object.fromEntries(Object.entries(run.notes).filter(([id2]) => noteIds.has(id2)));
1015
+ const path = run.path.map((entry) => ({ ...entry, state: renamed(entry.state), from: entry.from === null ? null : renamed(entry.from) }));
1016
+ const pausedClocks = Object.fromEntries(
1017
+ Object.entries(run.pausedClocks).map(([id2, ms]) => [renamed(id2), ms]).filter(([id2]) => exists(id2))
1018
+ );
1019
+ if (exists(run.state)) return { ...run, path, pausedClocks, notes };
1020
+ const targetId = map[run.state];
1021
+ if (targetId === void 0) {
1022
+ throw new Error(`\u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F \xAB${run.state}\xBB \u043D\u0435\u0442 \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u044F \xAB${next.id}\xBB, \u0430 \u043A\u0443\u0434\u0430 \u043F\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u0447\u0430\u0442 \u2014 \u043D\u0435 \u0432\u044B\u0431\u0440\u0430\u043D\u043E.`);
1023
+ }
1024
+ const target = stateById(next, targetId);
1025
+ if (!target) throw new Error(`\u0421\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u044F \xAB${targetId}\xBB \u043D\u0435\u0442 \u0432 \u043D\u043E\u0432\u043E\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u0441\u0446\u0435\u043D\u0430\u0440\u0438\u044F \xAB${next.id}\xBB: \u0442\u0443\u0434\u0430 \u043D\u0435 \u043F\u0435\u0440\u0435\u0432\u0435\u0441\u0442\u0438 \u0447\u0430\u0442 \u0438\u0437 \xAB${run.state}\xBB.`);
1026
+ return {
1027
+ ...run,
1028
+ state: target.id,
1029
+ enteredAt: now,
1030
+ pausedClocks: {},
1031
+ notes,
1032
+ path: appendPath(path, { state: target.id, at: now, tags: target.tags, from: run.state, exitId: null, decidedBy: "migrate" }),
1033
+ lastRejected: null
1034
+ };
1035
+ }
1036
+
1037
+ // src/engine/day-frame.ts
1038
+ var DEFAULT_HARD_FRAME_TEXT = "\u041D\u0430 \u0441\u0435\u0433\u043E\u0434\u043D\u044F \u0432\u0441\u0451 \u2014 \u0443\u0442\u0440\u043E\u043C \u043E\u0431\u044F\u0437\u0430\u0442\u0435\u043B\u044C\u043D\u043E \u043E\u0442\u0432\u0435\u0447\u0443!";
1039
+ function frameState(humanMessagesToday2, frame) {
1040
+ if (!frame) return "normal";
1041
+ const { softFrom, hardAt } = frame;
1042
+ if (hardAt !== void 0 && humanMessagesToday2 > hardAt) return "closed";
1043
+ if (hardAt !== void 0 && humanMessagesToday2 === hardAt) return "last";
1044
+ if (softFrom !== void 0 && humanMessagesToday2 >= softFrom) return "soft";
1045
+ return "normal";
1046
+ }
1047
+ function hardFrameText(frame) {
1048
+ return frame?.hardText?.trim() || DEFAULT_HARD_FRAME_TEXT;
1049
+ }
1050
+ function humanMessagesToday(run, at, timeZone, schedule2) {
1051
+ return run.dayCounts?.[dayInProgress(at, timeZone, schedule2)] ?? 0;
1052
+ }
1053
+ function dayClosedToday(dayClosedOn, at, timeZone, schedule2) {
1054
+ return dayClosedOn !== null && dayClosedOn === dayInProgress(at, timeZone, schedule2);
1055
+ }
1056
+
953
1057
  // src/check/app-checks.ts
954
1058
  function pick(items, key) {
955
1059
  if (!items || key === void 0) return void 0;
@@ -1223,6 +1327,7 @@ export {
1223
1327
  catalogEventNames,
1224
1328
  catalogEventLabel,
1225
1329
  compactFlow,
1330
+ parseDuration,
1226
1331
  FLOW_STATE_KEY,
1227
1332
  FLOW_PATH_LIMIT,
1228
1333
  FLOW_CHAIN_LIMIT,
@@ -1238,10 +1343,6 @@ export {
1238
1343
  fieldValueFits,
1239
1344
  dataConditionHolds,
1240
1345
  returnTarget,
1241
- parseClock,
1242
- clockMinutes,
1243
- nextClockTime,
1244
- startOfNextDay,
1245
1346
  localDay,
1246
1347
  dayOf,
1247
1348
  dayInProgress,
@@ -1271,7 +1372,6 @@ export {
1271
1372
  describeCondition,
1272
1373
  nextTransition,
1273
1374
  startRun,
1274
- appendPath,
1275
1375
  applyTransition,
1276
1376
  noteHumanMessage,
1277
1377
  noteAgentAnswer,
@@ -1280,10 +1380,17 @@ export {
1280
1380
  claimAllowed,
1281
1381
  noteSideQuestion,
1282
1382
  withNudgeStep,
1383
+ resolveFirstMessages,
1384
+ firstMessageSlot,
1283
1385
  isFieldStale,
1284
1386
  sideQuestion,
1387
+ migrateRun,
1388
+ frameState,
1389
+ hardFrameText,
1390
+ humanMessagesToday,
1391
+ dayClosedToday,
1285
1392
  placeOfPath,
1286
1393
  appIssues,
1287
1394
  checkFlow
1288
1395
  };
1289
- //# sourceMappingURL=chunk-LT4QELAZ.js.map
1396
+ //# sourceMappingURL=chunk-7VR4YNTJ.js.map