Node.js-এ Claude-এর tool-calling লুপগুলো জটিল promise কোড তৈরি করার জন্য পরিচিত।
Node.js 22-এর নতুন Promise.withResolvers() ডেভেলপারদের boilerplate-heavy new Promise প্যাটার্নের পরিবর্তে একটি মাত্র লাইনের মাধ্যমে promise এবং এর resolve/reject ফাংশনগুলো ব্যবহার করার সুযোগ দেয়। এর ফলে resolve কল করতে ভুলে যাওয়ার সম্ভাবনা কমে যায়, কোনো double-reject warning আসে না এবং একটি flatter control flow পাওয়া যায় যা serverless এনভায়রনমেন্টে টেস্ট করা এবং সচল রাখা সহজ।
কেন পুরনো প্যাটার্ন flow নষ্ট করে
যখন Claude-এর মতো কোনো LLM একটি tool-এর কাছে অনুরোধ করে, তখন সাধারণ Node ইমপ্লিমেন্টেশনটি দেখতে অনেকটা এরকম হয়:
return new Promise((resolve, reject) => {
// launch the tool, attach callbacks, maybe fire another async call
});
এখানে তিনটি সাধারণ সমস্যা দেখা দেয়:
- Forgotten resolve – যদি কোড পাথটি কখনো
resolveকল না করে, তবে একটি Lambda বা অন্যান্য serverless handler টাইম-আউট না হওয়া পর্যন্ত আটকে থাকে, যা খরচ বাড়িয়ে দেয়। - Double reject – যদি কোনো error path দুবার
rejectকল করে, তবে এটি “unhandled rejection” warning তৈরি করে যা strict mode-এ প্রসেসটিকে ক্র্যাশ করাতে পারে। - Deep nesting – প্রতিটি async ধাপ constructor-এর ভেতরে আরেকটি callback যুক্ত করে, যা লজিককে ছড়িয়ে দেয় এবং unit test করা কঠিন করে তোলে।
এই সমস্যাগুলোর মূল কারণ হলো promise-এর control ফাংশনগুলো constructor-এর closure-এর ভেতরে আটকে থাকে, যার ফলে বাকি কোডকে পুনরায় সেটির কাছে ফিরে আসতে হয়।
এক লাইনে Promise.withResolvers()
Node 22 একটি static helper যুক্ত করেছে যা একটি object রিটার্ন করে, যার মধ্যে একটি promise এবং এটি settle করার জন্য প্রয়োজনীয় দুটি ফাংশন থাকে:
const { promise, resolve, reject } = Promise.withResolvers();
এখন promise-টি সিস্টেমের যেকোনো অংশে—যেমন একটি HTTP handler, database listener, বা background worker-এ পাঠিয়ে দেওয়া সম্ভব, আর মূল caller শুধু promise-টি await করবে। পুরো tool-execution ব্লকটিকে new Promise constructor-এর ভেতরে রাখার আর প্রয়োজন নেই।
Claude-এর tool loop-এ এর প্রয়োগ
Claude-এর workflow হলো:
- LLM একটি tool request পাঠায়।
- আপনার কোডটি tool-টি চালায় (যেমন, একটি API call বা file read)।
- tool-এর ফলাফলটি পরবর্তী ধাপের জন্য 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
}
}
tool implementation-কে আর নতুন কোনো promise-এর ভেতরে wrap করার প্রয়োজন নেই; এটি সরাসরি resolve এবং reject গ্রহণ করে। এটি উপরে উল্লিখিত তিনটি সমস্যা দূর করে।
প্রোডাকশনে যে বিষয়গুলো খেয়াল রাখা জরুরি
promise-এর গঠন আরও সহজ হলেও, বাস্তব জগতের agents-দের আরও কিছু সীমাবদ্ধতার সম্মুখীন হতে হয়:
- Timeouts – উপরের কোডটিতে একটি সাধারণ timer দেখানো হয়েছে যা tool-টি একটি নির্দিষ্ট সময় অতিক্রম করলে reject করে দেয়। SLA অনুযায়ী এই সময়টি সমন্বয় করে নিন।
- Throttling – যখন মূল service থেকে throttling error আসে (যেমন, Bedrock-এর
ThrottlingException), তখন সেটি catch করুন, কিছুক্ষণ বিরতি দিন এবং exponential back-off পদ্ধতিতে পুনরায় চেষ্টা (retry) করুন। এখানে resolve/reject জোড়া একই থাকে; শুধু retry লজিকটি পরিবর্তন হয়। - Lambda cost – AWS Lambda-তে
callbackWaitsForEmptyEventLoop = falseসেট করুন। এটি runtime-কে নির্দেশ দেয় যে handler রিটার্ন করার সাথে সাথেই ফাংশনটি বন্ধ করে দিতে হবে, এমনকি যদি streams বা অন্যান্য background handles খোলা থাকে তবুও। এটি promise অন্য কোথাও settle হওয়ার সময় ফাংশনটিকে অপ্রয়োজনীয়ভাবে চালু রাখতে বাধা দেয়।
যখন এই নতুন helper সব সমস্যার সমাধান নয়
Promise.withResolvers() শুধুমাত্র Node 22 এবং তার পরবর্তী ভার্সনগুলোতে পাওয়া যায়। যেসব প্রজেক্ট পুরনো LTS ভার্সনে চলছে, তাদের হয় এই প্যাটার্নটি polyfill করতে হবে অথবা ক্লাসিক constructor ব্যবহার করতে হবে। Polyfill-গুলো API-এর মতো কাজ করতে পারলেও native performance-এর সুবিধা দেবে না। তা ছাড়া, এই helper কোনো লজিক্যাল বাগ (logical bug) জাদুকরীভাবে সমাধান করে না: ডেভেলপারদের অবশ্যই নিশ্চিত করতে হবে যে প্রতিটি request-এর জন্য resolve অথবা reject-এর মধ্যে ঠিক একটি কল করা হয়েছে, অন্যথায় promise-টি অনির্দিষ্টকালের জন্য pending অবস্থায় থেকে যাবে।
ভবিষ্যতে যা খেয়াল রাখতে পারেন
- Framework adoption – যেসব library LLM agent loop-কে abstract করে (যেমন, open-source Claude wrappers), তারা
withResolvers-কে একটি opt-in feature হিসেবে যুক্ত করতে শুরু করেছে। ভবিষ্যতে এই প্যাটার্নটি ডিফল্ট হিসেবে আসবে কি না, সেদিকে নজর রাখুন। - Node ecosystem – আরও বেশি service যখন Node 22-তে চলে আসবে, তখন এই helper-টি শুধুমাত্র LLM agent-এর জন্য নয়, বরং যেকোনো “fire-and-wait” async প্যাটার্নের জন্য একটি standard হয়ে উঠবে।
- Tool-calling standards – LLM tool call-এর জন্য নতুন যেসব specification আসছে, সেখানে একটি “single-promise” contract থাকতে পারে, যা
withResolversপদ্ধতির সাথে পুরোপুরি সামঞ্জস্যপূর্ণ।
সারকথা: বিস্তারিত new Promise wrapper-এর পরিবর্তে এক লাইনের Promise.withResolvers() ব্যবহার করার মাধ্যমে, Claude-ভিত্তিক agent-গুলো আরও পরিষ্কার flow, কম runtime surprise এবং serverless খরচ নিয়ন্ত্রণে আরও ভালো নিয়ন্ত্রণ পায়—যদি runtime-টি Node 22 সাপোর্ট করে।
