Claude ನ tool-calling loops ಗಳು Node.js ನಲ್ಲಿ ಗೊಂದಲಮಯವಾದ promise code ಅನ್ನು ಸೃಷ್ಟಿಸುತ್ತವೆ ಎಂಬ ಹೆಸರನ್ನು ಹೊಂದಿವೆ. Node.js 22 ರ ಹೊಸ Promise.withResolvers() ಅಭಿವೃದ್ಧಿಪಡಿಸುವವರಿಗೆ (developers) ಬೋയ്‌ಲರ್‌ಪ್ಲೇಟ್-ಭಾರಿತ (boilerplate-heavy) new Promise ಮಾದರಿಯನ್ನು, promise ಮತ್ತು ಅದರ resolve/reject ಫಂಕ್ಷನ್‌ಗಳನ್ನು ಒಟ್ಟಿಗೆ ನೀಡುವ ಒಂದೇ ಸಾಲಿನ ಕೋಡ್‌ನೊಂದಿಗೆ ಬದಲಾಯಿಸಲು ಅನುಮತಿಸುತ್ತದೆ. ಇದರ ಪರಿಣಾಮವಾಗಿ, ಮರೆತುಹೋದ resolve ಕರೆಗಳು ಕಡಿಮೆಯಾಗುತ್ತವೆ, double-reject ಎಚ್ಚರಿಕೆಗಳು ಬರುವುದಿಲ್ಲ ಮತ್ತು serverless ಪರಿಸರಗಳಲ್ಲಿ ಪರೀಕ್ಷಿಸಲು ಮತ್ತು ಕಾರ್ಯನಿರ್ವಹಣೆಯನ್ನು ಕಾಪಾಡಿಕೊಳ್ಳಲು ಸುಲಭವಾದ ಫ್ಲಾಟ್ ಕಂಟ್ರೋಲ್ ಫ್ಲೋ (flatter control flow) ಲಭ್ಯವಾಗುತ್ತದೆ.

ಹಳೆಯ ಮಾದರಿಯು ಫ್ಲೋ ಅನ್ನು ಏಕೆ ಹಾಳುಮಾಡುತ್ತದೆ

Claude ನಂತಹ LLM ಒಂದು tool ಅನ್ನು ಕೇಳಿದಾಗ, ಸಾಮಾನ್ಯ 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 ಸಮಯ ಮುಗಿಯುವವರೆಗೆ (timeout) ಅತಂತ್ರವಾಗಿ ಉಳಿಯುತ್ತದೆ, ಇದರಿಂದ ವೆಚ್ಚ ಹೆಚ್ಚಾಗುತ್ತದೆ.
  • Double reject – reject ಅನ್ನು ಎರಡು ಬಾರಿ ಕರೆಯುವ error path, “unhandled rejection” ಎಚ್ಚರಿಕೆಗಳನ್ನು ನೀಡುತ್ತದೆ, ಇದು strict mode ನಲ್ಲಿ ಪ್ರಕ್ರಿಯೆಯನ್ನು (process) ಕುಸಿಯುವಂತೆ ಮಾಡಬಹುದು.
  • Deep nesting – ಪ್ರತಿಯೊಂದು async ಹಂತವು constructor ಒಳಗೆ ಮತ್ತೊಂದು callback ಅನ್ನು ಅಡಗಿಸುತ್ತದೆ, ಇದು ಲಾಜಿಕ್ ಅನ್ನು ಚದುರಿಸುತ್ತದೆ ಮತ್ತು unit tests ಗಳನ್ನು ದುರ್ಬಲಗೊಳಿಸುತ್ತದೆ.

ಈ ಎಲ್ಲಾ ಸಮಸ್ಯೆಗಳು promise ನ ಕಂಟ್ರೋಲ್ ಫಂಕ್ಷನ್‌ಗಳು constructor ನ closure ಒಳಗೆ ಲಾಕ್ ಆಗಿರುವುದರಿಂದ ಉಂಟಾಗುತ್ತವೆ, ಇದರಿಂದ ಉಳಿದ ಕೋಡ್ ಅದನ್ನು ಮತ್ತೆ ಬಳಸಲು ಕಷ್ಟವಾಗುತ್ತದೆ.

ಒಂದೇ ಸಾಲಿನಲ್ಲಿ Promise.withResolvers()

Node 22 ಒಂದು static helper ಅನ್ನು ಸೇರಿಸಿದೆ, ಇದು promise ಮತ್ತು ಅದನ್ನು settle ಮಾಡುವ ಎರಡು ಫಂಕ್ಷನ್‌ಗಳನ್ನು ಒಳಗೊಂಡಿರುವ ಒಂದು object ಅನ್ನು ಹಿಂತಿರುಗಿಸುತ್ತದೆ:

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

