Claude ਦੇ tool-calling loops Node.js ਵਿੱਚ ਉਲਝੇ ਹੋਏ promise code ਪੈਦਾ ਕਰਨ ਲਈ ਜਾਣੇ ਜਾਂਦੇ ਹਨ।
Node.js 22 ਦਾ ਨਵਾਂ Promise.withResolvers() developers ਨੂੰ boilerplate-heavy new Promise pattern ਦੀ ਜਗ੍ਹਾ ਇੱਕ ਸਿੰਗਲ ਲਾਈਨ ਦੀ ਵਰਤੋਂ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਦਿੰਦਾ ਹੈ, ਜੋ promise ਅਤੇ ਉਸਦੇ resolve/reject functions ਨੂੰ ਇੱਕੋ ਵਾਰ ਵਿੱਚ ਪ੍ਰਦਾਨ ਕਰਦਾ ਹੈ। ਇਸਦਾ ਨਤੀਜਾ ਇਹ ਹੈ ਕਿ resolve calls ਭੁੱਲਣ ਦੀਆਂ ਘਟੀਆਂ ਮਾਮਲੇ, ਕੋਈ double-reject warnings ਨਹੀਂ, ਅਤੇ ਇੱਕ flatter control flow ਮਿਲਦਾ ਹੈ ਜੋ test ਕਰਨ ਅਤੇ serverless environments ਵਿੱਚ ਚਾਲੂ ਰੱਖਣਾ ਆਸਾਨ ਹੈ।

ਪੁਰਾਣਾ pattern flow ਨੂੰ ਕਿਉਂ ਤੋੜਦਾ ਹੈ

ਜਦੋਂ Claude ਵਰਗਾ LLM ਕਿਸੇ tool ਨੂੰ ਪੁੱਛਦਾ ਹੈ, ਤਾਂ ਆਮ Node implementation ਇਸ ਤਰ੍ਹਾਂ ਦਿਖਾਈ ਦਿੰਦੀ ਹੈ:

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

ਤਿੰਨ ਵਾਰ-ਵਾਰ ਹੋਣ ਵਾਲੀਆਂ ਮੁਸ਼ਕਲਾਂ ਸਾਹਮਣੇ ਆਉਂਦੀਆਂ ਹਨ:

  • Forgotten resolve – ਜੇਕਰ code path ਕਦੇ ਵੀ resolve ਨੂੰ call ਨਹੀਂ ਕਰਦਾ, ਤਾਂ Lambda ਜਾਂ ਹੋਰ serverless handler timeout ਹੋਣ ਤੱਕ ਲਟਕਿਆ ਰਹਿੰਦਾ ਹੈ, ਜਿਸ ਨਾਲ ਲਾਗਤ (cost) ਵਧ ਜਾਂਦੀ ਹੈ।
  • Double reject – ਇੱਕ error path ਜੋ reject ਨੂੰ ਦੋ ਵਾਰ call ਕਰਦਾ ਹੈ, ਉਹ “unhandled rejection” warnings trigger ਕਰਦਾ ਹੈ ਜੋ strict mode ਵਿੱਚ process ਨੂੰ crash ਕਰ ਸਕਦਾ ਹੈ।
  • Deep nesting – ਹਰ async step constructor ਦੇ ਅੰਦਰ ਇੱਕ ਹੋਰ callback ਨੂੰ nest ਕਰਦਾ ਹੈ, ਜਿਸ ਨਾਲ logic ਖਿੰਡ ਜਾਂਦਾ ਹੈ ਅਤੇ unit tests ਕਮਜ਼ੋਰ ਹੋ ਜਾਂਦੇ ਹਨ।

ਇਹ ਸਾਰੀਆਂ ਸਮੱਸਿਆਵਾਂ ਇਸ ਤੱਥ ਤੋਂ ਪੈਦਾ ਹੁੰਦੀਆਂ ਹਨ ਕਿ promise ਦੇ control functions constructor ਦੇ closure ਦੇ ਅੰਦਰ locked ਹੁੰਦੇ ਹਨ, ਜਿਸ ਕਾਰਨ ਬਾਕੀ code ਨੂੰ ਵਾਪਸ ਉਸ ਤੱਕ ਪਹੁੰਚਣਾ ਪੈਂਦਾ ਹੈ।

ਇੱਕ ਲਾਈਨ ਵਿੱਚ Promise.withResolvers()

Node 22 ਇੱਕ static helper ਜੋੜਦਾ ਹੈ ਜੋ ਇੱਕ object return ਕਰਦਾ ਹੈ ਜਿਸ ਵਿੱਚ ਇੱਕ promise ਅਤੇ ਉਹ ਦੋ functions ਹੁੰਦੇ ਹਨ ਜੋ ਇਸਨੂੰ settle ਕਰਦੇ ਹਨ:

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

ਹੁਣ promise ਨੂੰ system ਦੇ ਕਿਸੇ ਵੀ ਹਿੱਸੇ—ਜਿਵੇਂ ਕਿ HTTP handler, database listener, ਜਾਂ background worker—ਨੂੰ ਸੌਂਪਿਆ ਜਾ ਸਕਦਾ ਹੈ, ਜਦੋਂ ਕਿ original caller ਸਿਰਫ਼ promise ਨੂੰ await ਕਰਦਾ ਹੈ। ਪੂਰੇ tool-execution block ਨੂੰ new Promise constructor ਵਿੱਚ wrap ਕਰਨ ਦੀ ਕੋਈ ਲੋੜ ਨਹੀਂ ਹੈ।

