@ykurilov/agent-chat-core 0.69.0 → 0.70.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,26 @@
1
1
  # @ykurilov/agent-chat-core
2
2
 
3
+ ## 0.70.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7e54665: Сетевые отказы (закрытый порт, DNS, сброс или таймаут соединения, Cannot connect to API) распознаются по коду и
8
+ вложенной причине как временный сбой. До начала ответа ход переходит к запасному, как при 5xx.
9
+ При доступном запасном по умолчанию один повтор через 250 мс. `providerFallback.retryDelaysMs` задаёт паузы и
10
+ число повторов; `[]` - переход без повтора. Без доступного запасного сохраняются три повтора с паузами 1/3/6 с.
11
+ Сетевые сбои и 5xx не ставят основной на пятиминутную паузу: следующий ход снова пробует его.
12
+ Начавшийся ответ не переводится на другого провайдера, сроки молчания и onSilence остаются прежними.
13
+
14
+ Как обновить приложение: обновить пакеты SDK одной версией; миграций нет. Быстрый режим включён автоматически.
15
+ Для немедленного перехода добавить `retryDelaysMs: []` в существующий `providerFallback`; для прежних повторов -
16
+ `retryDelaysMs: [1000, 3000, 6000]`. Настройка ограничивает паузы повторов, а не время соединения или ответа.
17
+
18
+ ## 0.69.1
19
+
20
+ ### Patch Changes
21
+
22
+ - 857777b: Ряд значков шагов в Тандеме — по настоящей ширине строки, «+N» всегда помещается, не вылезает за край узкой колонки; модели заголовков раздумий уходит не больше 1200 знаков раздумья (начало и конец) — длинный заголовок вдвое дешевле.
23
+
3
24
  ## 0.69.0
4
25
 
5
26
  ### Minor Changes
@@ -3513,10 +3513,47 @@ function looksTransient(text) {
3513
3513
  return RETRYABLE_SIGNS.test(text);
3514
3514
  }
3515
3515
 
3516
- // src/provider-errors/transient-retry-middleware.ts
3516
+ // src/provider-errors/network-failure.ts
3517
3517
  import { APICallError as APICallError2 } from "ai";
3518
+ var NETWORK_CODES = /* @__PURE__ */ new Set([
3519
+ "ECONNREFUSED",
3520
+ "ENOTFOUND",
3521
+ "EAI_AGAIN",
3522
+ "EHOSTUNREACH",
3523
+ "ENETUNREACH",
3524
+ "ETIMEDOUT",
3525
+ "ECONNRESET",
3526
+ "EPIPE",
3527
+ "ECONNABORTED",
3528
+ "UND_ERR_CONNECT_TIMEOUT",
3529
+ "UND_ERR_SOCKET",
3530
+ "UND_ERR_HEADERS_TIMEOUT",
3531
+ "UND_ERR_BODY_TIMEOUT",
3532
+ "ConnectionRefused",
3533
+ "FailedToOpenSocket",
3534
+ "ConnectionClosed"
3535
+ ]);
3536
+ function isNetworkFailure(error) {
3537
+ const pending = [underlyingError(error)];
3538
+ const seen = /* @__PURE__ */ new Set();
3539
+ while (pending.length > 0) {
3540
+ const current = pending.pop();
3541
+ if (typeof current !== "object" || current === null || seen.has(current)) continue;
3542
+ seen.add(current);
3543
+ if (errorStatusCode(current) !== void 0) continue;
3544
+ const fields = current;
3545
+ if (typeof fields.code === "string" && NETWORK_CODES.has(fields.code)) return true;
3546
+ if (APICallError2.isInstance(current) && current.isRetryable) return true;
3547
+ if (typeof fields.message === "string" && /^Cannot connect to API\b/i.test(fields.message)) return true;
3548
+ pending.push(fields.cause);
3549
+ if (Array.isArray(fields.errors)) pending.push(...fields.errors);
3550
+ }
3551
+ return false;
3552
+ }
3553
+
3554
+ // src/provider-errors/transient-retry-middleware.ts
3555
+ import { APICallError as APICallError3 } from "ai";
3518
3556
  var RETRY_DELAYS_MS = [1e3, 3e3, 6e3];
