Claude के tool-calling loops Node.js में उलझे हुए (tangled) promise code बनाने के लिए जाने जाते हैं। Node.js 22 का नया Promise.withResolvers() डेवलपर्स को boilerplate-heavy new Promise पैटर्न को एक सिंगल लाइन से बदलने की सुविधा देता है, जो promise और उसके resolve/reject फंक्शन्स को एक साथ प्रदान करता है। इसका परिणाम है—कम भूले हुए resolve कॉल्स, कोई double-reject चेतावनियाँ नहीं, और एक flatter control flow जिसे टेस्ट करना और serverless environments में चालू रखना आसान है।
पुराना पैटर्न फ्लो को क्यों बिगाड़ता है
जब Claude जैसा LLM किसी tool के बारे में पूछता है, तो typical Node implementation कुछ इस तरह दिखता है:
return new Promise((resolve, reject) => {
// launch the tool, attach callbacks, maybe fire another async call
});
तीन बार-बार होने वाली समस्याएँ (pitfalls) सामने आती हैं:
- Forgotten resolve – यदि code path कभी
resolveको कॉल नहीं करता है, तो Lambda या अन्य serverless handler तब तक लटका (hang) रहता है जब तक कि वह timeout न हो जाए, जिससे लागत (cost) बढ़ जाती है। - Double reject – एक error path जो
rejectको दो बार कॉल करता है, वह “unhandled rejection” चेतावनियाँ ट्रिगर करता है जो strict mode में process को क्रैश कर सकती हैं। - Deep nesting – प्रत्येक async स्टेप constructor के अंदर एक और callback को नेस्ट करता है, जिससे logic बिखर जाता है और unit tests कमज़ोर (brittle) हो जाते हैं।
ये सभी समस्याएँ इस तथ्य से उत्पन्न होती हैं कि promise के control functions constructor के closure के अंदर लॉक होते हैं, जिससे बाकी कोड को वापस उसमें पहुँचने के लिए मजबूर होना पड़ता है।
एक लाइन में Promise.withResolvers()
Node 22 एक static helper जोड़ता है जो एक object लौटाता है जिसमें एक promise और उसे settle करने वाले दो functions होते हैं:
const { promise, resolve, reject } = Promise.withResolvers();
अब promise को सिस्टम के किसी भी हिस्से—एक HTTP handler, एक database listener, या एक background worker—को सौंपा जा सकता है, जबकि original caller बस promise का await करता है। पूरे tool-execution block को new Promise constructor में लपेटने (wrap करने) की आवश्यकता नहीं है।
इसे Claude के tool loop पर लागू करना
Claude का workflow इस प्रकार है:
- LLM एक tool request जारी करता है।
- आपका code tool को चलाता है (जैसे, एक API call, एक file read)।
- tool का result अगले turn के लिए Claude को वापस भेज दिया जाता है।
withResolvers के साथ, loop संक्षिप्त होकर ऐसा हो जाता है:
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
}
}
tool implementation को अब नए promise में wrap करने की आवश्यकता नहीं है; इसे बस resolve और reject प्राप्त होते हैं। यह ऊपर सूचीबद्ध तीन failure modes को समाप्त कर देता है।
Production knobs जो अभी भी मायने रखते हैं
एक साफ़ promise shape के बावजूद, real-world agents अन्य बाधाओं (constraints) का सामना करते हैं:
- Timeouts – ऊपर दिया गया snippet एक सरल timer दिखाता है जो reject कर देता है यदि tool एक threshold से अधिक समय लेता है। SLA expectations के आधार पर अवधि (duration) को एडजस्ट करें।
- Throttling – जब underlying service एक throttling error (जैसे, Bedrock का
ThrottlingException) लौटाती है, तो उसे catch करें, pause करें, और exponential back-off के साथ retry करें। resolve/reject pair वही रहता है; केवल retry logic बदलता है। - Lambda cost – AWS Lambda में,
callbackWaitsForEmptyEventLoop = falseसेट करें। यह runtime को बताता है कि जैसे ही handler return करे, function को समाप्त कर दिया जाए, भले ही streams या अन्य background handles अभी भी खुले हों। यह function को तब तक लटकने (lingering) से रोकता है जब तक कि promise कहीं और settle न हो जाए।
जब नया helper कोई रामबाण (silver bullet) नहीं है
Promise.withResolvers() केवल Node 22 और उसके बाद के वर्ज़न में उपलब्ध है। पुराने LTS releases पर आधारित projects को या तो इस pattern को polyfill करना होगा या classic constructor के साथ ही रहना होगा। Polyfills API की नकल कर सकते हैं लेकिन उन्हें native performance benefits नहीं मिलेंगे। इसके अलावा, helper जादुई रूप से logical bugs को हल नहीं करता है: डेवलपर्स को अभी भी यह सुनिश्चित करने की आवश्यकता है कि प्रत्येक request के लिए resolve या reject में से ठीक एक को ही कॉल किया जाए, अन्यथा promise अनिश्चित काल (indefinitely) के लिए pending रहेगा।
आगे क्या देखें
- Framework adoption – जो libraries LLM agent loops को abstract करती हैं (जैसे, open-source Claude wrappers), वे
withResolversको एक opt-in feature के रूप में पेश करना शुरू कर रही हैं। उन updates पर नज़र रखें जो इस pattern को default बनाते हैं। - Node ecosystem – जैसे-जैसे अधिक services Node 22 पर जाएँगी, यह helper केवल LLM agents के लिए ही नहीं, बल्कि किसी भी “fire-and-wait” async pattern के लिए एक de-facto standard बन जाएगा।
- Tool-calling standards – LLM tool calls के लिए उभरते हुए specifications एक “single-promise” contract का सुझाव दे सकते हैं, जो
withResolversapproach के साथ पूरी तरह मेल खाता है।
Takeaway: verbose new Promise wrapper को एक-लाइनर Promise.withResolvers() से बदलकर, Claude-आधारित agents को अधिक स्पष्ट flow, कम runtime surprises, और serverless costs पर बेहतर नियंत्रण मिलता है—बशर्ते कि runtime Node 22 को सपोर्ट करता हो।