ಈಗ promise ಅನ್ನು ಸಿಸ್ಟಮ್‌ನ ಯಾವುದೇ ಭಾಗಕ್ಕೆ—HTTP handler, database listener, ಅಥವಾ background worker—ಹಸ್ತಾಂತರಿಸಬಹುದು, ಮತ್ತು ಮೂಲ caller ಕೇವಲ promise ಅನ್ನು await ಮಾಡಬಹುದು. ಇಡೀ tool-execution ಬ್ಲಾಕ್ ಅನ್ನು new Promise constructor ನಲ್ಲಿ ಸುತ್ತುವರಿಯುವ (wrap) ಅಗತ್ಯವಿಲ್ಲ.

Claude ನ tool loop ಗೆ ಇದನ್ನು ಅನ್ವಯಿಸುವುದು

Claude ನ ಕಾರ್ಯವಿಧಾನ (workflow) ಹೀಗಿದೆ:

  1. LLM ಒಂದು tool ವಿನಂತಿಯನ್ನು (request) ಹೊರಡಿಸುತ್ತದೆ.
  2. ನಿಮ್ಮ ಕೋಡ್ tool ಅನ್ನು ರನ್ ಮಾಡುತ್ತದೆ (ಉದಾಹರಣೆಗೆ, API call, file read).
  3. tool ನ ಫಲಿತಾಂಶವನ್ನು ಮುಂದಿನ ಹಂತಕ್ಕಾಗಿ 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 ಅನುಷ್ಠಾನವನ್ನು ಇನ್ನು ಮುಂದೆ ಹೊಸ promise ನಲ್ಲಿ ಸುತ್ತುವರಿಯುವ ಅಗತ್ಯವಿಲ್ಲ; ಅದು ಕೇವಲ resolve ಮತ್ತು reject ಅನ್ನು ಪಡೆಯುತ್ತದೆ. ಇದು ಮೇಲೆ ಪಟ್ಟಿ ಮಾಡಲಾದ ಮೂರು ವೈಫಲ್ಯದ ವಿಧಾನಗಳನ್ನು (failure modes) ನಿವಾರಿಸುತ್ತದೆ.

ಇಂದಿಗೂ ಮುಖ್ಯವಾದ production knobs

ಹೆಚ್ಚು ಸುಂದರವಾದ promise ರೂಪವಿದ್ದರೂ ಸಹ, ನೈಜ ಪ್ರಪಂಚದ agents ಇತರ ಮಿತಿಗಳನ್ನು ಎದುರಿಸುತ್ತವೆ:

  • Timeouts – ಮೇಲಿನ ಸ್ನಿಪ್ಪೆಟ್ (snippet) tool ಒಂದು ಮಿತಿಯನ್ನು ಮೀರಿದರೆ reject ಮಾಡುವ ಸರಳ timer ಅನ್ನು ತೋರಿಸುತ್ತದೆ. SLA ನಿರೀಕ್ಷೆಗಳ ಆಧಾರದ ಮೇಲೆ ಅವಧಿಯನ್ನು ಹೊಂದಿಸಿ.
  • Throttling – ಮೂಲ ಸೇವೆ (underlying service) throttling error ಅನ್ನು ನೀಡಿದಾಗ (ಉದಾಹರಣೆಗೆ, Bedrock ನ ThrottlingException), ಅದನ್ನು capture ಮಾಡಿ, ಸ್ವಲ್ಪ ವಿರಾಮ ನೀಡಿ, ಮತ್ತು exponential back-off ನೊಂದಿಗೆ ಮರುಪ್ರಯತ್ನಿಸಿ (retry). resolve/reject ಜೋಡಿಯು ಒಂದೇ ಆಗಿರುತ್ತದೆ; ಕೇವಲ retry logic ಮಾತ್ರ ಬದಲಾಗುತ್ತದೆ.
  • Lambda cost – AWS Lambda ನಲ್ಲಿ, callbackWaitsForEmptyEventLoop = false ಎಂದು ಸೆಟ್ ಮಾಡಿ. ಇದು handler ಹಿಂತಿರುಗಿದ ತಕ್ಷಣ ಫಂಕ್ಷನ್ ಅನ್ನು ಕೊನೆಗೊಳಿಸಲು runtime ಗೆ ಸೂಚಿಸುತ್ತದೆ, ទោះបីಗೆ streams ಅಥವಾ ಇತರ background handles ಇನ್ನೂ ತೆರೆದಿರಲಿ. ಇದು promise ಬೇರೆಡೆ settle ಆಗುವವರೆಗೆ ಫಂಕ್ಷನ್ ಅತಂತ್ರವಾಗಿ ಉಳಿಯುವುದನ್ನು ತಡೆಯುತ್ತದೆ.

