Claude-യുടെ tool-calling loops Node.js-ൽ സങ്കീർണ്ണമായ (tangled) promise കോഡുകൾ ഉണ്ടാക്കുന്നു എന്നൊരു പേരുണ്ട്. Node.js 22-ലെ പുതിയ Promise.withResolvers() ഉപയോഗിച്ച്, കൂടുതൽ കോഡ് ആവശ്യമുള്ള (boilerplate-heavy) new Promise പാറ്റേണിന് പകരം, പ്രോമിസും അതിന്റെ resolve/reject ഫംഗ്ഷനുകളും ഒരൊറ്റ വരിയിൽ ലഭ്യമാക്കാൻ ഡെവലപ്പർമാർക്ക് സാധിക്കും. ഇതിലൂടെ resolve വിളിക്കാൻ മറന്നുപോകുന്ന സാഹചര്യം കുറയുന്നു, "double-reject" മുന്നറിയിപ്പുകൾ ഒഴിവാക്കാം, കൂടാതെ സെർവ്ലെസ് (serverless) എൻവയോൺമെന്റുകളിൽ ടെസ്റ്റ് ചെയ്യാനും നിലനിർത്താനും എളുപ്പമുള്ള ലളിതമായ കൺട്രോൾ ഫ്ലോ (flatter control flow) ലഭിക്കുകയും ചെയ്യുന്നു.

പഴയ പാറ്റേൺ എങ്ങനെയെല്ലാമാണ് പ്രവർത്തനരീതിയെ (flow) തടസ്സപ്പെടുത്തുന്നത്

Claude പോലുള്ള ഒരു LLM ഒരു ടൂൾ ആവശ്യപ്പെടുമ്പോൾ, സാധാരണയായി Node-ൽ നടപ്പിലാക്കുന്ന രീതി ഇപ്രകാരമാണ്:

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

ഇതിൽ പ്രധാനമായും മൂന്ന് പ്രശ്നങ്ങൾ ഉണ്ടാകുന്നു:

  • Forgotten resolve – കോഡ് പാത്ത് ഒരിക്കലും resolve വിളിക്കുന്നില്ലെങ്കിൽ, ഒരു Lambda അല്ലെങ്കിൽ മറ്റ് സെർവ്ലെസ് ഹാൻഡ്‌ലറുകൾ ടൈമൗട്ട് ആകുന്നത് വരെ ഹാങ്ങ് ആയിരിക്കും, ഇത് ചിലവ് വർദ്ധിപ്പിക്കുന്നു.
  • Double reject – ഒരു എറർ പാത്ത് reject രണ്ട് തവണ വിളിച്ചാൽ, “unhandled rejection” മുന്നറിയിപ്പുകൾ വരികയും ഇത് സ്ട്രിക്റ്റ് മോഡിൽ (strict mode) പ്രോസസ്സ് ക്രാഷ് ആകാൻ കാരണമാവുകയും ചെയ്യും.
  • Deep nesting – ഓരോ async സ്റ്റെപ്പും കൺസ്ട്രക്റ്ററിനുള്ളിൽ മറ്റൊരു കോൾബാക്ക് (callback) ഉണ്ടാക്കുന്നു, ഇത് ലോജിക് ചിതറിക്കിടക്കാനും യൂണിറ്റ് ടെസ്റ്റുകൾ പ്രയാസകരമാക്കാനും കാരണമാകുന്നു.

ഈ പ്രശ്നങ്ങളെല്ലാം ഉണ്ടാകുന്നത് പ്രോമിസിന്റെ കൺട്രോൾ ഫംഗ്ഷനുകൾ കൺസ്ട്രക്റ്ററിന്റെ ക്ലോഷറിനുള്ളിൽ (closure) ലോക്ക് ചെയ്യപ്പെട്ടിരിക്കുന്നതുകൊണ്ടാണ്. ഇത് ബാക്കി കോഡുകൾ വീണ്ടും അതിലേക്ക് എത്തി നോക്കേണ്ടി വരുന്ന സാഹചര്യം ഉണ്ടാക്കുന്നു.

Promise.withResolvers() ഒരൊറ്റ വരിയിൽ