ਇਸਨੂੰ Claude ਦੇ tool loop 'ਤੇ ਲਾਗੂ ਕਰਨਾ

Claude ਦਾ workflow ਇਹ ਹੈ:

  1. LLM ਇੱਕ tool request ਜਾਰੀ ਕਰਦਾ ਹੈ।
  2. ਤੁਹਾਡਾ code tool ਨੂੰ ਚਲਾਉਂਦਾ ਹੈ (ਜਿਵੇਂ ਕਿ API call, file read)।
  3. 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 ਉਮੀਦਾਂ ਦੇ ਅਧਾਰ 'ਤੇ duration ਨੂੰ ਐਡਜਸਟ ਕਰੋ।
  • Throttling – ਜਦੋਂ underlying service ਇੱਕ throttling error (ਜਿਵੇਂ ਕਿ Bedrock ਦਾ ThrottlingException) ਵਾਪਸ ਕਰਦੀ ਹੈ, ਤਾਂ ਇਸਨੂੰ catch ਕਰੋ, ਰੁਕੋ, ਅਤੇ exponential back-off ਨਾਲ ਦੁਬਾਰਾ ਕੋਸ਼ਿਸ਼ (retry) ਕਰੋ। Resolve/reject ਜੋੜੜਾ ਉਹੀ ਰਹਿੰਦਾ ਹੈ; ਸਿਰਫ਼ retry logic ਬਦਲਦਾ ਹੈ।
  • Lambda cost – AWS Lambda ਵਿੱਚ, callbackWaitsForEmptyEventLoop = false ਸੈੱਟ ਕਰੋ। ਇਹ runtime ਨੂੰ ਦੱਸਦਾ ਹੈ ਕਿ ਜਿਵੇਂ ਹੀ handler ਵਾਪਸ ਆਵੇ, function ਨੂੰ ਖਤਮ ਕਰ ਦਿਓ, ਭਾਵੇਂ streams ਜਾਂ ਹੋਰ background handles ਅਜੇ ਵੀ ਖੁੱਲ੍ਹੇ ਹੋਣ। ਇਹ function ਨੂੰ ਉਦੋਂ ਤੱਕ ਲਟਕਣ ਤੋਂ ਰੋਕਦਾ ਹੈ ਜਦੋਂ ਤੱਕ promise ਕਿਤੇ ਹੋਰ settle ਨਹੀਂ ਹੋ ਜਾਂਦਾ।

ਜਦੋਂ ਨਵਾਂ helper ਕੋਈ ਜਾਦੂਈ ਹੱਲ (silver bullet) ਨਹੀਂ ਹੁੰਦਾ

Promise.withResolvers() ਸਿਰਫ਼ Node 22 ਅਤੇ ਇਸ ਤੋਂ ਬਾਅਦ ਦੇ ਵਰਜ਼ਨਾਂ ਵਿੱਚ ਉਪਲਬਧ ਹੈ। ਪੁਰਾਣੇ LTS releases 'ਤੇ ਅਟਕ ਗਏ projects ਨੂੰ ਜਾਂ ਤਾਂ pattern ਨੂੰ polyfill ਕਰਨਾ ਪਵੇਗਾ ਜਾਂ classic constructor ਨਾਲ ਹੀ ਰਹਿਣਾ ਪਵੇਗਾ। Polyfills API ਦੀ ਨਕਲ ਕਰ ਸਕਦੇ ਹਨ ਪਰ native performance ਦੇ ਫਾਇਦੇ ਨਹੀਂ ਮਿਲਣਗੇ। ਇਸ ਤੋਂ ਇਲਾਵਾ, helper ਜਾਦੂਈ ਤਰੀਕੇ ਨਾਲ logical bugs ਨੂੰ ਹੱਲ ਨਹੀਂ ਕਰਦਾ: developers ਨੂੰ ਅਜੇ ਵੀ ਇਹ ਯਕੀਨੀ ਬਣਾਉਣ ਦੀ ਲੋੜ ਹੈ ਕਿ ਹਰ request ਲਈ resolve ਜਾਂ reject ਵਿੱਚੋਂ ਸਿਰਫ਼ ਇੱਕ ਹੀ call ਕੀਤਾ ਜਾਵੇ, ਨਹੀਂ ਤਾਂ promise ਅਨੰਤ ਸਮੇਂ ਲਈ 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 ਨਿਰਧਾਰਤ ਕਰ ਸਕਦੀਆਂ ਹਨ, ਜੋ withResolvers approach ਦੇ ਨਾਲ ਬਿਲਕੁਲ ਮੇਲ ਖਾਂਦੀਆਂ ਹਨ।

Takeaway: Verbose new Promise wrapper ਦੀ ਜਗ੍ਹਾ ਇੱਕ ਲਾਈਨ ਵਾਲਾ Promise.withResolvers() ਵਰਤ ਕੇ, Claude-based agents ਨੂੰ ਸਪੱਸ਼ਟ flow, runtime surprises ਦੀ ਘੱਟ ਸੰਭਾਵਨਾ, ਅਤੇ serverless costs 'ਤੇ ਬਿਹਤਰ ਕੰਟਰੋਲ ਮਿਲਦਾ ਹੈ—ਬਸ਼ਰਤੇ ਕਿ runtime Node 22 ਦਾ ਸਮਰਥਨ ਕਰਦਾ ਹੋਵੇ।