Циклы вызова инструментов Claude известны тем, что порождают запутанный код с промисами в Node.js.
Новый метод Promise.withResolvers() в Node.js 22 позволяет разработчикам заменить громоздкий паттерн new Promise одной строкой, которая сразу возвращает промис и функции resolve/reject. В результате становится меньше забытых вызовов resolve, исчезают предупреждения о двойном reject, а поток управления становится более плоским, что упрощает тестирование и поддержание работы в serverless-средах.

Почему старый паттерн нарушает логику выполнения

Когда LLM, такая как Claude, запрашивает инструмент, типичная реализация на Node выглядит так:

return new Promise((resolve, reject) => {
  // launch the tool, attach callbacks, maybe fire another async call
});

Возникают три повторяющиеся проблемы:

  • Забытый resolve — если путь выполнения кода никогда не вызывает resolve, Lambda или другой serverless-обработчик зависает до таймаута, что увеличивает расходы.
  • Двойной reject — путь обработки ошибок, вызывающий reject дважды, провоцирует предупреждения «unhandled rejection», которые в строгом режиме (strict mode) могут привести к падению процесса.
  • Глубокая вложенность — каждый асинхронный шаг вкладывает очередной callback внутрь конструктора, разбрасывая логику и делая юнит-тесты хрупкими.

Все эти проблемы проистекают из того, что функции управления промисом заблокированы внутри замыкания конструктора, что вынуждает остальной код «дотягиваться» до них.

Promise.withResolvers() в одну строку

Node 22 добавляет статический помощник, который возвращает объект, содержащий промис и две функции для его завершения:

const { promise, resolve, reject } = Promise.withResolvers();

Теперь промис можно передать любой части системы — HTTP-обработчику, слушателю базы данных или фоновому воркеру, — в то время как исходный вызывающий код просто делает await промиса. Нет необходимости оборачивать весь блок выполнения инструмента в конструктор new Promise.

Применение к циклу инструментов Claude

Рабочий процесс Claude выглядит так:

  1. LLM отправляет запрос на использование инструмента.
  2. Ваш код выполняет инструмент (например, вызов API или чтение файла).
  3. Результат инструмента отправляется обратно Claude для следующего шага.

С withResolvers цикл упрощается до:

async function runTool(request) {
  const { promise, resolve, reject } = Promise.withResolvers();

  // Kick off the tool; it can call resolve/reject from anywhere
  executeTool(request, { resolve, reject });

  // Optional timeout wrapper
  const timeout = setTimeout(() => reject(new Error('Tool timed out')), 10_000);
  try {
    const result = await promise;
    clearTimeout(timeout);
    return result;               // feed back to Claude
  } finally {
    // clean-up if needed
  }
}

Реализацию инструмента больше не нужно оборачивать в новый промис; она просто получает resolve и reject. Это устраняет три вышеупомянутых сценария сбоев.

Параметры для продакшена, которые все еще важны

Даже при более чистой структуре промиса, реальные агенты сталкиваются с другими ограничениями:

  • Таймауты — в приведенном выше фрагменте кода показан простой таймер, который вызывает reject, если выполнение инструмента превышает порог. Настраивайте длительность в соответствии с ожиданиями SLA.
  • Троттлинг (Throttling) — если базовый сервис возвращает ошибку ограничения частоты запросов (например, ThrottlingException в Bedrock), перехватите её, сделайте паузу и повторите попытку с экспоненциальной задержкой (exponential back-off). Пара resolve/reject остается прежним; меняется только логика повторных попыток.
  • Стоимость Lambda — в AWS Lambda установите callbackWaitsForEmptyEventLoop = false. Это дает команду среде выполнения завершить функцию сразу после возврата обработчика, даже если потоки (streams) или другие фоновые дескрипторы все еще открыты. Это предотвращает «зависание» функции, пока промис завершается в другом месте.

Когда новый помощник не является панацеей

Promise.withResolvers() доступен только в Node 22 и выше. Проекты, привязанные к более старым LTS-релизам, должны либо использовать полифил для этого паттерна, либо придерживаться классического конструктора. Полифилы могут имитировать API, но не обеспечат преимуществ нативной производительности. Более того, этот помощник не решает логические ошибки магическим образом: разработчикам по-прежнему нужно гарантировать, что для каждого запроса вызывается ровно одна из функций — resolve или reject, иначе промис останется в состоянии pending бесконечно.

На что обратить внимание дальше

  • Внедрение во фреймворки — Библиотеки, абстрагирующие циклы LLM-агентов (например, open-source обертки для Claude), начинают предлагать withResolvers как опциональную функцию. Следите за обновлениями, которые сделают этот паттерн стандартом по умолчанию.
  • Экосистема Node — По мере перехода большего числа сервисов на Node 22, этот помощник станет стандартом де-факто для любого асинхронного паттерна типа «запустил и жди» (fire-and-wait), а не только для LLM-агентов.
  • Стандарты вызова инструментов — Разрабатываемые спецификации для вызовов инструментов LLM могут предписывать контракт «одного промиса», что идеально согласуется с подходом withResolvers.

Итог: Заменяя многословную обертку new Promise на однострочный Promise.withResolvers(), агенты на базе Claude получают более прозрачный поток выполнения, меньше сюрпризов во время выполнения и более строгий контроль над расходами на serverless — при условии, что среда выполнения поддерживает Node 22.