Node 22 ഒരു സ്റ്റാറ്റിക് ഹെൽപ്പർ (static helper) ചേർത്തിട്ടുണ്ട്. ഇത് പ്രോമിസും അത് സെറ്റിൽ ചെയ്യുന്ന രണ്ട് ഫംഗ്ഷനുകളും അടങ്ങിയ ഒരു ഒബ്‌ജക്റ്റ് തിരികെ നൽകുന്നു:

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

ഇപ്പോൾ പ്രോമിസ് സിസ്റ്റത്തിന്റെ ഏത് ഭാഗത്തേക്കും—ഒരു HTTP ഹാൻഡ്‌ലർ, ഒരു ഡാറ്റാബേസ് ലിസണർ, അല്ലെങ്കിൽ ഒരു ബാക്ക്ഗ്രൗണ്ട് വർക്കർ—കൈമാറാൻ സാധിക്കും, അതേസമയം ഒറിജിനൽ കോൾ simplemente പ്രോമിസിനായി await ചെയ്താൽ മതി. ടൂൾ എക്സിക്യൂഷൻ ബ്ലോക്ക് മുഴുവനായി ഒരു new Promise കൺസ്ട്രക്റ്ററിൽ പൊതിയേണ്ട ആവശ്യമില്ല.

Claude-യുടെ tool loop-ൽ ഇത് പ്രയോഗിക്കുമ്പോൾ

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 എന്നിവ ലഭിച്ചാൽ മതി. ഇത് മുകളിൽ പറഞ്ഞ മൂന്ന് പരാജയ സാധ്യതകളെയും ഇല്ലാതാക്കുന്നു.

പ്രൊഡക്ഷനിൽ ശ്രദ്ധിക്കേണ്ട കാര്യങ്ങൾ

പ്രോമിസിന്റെ ഘടന ലളിതമാണെങ്കിലും, യഥാർത്ഥ ലോകത്തെ ഏജന്റുകൾ മറ്റ് ചില പരിമിതികളും നേരിടാറുണ്ട്:

  • Timeouts – മുകളിൽ നൽകിയിരിക്കുന്ന സ്നിപ്പറ്റ് ഒരു സിംപിൾ ടൈമർ കാണിക്കുന്നു; ടൂൾ നിശ്ചിത സമയപരിധി കഴിഞ്ഞാൽ അത് reject ചെയ്യും. നിങ്ങളുടെ SLA പ്രതീക്ഷകൾക്കനുസരിച്ച് ഈ സമയം ക്രമീകരിക്കുക.
  • Throttling – അണ്ടർലൈയിംഗ് സർവീസ് ഒരു ത്രോട്ട്ലിംഗ് എറർ (ഉദാഹരണത്തിന്, Bedrock-ന്റെ ThrottlingException) നൽകിയാൽ, അത് ക്യാച്ച് ചെയ്ത്, അല്പനേരം നിർത്തി, എക്സ്പോണൻഷ്യൽ ബാക്ക്-ഓഫ് (exponential back-off) ഉപയോഗിച്ച് വീണ്ടും ശ്രമിക്കുക. ഇവിടെ resolve/reject ജോഡികൾ മാറുന്നില്ല, റീട്രൈ ലോജിക് മാത്രമേ മാറുന്നുള്ളൂ.
  • Lambda cost – AWS Lambda-യിൽ, callbackWaitsForEmptyEventLoop = false എന്ന് സെറ്റ് ചെയ്യുക. ഹാൻഡ്‌ലർ റിട്ടേൺ ചെയ്യുന്ന ഉടൻ തന്നെ ഫംഗ്ഷൻ അവസാനിപ്പിക്കാൻ ഇത് റൺടൈമിനോട് ആവശ്യപ്പെടുന്നു (സ്ട്രീമുകളോ മറ്റ് ബാക്ക്ഗ്രൗണ്ട് ഹാൻഡിലുകളോ തുറന്നിരിക്കുകയാണെങ്കിൽ പോലും). പ്രോമിസ് മറ്റൊരിടത്ത് സെറ്റിൽ ആകുന്നത് വരെ ഫംഗ്ഷൻ നീണ്ടുനിൽക്കുന്നത് ഇത് തടയുന്നു.