ಹೊಸ helper ಯಾವಾಗ ಎಲ್ಲದಕ್ಕೂ ಪರಿಹಾರವಲ್ಲ (silver bullet)

Promise.withResolvers() ಕೇವಲ Node 22 ಮತ್ತು ನಂತರದ ಆವೃತ್ತಿಗಳಲ್ಲಿ ಲಭ್ಯವಿದೆ. ಹಳೆಯ LTS release ಗಳಿಗೆ ಸೀಮಿತವಾಗಿರುವ ಪ್ರಾಜೆಕ್ಟ್‌ಗಳು ಈ ಮಾದರಿಯನ್ನು polyfill ಮಾಡಬೇಕು ಅಥವಾ ಕ್ಲಾಸಿಕ್ constructor ಅನ್ನು ಬಳಸಲೇಬೇಕು. Polyfills API ಅನ್ನು ಅನುಕರಿಸಬಹುದು ಆದರೆ native performance ಪ್ರಯೋಜನಗಳನ್ನು ಪಡೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ. ಅಷ್ಟೇ ಅಲ್ಲದೆ, ಈ helper ತಾರ್ಕಿಕ ದೋಷಗಳನ್ನು (logical bugs) ಮಾಂತ್ರಿಕವಾಗಿ ಪರಿಹರಿಸುವುದಿಲ್ಲ: ಪ್ರತಿಯೊಂದು ವಿನಂತಿಗೂ (request) resolve ಅಥವಾ reject ಇವುಗಳಲ್ಲಿ ನಿಖರವಾಗಿ ಒಂದನ್ನು ಮಾತ್ರ ಕರೆಯಲಾಗಿದೆಯೇ ಎಂದು ಅಭಿವೃದ್ಧಿಪಡಿಸುವವರು (developers) ಖಚಿತಪಡಿಸಿಕೊಳ್ಳಬೇಕು, ಇಲ್ಲದಿದ್ದರೆ promise ಅನಿಯಮಿತವಾಗಿ pending ಸ್ಥಿತಿಯಲ್ಲಿರುತ್ತದೆ.

ಮುಂದೆ ಗಮನಿಸಬೇಕಾದವುಗಳು

  • Framework adoption – LLM agent loops ಅನ್ನು abstract ಮಾಡುವ ಲೈಬ್ರರಿಗಳು (ಉದಾಹರಣೆಗೆ, open-source Claude wrappers) withResolvers ಅನ್ನು ಒಂದು opt-in ಫೀಚರ್ ಆಗಿ ಪರಿಚಯಿಸಲು ಪ್ರಾರಂಭಿಸುತ್ತಿವೆ. ಈ ಮಾದರಿಯನ್ನು ಡಿಫಾಲ್ಟ್ ಆಗಿ ಮಾಡುವ ಅಪ್‌ಡೇಟ್‌ಗಳ ಮೇಲೆ ಕಣ್ಣಿಡಿ.
  • Node ecosystem – ಹೆಚ್ಚಿನ ಸೇವೆಗಳು Node 22 ಗೆ ಬದಲಾಗುತ್ತಿದ್ದಂತೆ, ಈ helper ಕೇವಲ LLM agents ಗೆ ಮಾತ್ರವಲ್ಲದೆ, ಯಾವುದೇ “fire-and-wait” async ಮಾದರಿಗೆ ಒಂದು standard ಆಗಿ ಪರಿಣಮಿಸುತ್ತದೆ.
  • Tool-calling standards – LLM tool calls ಗಾಗಿ ಹೊರಬರುತ್ತಿರುವ ಹೊಸ ನಿಯಮಗಳು (specifications) “single-promise” ಒಪ್ಪಂದವನ್ನು ಸೂಚಿಸಬಹುದು, ಇದು withResolvers ವಿಧಾನಕ್ಕೆ ಸಂಪೂರ್ಣವಾಗಿ ಹೊಂದಿಕೆಯಾಗುತ್ತದೆ.

Takeaway: ವಿವರವಾದ new Promise wrapper ಬದಲಿಗೆ ಒಂದೇ ಸಾಲಿನ Promise.withResolvers() ಅನ್ನು ಬಳಸುವ ಮೂಲಕ, Claude ಆಧಾರಿತ agents ಹೆಚ್ಚು ಸ್ಪಷ್ಟವಾದ ಫ್ಲೋ, ಕಡಿಮೆ runtime ಅನಿರೀಕ್ಷಿತ ಘಟನೆಗಳು ಮತ್ತು serverless ವೆಚ್ಚಗಳ ಮೇಲೆ ಹೆಚ್ಚಿನ ನಿಯಂತ್ರಣವನ್ನು ಪಡೆಯುತ್ತವೆ—runtime Node 22 ಅನ್ನು ಬೆಂಬಲಿಸಿದರೆ ಮಾತ್ರ.