ללופים (loops) של קריאה לכלים (tool-calling) של Claude יש מוניטין של יצירת קוד promise מסורבל ב-Node.js. הפונקציה החדשה Promise.withResolvers() ב-Node.js 22 מאפשרת למפתחים להחליף את תבנית ה-new Promise העמוסה ב-boilerplate בשורה אחת שמחלקת את ה-promise ואת פונקציות ה-resolve/reject שלו. התוצאה היא פחות קריאות resolve שנשכחו, ללא אזהרות על double-reject, וזרימת בקרה (control flow) שטוחה יותר שקל יותר לבדוק ולשמור על פעילות בסביבות serverless.
למה התבנית הישנה שוברת את זרימת העבודה
כש-LLM כמו Claude מבקש להשתמש בכלי, מימוש ה-Node הטיפוסי נראה כך:
return new Promise((resolve, reject) => {
// launch the tool, attach callbacks, maybe fire another async call
});
עולים שלושה מלכודות חוזרות:
- resolve שנשכח – אם נתיב הקוד לעולם לא קורא ל-
resolve, פונקציית Lambda או כל handler אחר ב-serverless ייתקעו עד לפקיעת הזמן (timeout), מה שמעלה את העלויות. - double reject – נתיב שגיאה שקורא ל-
rejectפעמיים מפעיל אזהרות "unhandled rejection" שעלולות להפיל את התהליך במצב strict mode. - קינון עמוק (Deep nesting) – כל שלב אסינכרוני מקנן callback נוסף בתוך ה-constructor, מה שמפזר את הלוגיקה והופך את בדיקות היחידה (unit tests) לשבירות.
כל הבעיות הללו נובעות מהעובדה שפונקציות הבקרה של ה-promise נעולות בתוך ה-closure של ה-constructor, מה שמאלץ את שאר הקוד "להגיע" בחזרה אליהן.
Promise.withResolvers() בשורה אחת
Node 22 מוסיפה helper סטטי שמחזיר אובייקט המכיל promise ושתי הפונקציות שמסיימות אותו (settle):
const { promise, resolve, reject } = Promise.withResolvers();
כעת ניתן להעביר את ה-promise לכל חלק במערכת — HTTP handler, מאזין למסד נתונים (database listener), או worker ברקע — בעוד שהקורא המקורי פשוט מבצע await ל-promise. אין צורך לעטוף את כל בלוק הרצת הכלי בתוך constructor של new Promise.
יישום על לופ הכלי של Claude
זרימת העבודה של Claude היא:
- ה-LLM מוציא בקשה לשימוש בכלי.
- הקוד שלך מריץ את הכלי (למשל, קריאת API, קריאת קובץ).
- תוצאת הכלי נשלחת חזרה ל-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
}
}
אין צורך יותר לעטוף את מימוש הכלי ב-promise חדש; הוא פשוט מקבל resolve ו-reject. זה מבטל את שלושת מצבי הכשל שפורטו לעיל.
כפתורי בקרה (knobs) בסביבת ייצור שעדיין חשובים
גם עם מבנה promise נקי יותר, סוכנים (agents) בעולם האמיתי נתקלים באילוצים אחרים:
- Timeouts – הקטע לעיל מציג טיימר פשוט שמבצע
rejectאם הכלי חורג מסף מסוים. כדאי להתאים את משך הזמן בהתאם לציפיות ה-SLA. - Throttling – כאשר השירות שבבסיס מחזיר שגיאת throttling (למשל,
ThrottlingExceptionשל Bedrock), יש לתפוס אותה, להשהות, ולנסות שוב עם exponential back-off. זוג ה-resolve/reject נשאר זהה; רק לוגיקת הניסיון החוזר משתנה. - עלות Lambda – ב-AWS Lambda, הגדירו
callbackWaitsForEmptyEventLoop = false. זה אומר לסביבת ההרצה (runtime) לסיים את הפונקציה ברגע שה-handler מחזיר ערך, גם אם streams או handles אחרים ברקע עדיין פתוחים. זה מונע מהפונקציה להישאר פעילה בזמן שה-promise נסגר במקום אחר.
מתי ה-helper החדש אינו "פתרון קסם"
Promise.withResolvers() זמין רק ב-Node 22 ומעלה. פרויקטים שנעולים על גרסאות LTS ישנות יותר חייבים להשתמש ב-polyfill עבור התבנית או להיצמד ל-constructor הקלאסי. polyfills יכולים לחקות את ה-API אך לא יספקו את יתרונות הביצועים הטבעיים (native). יתרה מכך, ה-helper לא פותר באורח קסם באגים לוגיים: מפתחים עדיין צריכים לוודא שאחת ורק אחת מהפונקציות resolve או reject נקראת עבור כל בקשה, אחרת ה-promise יישאר במצב pending לנצח.
מה כדאי לעקוב אחריו בהמשך
- אימוץ על ידי Frameworks – ספריות שמבצעות הפשטה (abstract) ללופים של סוכני LLM (למשל, Claude wrappers בקוד פתוח) מתחילות לחשוף את
withResolversכפיצ'ר שניתן להפעלה (opt-in). כדאי לעקוב אחר עדכונים שהופכים את התבנית לברירת מחדל. - אקוסיסטם ה-Node – ככל שיותר שירותים יעברו ל-Node 22, ה-helper יהפוך לתקן de-facto לכל תבנית async מסוג "fire-and-wait", ולא רק עבור סוכני LLM.
- סטנדרטים של קריאה לכלים – מפרטים מתהווים עבור קריאות כלים ב-LLM עשויים להגדיר חוזה של "single-promise", מה שמתיישב בצורה מושלמת עם גישת ה-
withResolvers.
שורה תחתונה: על ידי החלפת עטיפת ה-new Promise המפורטת בשורה אחת של Promise.withResolvers(), סוכנים מבוססי Claude מקבלים זרימת עבודה ברורה יותר, פחות הפתעות בזמן ריצה ושליטה הדוקה יותר בעלויות serverless — בתנאי שסביבת ההרצה תומכת ב-Node 22.