3519
- var RETRY_BUDGET_MS = RETRY_DELAYS_MS.reduce((sum, delay) => sum + delay, 0);
3520
3557
  var RETRY_JITTER = 0.25;
3521
3558
  var MAX_SILENCE_ATTEMPTS = 2;
3522
3559
  var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 409, 429]);
@@ -3547,7 +3584,8 @@ function isTransient(error) {
3547
3584
  const text = errorText(error);
3548
3585
  if (isNotRetryable(text, status) || isContextOverflow(text, status)) return false;
3549
3586
  if (status !== void 0) return RETRYABLE_STATUS_CODES.has(status) || status >= 500;
3550
- if (APICallError2.isInstance(error) && error.isRetryable) return true;
3587
+ if (APICallError3.isInstance(error) && error.isRetryable) return true;
3588
+ if (isNetworkFailure(error)) return true;
3551
3589
  return looksTransient(text);
3552
3590
  }
3553
3591
  function createTransientRetryMiddleware(context = {}) {
@@ -3557,6 +3595,7 @@ function createTransientRetryMiddleware(context = {}) {
3557
3595
  const silence = context.silence ?? DEFAULT_MODEL_SILENCE;
3558
3596
  const repeatSilence = context.repeatSilence ?? true;
3559
3597
  const silenceAttempts = () => (typeof repeatSilence === "function" ? repeatSilence() : repeatSilence) ? MAX_SILENCE_ATTEMPTS : 1;
3598
+ let answerStarted = false;
3560
3599
  const onSilence = (waited, timeoutMs) => context.logError?.(
3561
3600
  `[\u043C\u043E\u043B\u0447\u0430\u043D\u0438\u0435 \u043C\u043E\u0434\u0435\u043B\u0438] \u0447\u0430\u0442=${context.chatId ?? "\u043D\u0435\u0442"} \u043C\u043E\u0434\u0435\u043B\u044C=${context.modelId ?? "\u043D\u0435\u0442"} \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440=${context.providerId ?? "\u043D\u0435\u0442"} \u0436\u0434\u0430\u043B\u0438=${waited} \u0441\u0440\u043E\u043A=${timeoutMs}\u043C\u0441`
3562
3601
  );
@@ -3567,10 +3606,11 @@ function createTransientRetryMiddleware(context = {}) {
3567
3606
  try {
3568
3607
  return await attemptOnce();
3569
3608
  } catch (error) {
3570
- const delay = RETRY_DELAYS_MS[attempt];
3609
+ const delays = !answerStarted && !isTimeoutError(error) ? context.retryDelaysMs?.() ?? RETRY_DELAYS_MS : RETRY_DELAYS_MS;
3610
+ const delay = delays[attempt];
3571
3611
  if (isTimeoutError(error)) silences++;
3572
3612
  if (abortSignal?.aborted || delay === void 0 || !isTransient(error) || silences >= silenceAttempts()) throw error;
3573
- const remainingMs = RETRY_BUDGET_MS - waitedMs;
3613
+ const remainingMs = Math.max(0, delays.reduce((sum, value) => sum + value, 0) - waitedMs);
3574
3614
  const pauseMs = requestedPauseMs(error, remainingMs, now()) ?? Math.round(Math.min(delay * (1 + random() * RETRY_JITTER), remainingMs));
3575
3615
  context.logError?.(retryLine(context, attempt + 2, error, pauseMs));
3576
3616
  await pause2(pauseMs, abortSignal);
@@ -3580,7 +3620,11 @@ function createTransientRetryMiddleware(context = {}) {
3580
3620
  }
3581
3621
  return {
3582
3622
  specificationVersion: "v4",
3583
- wrapStream: ({ model, params }) => withRetries2(() => streamUntilFirstChunk(model, params, silence, onSilence), params.abortSignal),
3623
+ wrapStream: async ({ model, params }) => {
3624
+ const result = await withRetries2(() => streamUntilFirstChunk(model, params, silence, onSilence), params.abortSignal);
3625
+ answerStarted = true;
3626
+ return result;
3627
+ },
3584
3628
  wrapGenerate: ({ doGenerate, params }) => withRetries2(doGenerate, params.abortSignal)
3585
3629
  };
3586
3630
  }
@@ -3744,6 +3788,7 @@ function classifyProviderError(error, provider) {
3744
3788
  if (isProviderSilence(cause)) return "overloaded";
3745
3789
  if (status !== void 0) return status >= 500 ? "overloaded" : "other";
3746
3790
  if (RATE_LIMIT_SIGNS.test(text)) return "rate_limited";
3791
+ if (isNetworkFailure(cause)) return "overloaded";
3747
3792
  return looksTransient(text) ? "overloaded" : "other";
3748
3793
  }
3749
3794
  function isAccessRefusal(error) {
@@ -3755,7 +3800,7 @@ function isKeyLimit(error) {
3755
3800
  }
3756
3801
 
3757
3802
  // src/provider-errors/provider-failure-log.ts
3758
- import { APICallError as APICallError3 } from "ai";
3803
+ import { APICallError as APICallError4 } from "ai";
3759
3804
  var MAX_BODY_LENGTH = 4e3;
3760
3805
  function describeProviderFailure(error, context) {
3761
3806
  const cause = underlyingError(error);
@@ -3766,7 +3811,7 @@ function describeProviderFailure(error, context) {
3766
3811
  }
3767
3812
  function bodyOf(error) {
3768
3813
  const candidates = [];
3769
- if (APICallError3.isInstance(error)) candidates.push(error.responseBody);
3814
+ if (APICallError4.isInstance(error)) candidates.push(error.responseBody);
3770
3815
  if (typeof error === "object" && error !== null) {
3771
3816
  const fields = error;
3772
3817
  candidates.push(fields.message);
@@ -5031,7 +5076,8 @@ async function buildModelRequest(input) {
5031
5076
  providerId: provider.id,
5032
5077
  logError: input.logError,
5033
5078
  ...silenceOfProvider(provider, input.modelSilence),
5034
- ...input.repeatSilence && { repeatSilence: input.repeatSilence }
5079
+ ...input.repeatSilence && { repeatSilence: input.repeatSilence },
5080
+ ...input.retryDelaysMs && { retryDelaysMs: input.retryDelaysMs }
5035
5081
  });
5036
5082
  const tools = input.tools ? sortToolsByName(input.tools) : void 0;
5037
5083
  const untitled = withoutThoughtTitles(input.history);
@@ -5348,6 +5394,7 @@ function nextSilenceProvider(input) {
5348
5394
  return index === -1 ? input.fallbackProviderIds[0] : input.fallbackProviderIds[index + 1];
5349
5395
  }
5350
5396
  var PROVIDER_FALLBACK_BY_DEFAULT = {};
5397
+ var FALLBACK_RETRY_DELAYS_MS = [250];
5351
5398
  var TRANSIENT_KINDS = /* @__PURE__ */ new Set(["overloaded", "rate_limited"]);
5352
5399
  function fallbackProviders(input) {
5353
5400
  const setting = input.setting ?? PROVIDER_FALLBACK_BY_DEFAULT;
@@ -5397,7 +5444,10 @@ function checkProviderFallback(setting, catalog) {
5397
5444
  if (typeof setting !== "object" || setting === null || Array.isArray(setting)) {
5398
5445
  throw new TypeError("providerFallback: \u0436\u0434\u0451\u043C false \u0438\u043B\u0438 \u043E\u0431\u044A\u0435\u043A\u0442 { byModel }");
5399
5446
  }
5400
- const { byModel, onSilence, scope, maxSwitches } = setting;
5447
+ const { byModel, onSilence, scope, maxSwitches, retryDelaysMs } = setting;
5448
+ if (retryDelaysMs !== void 0 && (!Array.isArray(retryDelaysMs) || retryDelaysMs.some((delay) => typeof delay !== "number" || !Number.isFinite(delay) || delay < 0))) {
5449
+ throw new TypeError("providerFallback.retryDelaysMs: \u0436\u0434\u0451\u043C \u043C\u0430\u0441\u0441\u0438\u0432 \u043A\u043E\u043D\u0435\u0447\u043D\u044B\u0445 \u0447\u0438\u0441\u0435\u043B \u043E\u0442 \u043D\u0443\u043B\u044F (\u043C\u0441), [] \u2014 \u0431\u0435\u0437 \u043F\u043E\u0432\u0442\u043E\u0440\u043E\u0432");
5450
+ }
5401
5451
  if (onSilence !== void 0 && onSilence !== "continue" && onSilence !== "restart-step-on-next") {
5402
5452
  throw new TypeError(`providerFallback.onSilence: \u0436\u0434\u0451\u043C "continue" \u0438\u043B\u0438 "restart-step-on-next", \u043F\u0440\u0438\u0448\u043B\u043E ${JSON.stringify(onSilence)}`);
5403
5453
  }
@@ -5607,7 +5657,7 @@ ${summary}
5607
5657
  }
5608
5658
 
5609
5659
  // src/provider-errors/provider-error-details.ts
5610
- import { APICallError as APICallError4 } from "ai";
5660
+ import { APICallError as APICallError5 } from "ai";
5611
5661
  var MAX_REASON_LENGTH = 300;
5612
5662
  var MAX_RESET_AHEAD_MS = 2 * 366 * 24 * 60 * 60 * 1e3;
5613
5663
  function providerErrorDetails(error, context) {
@@ -5627,7 +5677,7 @@ function providerErrorDetails(error, context) {
5627
5677
  };
5628
5678
  }
5629
5679
  function providerMessageOf(error) {
5630
- if (APICallError4.isInstance(error)) {
5680
+ if (APICallError5.isInstance(error)) {
5631
5681
  const fromBody = typeof error.responseBody === "string" ? fromJson(error.responseBody) : void 0;
5632
5682
  if (fromBody?.message !== void 0) return fromBody;
5633
5683
  const raw = [error.responseBody, error.message].find(isText);
@@ -7188,6 +7238,7 @@ async function runTurn(turnInput, emitRaw) {
7188
7238
  mainLoopWaits = true;
7189
7239
  }
7190
7240
  let fallbackUsed = false;
7241
+ const remainingFallbacks = () => providerId === input.providerId ? fallbackProviderIds : fallbackProviderIds.slice(fallbackProviderIds.indexOf(providerId) + 1);
7191
7242
  let historyCompacted = false;
7192
7243
  let answerStepsCompacted = false;
7193
7244
  let autoContinues = 0;
@@ -7235,6 +7286,7 @@ async function runTurn(turnInput, emitRaw) {
7235
7286
  forgedBlocks,
7236
7287
  // Есть куда перейти при молчании — у того же провайдера оно не повторяется.
7237
7288
  repeatSilence: () => silenceTarget() === void 0,
7289
+ retryDelaysMs: () => !contentSeen && !fallbackUsed && remainingFallbacks().length > 0 && stepsDone < maxSteps ? (input.providerFallback || {}).retryDelaysMs ?? FALLBACK_RETRY_DELAYS_MS : void 0,
7238
7290
  ...silenceSwitch.scope === "step" && switchedBySilence && {
7239
7291
  backToPrimary: () => {
7240
7292
  backToPrimary = true;
@@ -7289,7 +7341,7 @@ async function runTurn(turnInput, emitRaw) {
7289
7341
  const nextState = () => ({
7290
7342
  fallbackUsed,
7291
7343
  // С запасного — только провайдеры после него: перегрузка у запасного не ведёт обратно к нему же.
7292
- fallbackProviderIds: providerId === input.providerId ? fallbackProviderIds : fallbackProviderIds.slice(fallbackProviderIds.indexOf(providerId) + 1),
7344
+ fallbackProviderIds: remainingFallbacks(),
7293
7345
  overflowCompaction: overflowCompaction(),
7294
7346
  autoContinue: { done: autoContinues, approvalRequested: pendingHumanInput(parts2, input.waitingWidgets).length > 0 },
7295
7347
  silenceSwitch: { done: silenceSwitches, max: silenceSwitch.maxSwitches, nextProviderId: silenceTarget() }
@@ -7762,6 +7814,7 @@ async function openAttempt(input, attempt, attemptSignal) {
7762
7814
  ...attempt.tailMessages && { tailMessages: attempt.tailMessages },
7763
7815
  logError,
7764
7816
  repeatSilence: attempt.repeatSilence,
7817
+ retryDelaysMs: attempt.retryDelaysMs,
7765
7818
  ...turnObserver && { observe: { observer: turnObserver, ownerId: input.ownerId ?? "", messageId: input.messageId, purpose: "answer" } },
7766
7819
  ...tools && { tools: approval.tools },
7767
7820
  // Каждый запрос, на который провайдер начал отвечать, — в расход хода с номером генерации (FR-051, FR-058).
@@ -9936,7 +9989,7 @@ function resolveReplyChecks(setting) {
9936
9989
  import { generateText as generateText3 } from "ai";
9937
9990
  var THOUGHT_TITLE_USAGE_SOURCE = "thought_title";
9938
9991
  var MAX_TITLE_TOKENS = 40;
9939
- var MAX_THOUGHT_CHARS = 4e3;
9992
+ var MAX_THOUGHT_CHARS = 1200;
9940
9993
  var THOUGHT_TITLE_INSTRUCTIONS = [
9941
9994
  "You write a short title for a fragment of an AI assistant's private reasoning, shown to the user as a step label.",
9942
9995
  "Rules:",
@@ -9973,11 +10026,21 @@ function languageName(language) {
9973
10026
  const code = language.toLowerCase().split(/[-_]/)[0] ?? language;
9974
10027
  return LANGUAGE_NAMES[code] ?? language;
9975
10028
  }
10029
+ function headOf(text, max) {
10030
+ const head = text.slice(0, max);
10031
+ const cut = head.search(/\s\S*$/);
10032
+ return (cut > 0 ? head.slice(0, cut) : head).trimEnd();
10033
+ }
10034
+ function tailOf(text, max) {
10035
+ const tail = text.slice(-max);
10036
+ const cut = tail.search(/\s/);
10037
+ return (cut >= 0 ? tail.slice(cut) : tail).trimStart();
10038
+ }
9976
10039
  function thoughtTitlePrompt(text, language) {
9977
10040
  const clean = text.replace(/<\/?thought>/gi, "");
9978
- const shown = clean.length > MAX_THOUGHT_CHARS ? `${clean.slice(0, MAX_THOUGHT_CHARS / 2)}
10041
+ const shown = clean.length > MAX_THOUGHT_CHARS ? `${headOf(clean, MAX_THOUGHT_CHARS / 2)}
9979
10042
  \u2026
9980
- ${clean.slice(-MAX_THOUGHT_CHARS / 2)}` : clean;
10043
+ ${tailOf(clean, MAX_THOUGHT_CHARS / 2)}` : clean;
9981
10044
  return `<thought>
9982
10045
  ${shown}
9983
10046
  </thought>
@@ -15065,4 +15128,4 @@ export {
15065
15128
  InvalidWidgetAnswerError,
15066
15129
  createChatService
15067
15130
  };
15068
- //# sourceMappingURL=chunk-BJTHXNB6.js.map
15131
+ //# sourceMappingURL=chunk-EW3572B5.js.map