ഈ പുതിയ ഹെൽപ്പർ എല്ലാ പ്രശ്നങ്ങൾക്കും പരിഹാരമല്ല

Promise.withResolvers() Node 22-ലും അതിനുശേഷമുള്ള പതിപ്പുകളിലും മാത്രമേ ലഭ്യമാകൂ. പഴയ LTS പതിപ്പുകൾ ഉപയോഗിക്കുന്ന പ്രോജക്റ്റുകൾക്ക് ഈ പാറ്റേൺ പോളിഫിൽ (polyfill) ചെയ്യുകയോ അല്ലെങ്കിൽ പഴയ കൺസ്ട്രക്റ്റർ തന്നെ ഉപയോഗിക്കുകയോ ചെയ്യേണ്ടി വരും. പോളിഫില്ലുകൾക്ക് ഈ API അനുകരിക്കാൻ കഴിയുമെങ്കിലും അവയ്ക്ക് നേറ്റീവ് പെർഫോമൻസ് ഗുണങ്ങൾ ലഭിക്കില്ല. കൂടാതെ, ഈ ഹെൽപ്പർ ലോജിക്കൽ ബഗുകളെ മാന്ത്രികമായി പരിഹരിക്കുന്നില്ല: ഓരോ റിക്വസ്റ്റിനും resolve അല്ലെങ്കിൽ reject എന്നിവയിൽ കൃത്യമായി ഒന്ന് മാത്രം വിളിക്കുന്നുണ്ടെന്ന് ഡെവലപ്പർമാർ ഉറപ്പാക്കേണ്ടതുണ്ട്, അല്ലെങ്കിൽ പ്രോമിസ് അനന്തമായി പെൻഡിംഗ് ആയി തുടരും.

ഇനി ശ്രദ്ധിക്കേണ്ട കാര്യങ്ങൾ

  • Framework adoption – LLM ഏജന്റ് ലൂപ്പുകൾ ലഘൂകരിക്കുന്ന ലൈബ്രറികൾ (ഉദാഹരണത്തിന്, ഓപ്പൺ സോഴ്സ് Claude wrappers) withResolvers ഒരു ഓപ്ഷനായി നൽകാൻ തുടങ്ങിയിട്ടുണ്ട്. ഈ പാറ്റേൺ ഡിഫോൾട്ട് ആകുന്നത് വരെ അപ്‌ഡേറ്റുകൾ ശ്രദ്ധിക്കുക.
  • Node ecosystem – കൂടുതൽ സർവീസുകൾ Node 22-ലേക്ക് മാറുന്നതോടെ, ഇത് LLM ഏജന്റുകൾക്ക് മാത്രമല്ല, ഏത് “fire-and-wait” async പാറ്റേണിനും ഒരു സ്റ്റാൻഡേർഡ് ആയി മാറും.
  • Tool-calling standards – LLM ടൂൾ കോളുകൾക്കായുള്ള പുതിയ സ്പെസിഫിക്കേഷനുകൾ ഒരു “single-promise” കരാർ നിർദ്ദേശിച്ചേക്കാം, ഇത് withResolvers രീതിയുമായി പൂർണ്ണമായും യോജിക്കുന്നു.

ചുരുക്കത്തിൽ: കൂടുതൽ കോഡ് ആവശ്യമുള്ള new Promise എന്ന രീതിക്ക് പകരം ഒരൊറ്റ വരിയിലുള്ള Promise.withResolvers() ഉപയോഗിക്കുന്നതിലൂടെ, Claude അടിസ്ഥാനമാക്കിയുള്ള ഏജന്റുകൾക്ക് കൂടുതൽ വ്യക്തമായ പ്രവർത്തനരീതിയും, റൺടൈം പ്രശ്നങ്ങൾ കുറയ്ക്കാനും, സെർവ്ലെസ് ചിലവുകൾ കൃത്യമായി നിയന്ത്രിക്കാനും സാധിക്കുന്നു—റൺടൈം Node 22 സപ്പോർട്ട് ചെയ്യുന്നുണ്ടെങ്കിൽ മാത്രം.