🧵 חוט הפרויקט
מאיפה באנו: בפרק 2, "אנטומיה של Harness דק", בנינו את harness.py — לולאת agent מפורשת שמקבלת goal, קוראת למודל, מריצה tools, ומצברת message state (system / user / assistant / tool_result) בין סבבים, עם max-turns כבלם ראשון.
איפה אנחנו עכשיו: הלולאה הזו עובדת מצוין על משימות קצרות — אבל על ריצה של 30, 50, 100 סבבים היא נתקלת בקיר שלא ראינו: ה-context window מתמלא. בפרק הזה נוסיף ל-harness שכבת ניהול context: מדידה בזמן ריצה, זיהוי ה-Compaction Buffer, ו-backup מבוקר שמונע איבוד היסטוריה.
לאן אנחנו הולכים: בפרק 4, "Tools, MCP ו-Governance", נחזק את שכבת ה-tools עצמה — structured output, חיבורים חיצוניים דרך MCP, ושערי governance דטרמיניסטיים. ניהול ה-context שנבנה כאן הוא תנאי מקדים: tool שמחזיר 50KB ישר ל-context יכול להפיל ריצה לבדו, ובפרק 4 נלמד להחזיר reference במקום dump.
🎯 מטרות הלמידה
בסוף הפרק הזה תוכל/י:
- למדוד את שימוש ה-context בזמן ריצה — input / output / cache / buffer — ולזהות בדיוק מתי הריצה מתקרבת לסף ה-compaction של 83.5%, לפני שהיא חוצה אותו.
- להסביר את ה-Compaction Buffer Trap — למה ה-Claude Agent SDK שומר באופן קשיח ~33K tokens ל-output ול-safety, ולמה זה אומר שה-history הזמין בפועל קטן ממה שנדמה.
- לממש backup מבוסס-threshold — hook בלולאה שכשהשימוש חוצה סף מקדים (למשל 75%), שומר state קריטי (decisions, deliverables) ל-store חיצוני לפני ש-compaction אוטומטי מוחק אותו.
- לעצב אסטרטגיית context management ל-harness: מה תמיד-נשמר, מה מסוכם (distillation), ומה נזרק ל-store חיצוני (scratchpad) — עם compaction מבוקר במקום אוטומטי-עיוור.
📋 דרישות קדם
- פרק 1 — Harness Engineering: מה זה harness ולמה ~70% מהביצועים חיים מחוץ למודל.
- פרק 2 — Harness דק: לולאת agent מפורשת, message state (system/user/assistant/tool_result),
max-turns, והבנה שהמודל הוא stateless וה-harness הוא ה-state. - סביבת עבודה — Claude Agent SDK מותקן (Python), חשבון Anthropic עם API key (לא subscription, לאוטומציה), ונוחות ב-Terminal.
- ידע מושגי — מה זה token, מה זה context window. אם המונחים האלה זרים — חזרו לפרק 2 לפני שתמשיכו.
📦 תוצרים (Deliverables)
בסוף הפרק יהיו לך שלושה רכיבים חדשים שמתחברים ישירות ל-harness.py מפרק 2:
- context-monitor — רכיב שמדווח בכל turn את אחוז השימוש ב-context (לפי input/output tokens מתגובת ה-SDK), ומסמן בצבע מתי מתקרבים ל-83.5%.
- threshold-backup — מנגנון שכשהשימוש חוצה סף מקדים (75% כברירת מחדל), משמר state קריטי — decisions, deliverables, constraints — ל-store חיצוני (קובץ JSON) לפני שה-compaction האוטומטי של 83.5% נורה.
- טבלת אסטרטגיית context — מסמך החלטה: מה תמיד-נשמר / מה מסוכם / מה נזרק ל-store חיצוני, עם נימוק לכל קטגוריה — כדי שה-compaction יהיה מבוקר, לא עיוור.
למה ריצה ארוכה נופלת מקיר שלא ראית
בפרק 2 הרצת את ה-harness על משימה קצרה — "קרא קובץ, סכם אותו, כתוב את הסיכום". הסוכן עשה שלושה-ארבעה סבבים, הלולאה נעצרה ב-final answer, וזה עבד. עכשיו דמיין/י משימה אמיתית: "עבור על ה-repo, מצא את כל ה-endpoints שלא מכוסים בטסטים, כתוב טסט לכל אחד, הרץ את הסוויטה, תקן מה שנשבר". זו ריצה של 40, 60, אולי 100 סבבים. ובמקום מסוים — בלי אזהרה — הסוכן מתחיל "לשכוח": הוא חוזר על decision שכבר קיבל, הוא מנסה לכתוב טסט לקובץ שכבר טיפל בו, הוא שואל אותך משהו שכבר עניתָ עליו.
זה לא bug במודל. זה קיר ה-context. כל סבב בלולאה שלך לא שולח רק את ההודעה האחרונה — הוא שולח את כל ההיסטוריה מחדש: ה-system prompt, ה-goal, כל ה-assistant messages, כל ה-tool_result שחזרו. המודל הוא stateless (כפי שראינו בפרק 2), אז ה-harness אחראי לשלוח את כל ההקשר בכל פעם. ככל שהריצה מתארכת, ההיסטוריה תופחת — וברגע מסוים היא נתקלת בגבול חלון ה-context.
אבל הגבול הזה לא נמצא איפה שאתה חושב. הוא נמוך יותר. הרבה יותר. וזו בדיוק הסכנה: ה-harness שלך נדחס ומאבד מידע הרבה לפני שספרת את כל ה-tokens שזמינים לכאורה. בפרק הזה נפרק בדיוק למה.
הדינמיקה שעושה את ההבדל: צמיחה מצטברת
הדבר שהופך את קיר ה-context לערמומי הוא שהוא לא ליניארי באינטואיציה שלך. אתה חושב על "הודעה אחת" — אבל ה-harness חושב על "כל ההודעות עד עכשיו, בכל סבב, מחדש". זו צמיחה מצטברת: בסבב 5 שלחת מחדש את 4 הסבבים הקודמים, בסבב 30 אתה שולח מחדש את 29 הקודמים. הנה (דוגמה מייצגת) איך זה מרגיש בשטח:
- סבבים 1–10: הכול ירוק. ה-context תופס אולי 15% מהחלון. אתה משוכנע שאין בעיה ושכל הדיבורים על compaction הם פרנויה.
- סבבים 11–25: tool results מתחילים להצטבר. כל קריאת
read_fileמוסיפה את תוכן הקובץ ל-context לתמיד. אתה ב-50%, אבל עדיין לא שמת לב כי לא מדדת. - סבב 26: tool יחיד מחזיר JSON של 40KB. בקפיצה אחת אתה מ-65% ל-84%. ה-compaction נורה. כל ההיסטוריה היפה שלך נדחסת — וה-decision מסבב 8 נעלם.
- סבב 31: הסוכן מתנהג כאילו סבב 8 לא קרה. אתה מבולבל. "למה הוא שולח email בלי אישור? הרי החלטנו..."
המספרים האלה הם המחשה, לא מדידה מדויקת של מקרה ספציפי — אבל הצורה הזו חוזרת בכל harness שלא מנהל context. השקט בהתחלה הוא בדיוק מה שמשטה בך. כשהבעיה מתפרצת היא כבר עברה את נקודת האל-חזור באותה ריצה.
חשבון ה-tokens בזמן אמת — מה קורה בסבב 5 ובסבב 26
כדי שהדינמיקה תהיה ממשית ולא מופשטת, בוא נעשה את החשבון מספרית על דוגמה מייצגת של משימת lead-enrichment — שירוצו 30 סבבים (בחלון של 200K):
| סבב | system_prompt | history | tool_results | סך-הכל | % חלון | סטטוס |
|---|---|---|---|---|---|---|
| 5 | 2,400 tok | 8,200 tok | 12,000 tok | 22,600 | 11.3% | 🟢 בטוח |
| 15 | 2,400 tok | 28,000 tok | 65,000 tok | 95,400 | 47.7% | 🟢 נוח, אבל גדל |
| 25 | 2,400 tok | 42,000 tok | 108,000 tok | 152,400 | 76.2% | 🟡 backup רץ |
| 26 | 2,400 tok | 45,000 tok | 120,000 tok | 167,400 | 83.7% | 🔴 compaction נורה! |
שים/י לב מה קורה בין סבב 25 לסבב 26: tool result אחד (Apollo enrichment ל-50 leads) הוסיף ~12,000 tokens בקפיצה אחת — מ-76.2% ל-83.7%. זהו בדיוק המנגנון שגורם ל-compaction לדחוס את ההיסטוריה מהר יותר מהאינטואיציה שלך. בסבב 5 הרגשת בנוח — 11.3% זה כאילו "יש עוד המון מקום". 21 סבבים לאחר מכן הריצה כבר בסכנה.
הנה המספרים שאיתם כדאי לעבוד — ב-system_prompt קבוע של 2,400 tokens ו-tool results ממוצעים של 5,000–8,000 tokens לסבב: אתה מגיע ל-75% (סף ה-backup) תוך בערך 22–25 סבבים. זה פחות מסבב 30, ובהחלט פחות ממה שרוב הבונים מדמיינים כ"ריצה ארוכה".
⚡ עשה עכשיו (3 דקות)
קח/י משימה אמיתית שכבר הרצת עם ה-harness מפרק 2, ונסה/נסי לשחזר בראש: כמה tool_result נכנסו לאורך הריצה, וכמה גדול היה הכבד שבהם? אם אפילו אחד מהם היה קובץ שלם או תשובת API ארוכה — סמן/סמני אותו בעיגול. זה החשוד הראשון שיקפיץ אותך לסף ה-compaction בריצה הבאה. רשום/רשמי: "ה-tool שהכי מסוכן ל-context שלי הוא ___".
tokens שה-Claude Agent SDK שומר באופן קשיח ל-output ול-safety — נפח שלא זמין ל-history שלך, גם אם החלון נראה ריק. (מקור: course.research.json → gotchas)
⚡ עשה עכשיו (2 דקות)
פתח/י את harness.py מפרק 2 וענה/י בכתב על שאלה אחת: איפה בלולאה שלך נמדד כמה context כבר נצרך? אם התשובה היא "בשום מקום" — מצוין, זו בדיוק הבעיה. רשום/רשמי לעצמך: "ה-harness שלי רץ עיוור מול ה-context". בסוף הפרק נתקן את זה.
מבנה חלון ה-Context — מאיפה נגמר המקום
בוא נפרק את חלון ה-context לרצועות. חלון ה-context (context window) הוא כמות ה-tokens המקסימלית שהמודל יכול "לראות" בקריאה אחת — input ו-output יחד. כשאתה קורא למודל בלולאה, הקריאה מורכבת מכמה שכבות שמצטברות:
| רצועה | מה יושב שם | גדל לאורך הריצה? |
|---|---|---|
| System | ה-system prompt — config ה-harness, הגדרות tools, constraints | לא (קבוע) |
| History | כל ה-user / assistant messages מהסבבים הקודמים | כן, בכל סבב |
| Tool results | ה-tool_result blocks — פלט של כל קריאת tool | כן, ולפעמים בקפיצות ענק |
| Output buffer | שטח שמור ל-output של המודל + safety margin | קבוע ושמור מראש (~33K) |
שלוש הרצועות הראשונות הן ה-input. הרצועה הרביעית היא העוקץ. ה-Claude Agent SDK לא נותן לך את כל החלון ל-input. הוא שומר באופן קשיח חלק מהחלון ל-output שהמודל עוד צריך לייצר, ועוד שוליים ל-safety. לפי ה-research של הקורס, ה-buffer הזה עומד על כ-33K tokens. כלומר: אם החלון הוא, נניח, 200K tokens, ה-history שלך לא יכול לתפוס 200K — הוא נחנק הרבה קודם, כי 33K כבר "תפוסים" עוד לפני שכתבת בית אחד.
⚠️ טעות נפוצה: להניח שכל חלון ה-context זמין ל-history
זו הטעות שמפילה harnesses ביתיים יותר מכל אחרת. מתכנת רואה "חלון של 200K" ומחשב שיש לו 200K להיסטוריה. בפועל יש לו פחות — ה-33K buffer חוטף נתח קבוע, וה-compaction מתחיל לדחוס עוד לפני שהגיע לגבול הנותר. התוצאה: הריצה נדחסת הרבה לפני שחשבת, ואיבדת state בלי לדעת. הכלל: תכנן תמיד מול ה-history הזמין בפועל, לא מול גודל החלון הנקוב.
🧭 Framework: שלושת כללי ה-context budget
- אם השימוש ב-context נמצא מתחת ל-50% אחרי 5 סבבים → אז אין צורך בפעולה; המשך לרוץ, אבל המשך למדוד.
- אם השימוש חוצה 60% לפני סבב 10 → אז זהה את ה-tool result הכבד ביותר ועבור אותו ל-offload. הצמיחה מהירה מדי לשרוד ריצה ארוכה.
- אם decisions ו-deliverables מצטברים דינמית (לא רק ב-system prompt) → אז חווט
record_decision+ threshold-backup לפני שאתה מוסיף אפילו סבב אחד ל-max-turns.
למה ה-buffer הזה קיים בכלל? כי המודל חייב מקום ליצור את התשובה. אם תמלא את כל החלון ב-input, לא יישאר מקום ל-output — והקריאה תיכשל או תיחתך באמצע. ה-SDK מגן עליך מזה בכך שהוא שומר את ה-33K מראש. הבעיה היא לא שה-buffer קיים — הבעיה היא שרוב הבונים לא יודעים שהוא קיים, ומגלים אותו רק כשהריצה כבר איבדה היסטוריה.
החשבון בפועל — שלוש שורות שכל בונה harness חייב לדעת בעל-פה
בוא נעשה את החשבון על חלון של 200K tokens (התאם/י למודל שלך — חלקם 200K, חלקם יותר):
| שלב | חישוב | תוצאה |
|---|---|---|
| חלון מלא | נתון | 200,000 tok |
| פחות Compaction Buffer | 200,000 − 33,000 | 167,000 tok זמינים ל-history |
| סף compaction אוטומטי | 200,000 × 0.835 | ~167,000 tok — וכאן הצירוף המסוכן |
שים/י לב להתכנסות המטרידה: ה-buffer מותיר ~167K, וסף ה-83.5% גם הוא נופל בערך באותו אזור. כלומר ברגע שה-history שלך מתקרב למלא את המקום הזמין — ה-compaction כבר נורה. אין "אזור ביטחון" נדיב בין שני המספרים. זה למה הסכנה אמיתית ולא תיאורטית: שני המנגנונים — ה-buffer וה-threshold — מצטלבים כמעט באותה נקודה, וכל קפיצה אחת (tool result כבד) מספיקה כדי לחצות.
הכלל שתישא/י איתך: כשאתה מתכנן כמה context "יש לך", התחל מ-83.5% של החלון, לא מ-100%. ומתוך זה — שמור עוד שוליים משלך, כי אתה רוצה לגבות לפני הסף, לא עליו.
⚡ עשה עכשיו (2 דקות)
מצא/י את גודל חלון ה-context של המודל שאתה משתמש בו (ב-docs של ה-SDK / API). עכשיו חשב/י בעצמך שלוש מספרים ורשום/רשמי אותם על דף: (א) החלון המלא, (ב) החלון פחות 33K, (ג) 83.5% מהחלון. אלה שלושת המספרים שכל ה-harness שלך יתבסס עליהם. הצמד/הצמידי אותם למסך.
📖 מילון מונחים
- Context Window (חלון ה-Context)
- כמות ה-tokens המקסימלית שהמודל מעבד בקריאה אחת — input ו-output יחד. כשהיא מתמלאת, צריך לפנות מקום.
- Compaction Buffer (חוצץ הדחיסה)
- נפח קבוע (~33K tokens ב-Claude Agent SDK) ששמור ל-output של המודל ול-safety, ולכן אינו זמין ל-history. (מקור: course.research.json)
- Context Compaction (דחיסת Context)
- התהליך שבו היסטוריית השיחה מסוכמת או נדחסת כדי לפנות מקום בחלון. יכול להיות אוטומטי (ה-SDK) או מבוקר (אתה).
- Lossy Compaction (דחיסה מאבדת-מידע)
- דחיסה שמוחקת מידע שלא ניתן לשחזר. כל compaction אוטומטי הוא lossy — השאלה היא מה בדיוק אבד.
- Token Accounting (חשבונאות tokens)
- מדידה בזמן ריצה של input tokens, output tokens ו-cache hits בכל turn — מתוך השדה
usageבתגובת ה-SDK. - Threshold-Based Backup (גיבוי מבוסס-סף)
- שמירה אוטומטית של state קריטי כשהשימוש ב-context חוצה סף מוגדר מראש (למשל 75%) — לפני ש-compaction נורה.
- Scratchpad / Working Memory (פנקס עבודה)
- store חיצוני (קובץ / KV) שאליו מוציאים state כבד מה-context, ומשאירים ב-context רק reference קצר.
- Distillation (זיקוק)
- תהליך של החלפת קבוצת הודעות ישנות ב-context בסיכום קצר שמשמר את התוצאות אבל זורק את תהליך ההגעה אליהן. בניגוד ל-compaction אוטומטי (עיוור), distillation מבוקרת שומרת את ה-deliverables המדויקים ("3 endpoints: /users/export, /billing/refund, /admin/purge") ומסכמת רק את ה-"איך".
- Context Budget per Turn (תקציב context לסבב)
- תקרה מוגדרת מראש על כמות ה-tokens שמותר לאובייקט בודד (tool result, הודעה יחידה) להכניס ל-context בסבב אחד. tool result שעובר את התקרה (למשל 4,000 tokens) חייב לעבור offload ל-scratchpad חיצוני ולהחזיר ל-context reference בלבד.
ה-Compaction Buffer Trap — מה קורה כשמגיעים ל-83.5%
עכשiו שיש לך את מבנה החלון, בוא נראה מה קורה כשהוא מתמלא. ה-Claude Agent SDK לא מחכה שהחלון יתפוצץ. הוא מפעיל compaction אוטומטי כשהשימוש נוגע ב-~83.5% מהחלון (מקור: course.research.json → gotchas). ברגע הזה ה-SDK לוקח את ההיסטוריה, מסכם אותה — ומחליף הרבה הודעות בייצוג דחוס יותר. המטרה טובה: לפנות מקום כדי שהריצה תמשיך. הבעיה: אתה לא בחרת מה לשמר.
סף השימוש בחלון שבו נורה compaction אוטומטי ב-Claude Agent SDK. מעבר לסף הזה — ההיסטוריה שלך נדחסת בלי שתשלוט מה נשאר ומה אבד. (מקור: course.research.json)
זה ה-Compaction Buffer Trap: שילוב של שני המספרים. ה-buffer של 33K כבר מקטין את ה-history הזמין, ואז סף ה-83.5% מפעיל דחיסה בתוך מה שנשאר. הריצה שלך נדחסת מוקדם מאוד ביחס לאינטואיציה — וה-compaction הוא lossy: הוא מאבד מידע. אם ה-harness שלך לא תוכנן לזה, decision קריטי שהסוכן קיבל בסבב 12, או deliverable חלקי שבנה בסבב 20, יכולים פשוט להיעלם תחת לחץ ה-compaction. הסוכן ימשיך לרוץ — אבל בלי הזיכרון של מה שעשה. וזו בדיוק נקודת הכישלון: אתה מגלה את זה רק כשהסוכן "שוכח" מה הוא עשה ומתחיל מחדש.
⚠️ טעות נפוצה: לסמוך על ה-compaction האוטומטי כברירת מחדל
ה-compaction האוטומטי נוח — הוא "פשוט עובד" ומונע קריסה. אבל הוא lossy ועיוור: הוא לא יודע ש-decision מסוים קריטי, או ש-deliverable מסוים עוד לא נשמר לדיסק. הוא דוחס לפי כלל כללי, וה-state הקריטי שלך עלול להימחק באמצע ריצה. אתה מגלה את זה רק כשהסוכן חוזר על עבודה שכבר עשה, או "מאבד" החלטה שכבר קיבל. הכלל: compaction אוטומטי הוא רשת ביטחון של מוצא אחרון — לא אסטרטגיית ניהול context.
למה "lossy" זה לא תיאורטי
בוא נמחיש עם תרחיש קונקרטי (דוגמה מייצגת). הסוכן שלך רץ על משימת lead-enrichment:
- סבב 8: הסוכן מקבל decision — "Lead מסוג enterprise תמיד עובר לאישור אנושי לפני שליחת email". זה החלטה לוגית קריטית.
- סבב 25: הסוכן צרך כבר הרבה tool results (Apollo enrichment, GitHub stars, drafts). השימוש נוגע ב-83.5%. compaction אוטומטי נורה.
- הדחיסה: ה-SDK מסכם את ההיסטוריה ל"הסוכן העשיר 14 leads ושייך אותם". ה-decision מסבב 8? לא הוזכר בסיכום — נמחק.
- סבב 31: הסוכן מגיע ל-lead enterprise. בלי ה-decision מסבב 8, הוא שולח email בלי אישור אנושי. נזק.
ה-compaction לא "טעה" — הוא עשה בדיוק מה שתוכנן: לסכם היסטוריה כדי לפנות מקום. הוא פשוט לא ידע שה-decision מסבב 8 חשוב יותר מ-14 רשומות enrichment. זה התפקיד שלך כבונה ה-harness: להגיד לו מה קריטי לפני שה-compaction מחליט בעצמו.
⚠️ מה בדיוק אובד ב-compaction — ולא רק "החלטה כלשהי"
כשאנשים שומעים "ה-compaction מחק decision" — הם מדמיינים משהו מעורפל. בפועל זה ספציפי מאוד. בתרחיש ה-lead-enrichment, ה-decision שאבד היה ה-constraint: "enterprise leads require human approval". זה לא רק "כלל כללי" — זה כלל שאמור לשנות את behavior הסוכן על כל lead מסוג enterprise. כך נראה ה-critical_state.json לפני שה-compaction מוחק אותו:
{
"goal": "enrich leads from github stars",
"decisions": [
{"t": 1718800200.1, "text": "enterprise leads require human approval"}
],
"deliverables": [],
"constraints": []
}
לאחר compaction אוטומטי, ה-SDK מסכם את ההיסטוריה הפעילה לטקסט כזה (דוגמה מייצגת): "The agent enriched 14 leads, tagged them by company size, and drafted email sequences. Next step: send outreach to the remaining leads." ה-constraint על enterprise leads? נעלם לחלוטין — הסיכום התמקד במה נעשה, לא בכיצד צריך להמשיך. וזה ה-gap שגורם לנזק.
🧭 Framework: מתי לתת ל-compaction אוטומטי לעבוד, ומתי לקחת שליטה
- אם הריצה קצרה (פחות מ-~10 סבבים) ולא מצטבר state קריטי שלא נשמר לדיסק → אז compaction אוטומטי בסדר; אל תסבך.
- אם הריצה ארוכה (20+ סבבים) או מצטברים decisions / deliverables שעוד לא persisted → אז חייבים threshold-backup מבוקר לפני סף ה-83.5%.
- אם tool כלשהו מחזיר פלט ענק (קובץ שלם, DB dump) → אז אל תכניס אותו ל-context בכלל; שמור ל-store חיצוני והחזר reference (נרחיב בהמשך).
- אם אתה לא יודע כמה context הריצה צורכת → אז עצור. אל תאופטם כלום לפני שמדדת (סעיף "מדידה לפני אופטימיזציה").
Token Accounting — למדוד context בזמן ריצה
אי אפשר לנהל מה שלא מודדים. השלב הראשון הוא להפסיק לרוץ עיוור: בכל turn, נשלוף מתגובת ה-SDK את ספירת ה-tokens האמיתית. תגובת המודל כוללת שדה usage עם המספרים: input_tokens, output_tokens, ולעיתים cache_read_input_tokens ו-cache_creation_input_tokens. אלה הנתונים שמהם נחשב את אחוז השימוש.
הקוד ממחיש את העיקרון; ודאו את שמות השדות מול ה-docs הרשמיים של ה-Claude Agent SDK לפני שמקבעים אותם בקוד production (ה-API surface השתנה ב-2026).
# context_monitor.py — מדידת שימוש בכל turn
CONTEXT_WINDOW = 200_000 # התאם לחלון של המודל שלך
OUTPUT_BUFFER = 33_000 # ה-Compaction Buffer (מקור: research)
COMPACT_AT = 0.835 # סף ה-compaction האוטומטי
BACKUP_AT = 0.75 # הסף שלנו — לפני ה-compaction
def usable_history_budget():
# מה שבאמת זמין ל-history, אחרי ניכוי ה-buffer
return CONTEXT_WINDOW - OUTPUT_BUFFER # = 167,000
def context_usage(response):
u = response.usage
used = u.input_tokens + u.output_tokens
pct_of_window = used / CONTEXT_WINDOW
pct_of_usable = used / usable_history_budget()
return {
"input": u.input_tokens,
"output": u.output_tokens,
"cache_read": getattr(u, "cache_read_input_tokens", 0),
"used": used,
"pct_window": round(pct_of_window * 100, 1),
"pct_usable": round(pct_of_usable * 100, 1),
"approaching_compaction": pct_of_window >= COMPACT_AT,
"should_backup": pct_of_window >= BACKUP_AT,
}
שים/י לב לשני האחוזים: pct_window מודד מול החלון המלא (כדי לדעת מתי ה-83.5% מתקרב), ו-pct_usable מודד מול ה-budget הזמין בפועל (כדי שלא תרמה את עצמך שיש לך יותר מקום ממה שיש).
עכשיו מחברים את זה ללולאה מפרק 2. בכל סבב, אחרי שקיבלנו תגובה מהמודל, נקרא ל-context_usage() ונדפיס שורת מצב. זו השכבה הראשונה: עיניים. בלי לשנות שום התנהגות — רק לראות.
# בתוך לולאת ה-agent מפרק 2
for turn in range(max_turns):
response = call_model(messages) # הקריאה הקיימת שלך
ctx = context_usage(response)
flag = "🟢"
if ctx["should_backup"]: flag = "🟡"
if ctx["approaching_compaction"]: flag = "🔴"
print(f"{flag} turn {turn}: {ctx['used']:,} tok "
f"({ctx['pct_window']}% window / {ctx['pct_usable']}% usable)")
# ... המשך הלולאה הקיים: parse tool_use, execute, append tool_result ...
שלושת הצבעים הם השפה שתשתמש בה לאורך כל הפרק: 🟢 בטוח, 🟡 הגיע זמן לגבות, 🔴 ה-compaction על הסף.
מעבר לאחוז מספרי, אפשר להפוך את השימוש ל-ויזואלי ומיידי — פס התקדמות ASCII שרואים בזרם הלוג ישירות:
def format_ctx_bar(pct: float, width: int = 10) -> str:
"""Returns an ASCII progress bar representing context usage.
Examples:
format_ctx_bar(11.3) -> '[█░░░░░░░░░] 11%'
format_ctx_bar(76.2) -> '[████████░░] 76%'
format_ctx_bar(83.7) -> '[████████░░] 84% ⚠'
"""
filled = int(pct / 100 * width)
bar = "█" * filled + "░" * (width - filled)
warn = " ⚠" if pct >= 83.5 else ""
return f"[{bar}] {int(pct)}%{warn}"
# דוגמת שימוש בלולאה:
# ctx = context_usage(response)
# print(f"turn {turn}: {format_ctx_bar(ctx['pct_window'])} "
# f"({ctx['used']:,} tok)")
פלט לדוגמה על ריצה אמיתית (מייצג):
turn 5: [█░░░░░░░░░] 11% (22,600 tok) turn 15: [████░░░░░░] 47% (95,400 tok) turn 25: [████████░░] 76% (152,400 tok) ← 🟡 backup triggered turn 26: [████████░░] 84% ⚠ (167,400 tok) ← 🔴 compaction!
הפס הזה הופך את ה-context usage לדבר ויזואלי ומיידי: במקום לפרש מספר מופשט כמו "76.2%", אתה רואה פס שמתקרב לקצה — ומרגיש מתי הריצה בסכנה. הוסף/י את format_ctx_bar לשורת המצב של הלולאה ותגלה/תגלי שאתה מתחיל לסגור tabs כשהפס מגיע לחמישה ריבועים.
⚡ עשה עכשיו (5 דקות)
הוסף/י את context_usage() ל-harness שלך, וחווט/חווטי את שורת המצב ללולאה. הרץ/הריצי משימה שגוזרת 5–6 סבבים וצפה/צפי במספרים עולים. אל תשנה/י עדיין שום התנהגות. המטרה היחידה: לראות בעיניים איך ה-context גדל סבב-אחר-סבב. רשום/רשמי את אחוז ה-pct_window בסבב האחרון — זו נקודת ההתחלה שלך.
תרגיל 1 — בנה את ה-context-monitor
🛠️ תרגיל 1: context-monitor שמדווח אחוז שימוש בכל turn
מטרה: רכיב ב-harness שמדפיס בכל turn את אחוז השימוש ב-context, עם דגל צבע, ומסמן מתי מתקרבים ל-83.5%.
שלבים:
- צור/צרי קובץ
context_monitor.pyעם הקבועים (CONTEXT_WINDOW,OUTPUT_BUFFER=33000,COMPACT_AT=0.835,BACKUP_AT=0.75) ועם הפונקציהcontext_usage(response). - חווט/חווטי אותה ללולאה מפרק 2: אחרי כל
call_model, חשב/י את ה-usage והדפס/הדפיסי שורת מצב עם דגל 🟢/🟡/🔴. - הרץ/הריצי משימה אמיתית שגוזרת לפחות 8 סבבים (למשל: "קרא 3 קבצים, סכם כל אחד, ואז כתוב סיכום-על מאוחד").
- אם ה-SDK שלך לא חושף
usageבפורמט הזה — בדוק/בדקי ב-docs מה השדה המקביל, או חשב/י אומדן:len(json.dumps(messages)) / 4כקירוב גס ל-tokens.
תוצר נראה: לוג של 8+ שורות, כל אחת עם turn number, מספר tokens, אחוז window, אחוז usable, ודגל צבע. שמור/שמרי אותו — נשתמש בו בתרגיל 2.
פלט צפוי לדוגמה (מייצג — המספרים שלך יהיו שונים לפי המשימה):
🟢 turn 1: 3,240 tok ( 1.6% window / 1.9% usable) 🟢 turn 4: 18,450 tok ( 9.2% window / 11.0% usable) 🟡 turn 9: 152,600 tok (76.3% window / 91.3% usable) 🔴 turn 10: 168,200 tok (84.1% window / 100.7% usable) ← compaction!
שים/י לב: ה-pct_usable בסבב 10 עובר 100% — זה מציאותי! זה אומר שנגמר ה-budget הזמין ל-history (167K), ורק ה-compaction מנע crash. זו בדיוק הנקודה שה-backup חייב להתרחש לפניה.
בדיקת הצלחה: אם בריצה של 8 סבבים האחוז עדיין ירוק לגמרי — הגדל/הגדילי את המשימה (יותר קבצים, tool results גדולים יותר) עד שתראה/י לפחות פעם אחת דגל 🟡.
Compaction אוטומטי מול מבוקר
עכשיו שאתה רואה את ה-context, השאלה הבאה היא: כשמתקרבים לסף — מי מחליט מה נשאר? יש שתי גישות, והן שונות מהותית:
| היבט | Compaction אוטומטי (ה-SDK) | Compaction מבוקר (אתה) |
|---|---|---|
| מתי נורה | אוטומטית ב-83.5% | אתה בוחר — למשל threshold-backup ב-75% |
| מה נשמר | סיכום כללי לפי כלל ה-SDK | בדיוק מה שסימנת כקריטי |
| מה אבד | לא ידוע מראש — lossy ועיוור | רק מה שאתה החלטת שאפשר לוותר עליו |
| שליטה | אפס — קופסה שחורה | מלאה |
| מאמץ | אפס — עובד מהקופסה | צריך לכתוב קוד |
החוכמה היא לא לבחור אחד מהם בלעדית, אלא לשלב: ה-compaction האוטומטי נשאר כרשת ביטחון של מוצא אחרון (אם משהו השתבש ועברנו 83.5% בכל זאת — שלא נקרוס). אבל אנחנו מקדימים אותו עם threshold-backup מבוקר ב-75%: עוד לפני שה-SDK נוגע בהיסטוריה, אנחנו כבר שמרנו את ה-state הקריטי ל-store חיצוני. כך, גם אם ה-compaction האוטומטי "ימחק" decision מההיסטוריה הפעילה — יש לנו עותק בטוח שאפשר להחזיר.
אתה לא מבטל את ה-compaction האוטומטי — אתה מקדים אותו. ה-backup ב-75% רץ לפני ה-compaction ב-83.5%, אז כשהדחיסה העיוורת קורית, כבר אין מה לאבד.
🧭 Framework: מה להזריק חזרה ל-context אחרי compaction
כשה-compaction האוטומטי נורה (🔴) וההיסטוריה נדחסת, ה-הודעת המשתמש הבאה שנשלחת למודל צריכה לכלול בלוק CRITICAL_STATE מובנה — כדי שהמודל "יזכור" את מה שנמחק:
- אם ה-critical_state.json קיים ומכיל decisions → אז הזרק אותו כולו (בפורמט קצר — ר'
critical_state_reminder()) כחלק מה-user message הבא. - אם יש deliverables חלקיים (קוד שנכתב, רשימה שנבנתה) → אז ציין את ה-references שלהם בהזרקה, לא את התוכן המלא.
- אם ה-compaction summary של ה-SDK כבר מכיל את ה-goal → אז אפשר לדלג על שורת ה-goal בהזרקה, אבל תמיד כלול decisions ו-constraints — הם לא מופיעים בסיכום האוטומטי.
- אם הוזרקה תזכורת — אפס את דגל
backed_up_this_phaseכדי שמחזור backup חדש יתחיל.
אסטרטגיית Context — מה תמיד-נשמר, מה מסוכם, מה נזרק
לפני שכותבים קוד, צריך החלטה: מה ב-context מקבל איזה יחס. כל פיסת מידע בהיסטוריה שייכת לאחת משלוש קטגוריות, וזה ה-deliverable השלישי שלך — טבלת אסטרטגיית context:
| קטגוריה | מה זה כולל | מה עושים | למה |
|---|---|---|---|
| תמיד-נשמר | ה-goal המקורי, decisions שהתקבלו, deliverables (גם חלקיים), constraints / policies | אף פעם לא נזרק; מגובה ל-store חיצוני ב-75% | איבוד שלהם = הסוכן שובר את המשימה או חוזר על עבודה |
| מסוכם (distillation) | סבבים ישנים של חקירה, הודעות ביניים, tool results שכבר עובדו | מוחלף בסיכום קצר ("בדקתי 12 endpoints, 3 ללא טסט: X, Y, Z") | הערך נשמר, הנפח קטן — אבל יש סיכון אם הסיכום מאבד פרט |
| נזרק ל-store חיצוני | tool results כבדים (קובץ שלם, DB dump, JSON של 50KB) | נשמר ל-scratchpad חיצוני; ב-context נשאר רק reference | אין סיבה שהחומר הגולמי יתפוס context — צריך רק את הכתובת אליו |
תמיד-נשמר: ארבעת הדברים שלעולם לא נזרקים
הקטגוריה הקריטית ביותר. ארבעה סוגי מידע שאם הם נמחקים — הריצה נשברת:
- ה-goal — המשימה המקורית. סוכן ששכח מה הוא אמור לעשות הוא חסר ערך.
- decisions — כל החלטה לוגית שהתקבלה במהלך הריצה ("enterprise → human approval"). אלה הכללים שהסוכן בנה לעצמו תוך כדי.
- deliverables — תוצרים, גם חלקיים. קוד שנכתב, רשימה שנבנתה, החלטות ביניים שכבר materialized.
- constraints — מגבלות ו-policies שחלות על הריצה. אלה לרוב יושבים ב-system prompt (שלא נדחס), אבל constraints שנוצרו דינמית צריכים שמירה מפורשת.
distillation: לסכם בלי לאבד את הקריטי
Distillation (זיקוק) הוא להחליף 20 הודעות ישנות בסיכום אחד קצר. זה חזק — אבל מסוכן אם נעשה עיוור. הכלל: סכם רק את ה"איך", שמור תמיד את ה"מה". אפשר לזקק את כל ה-steps שעשית כדי למצוא 3 endpoints ללא טסט — אבל את העובדה שמצאת בדיוק את X, Y, Z (deliverable) אסור לזקק. הסיכום מחליף את התהליך, לא את התוצאה.
הנה ההבחנה ב-before/after קונקרטי (דוגמה מייצגת):
| לפני distillation (8 הודעות, ~1,400 tok) | אחרי distillation (~90 tok) |
|---|---|
הסוכן הריץ list_files, קיבל 40 קבצים, הריץ grep על כל אחד, מצא 12 endpoints, בדק כל אחד מול תיקיית הטסטים, גילה ש-3 חסרים... |
"נסרקו 40 קבצים → 12 endpoints. 3 ללא טסט: /users/export, /billing/refund, /admin/purge (deliverable שמור)." |
שים/י לב מה נשמר ומה נזרק: התהליך (40 קבצים, ה-grep-ים, הבדיקות אחת-אחת) נזרק — אפשר לשחזר אותו אם צריך. התוצאה (שלושת ה-endpoints המדויקים) נשמרה מילה-במילה. אם הסיכום היה כותב "נמצאו 3 endpoints ללא טסט" בלי השמות — זה היה distillation גרוע: הוא איבד את ה-deliverable. הכלל המעשי: אחרי שכתבת סיכום, שאל את עצמך — "האם הסוכן יכול להמשיך לעבוד רק מהסיכום הזה?" אם הוא צריך לחזור לחומר המקורי כדי לדעת מה לעשות הלאה, הסיכום מחק משהו קריטי.
⚡ עשה עכשיו: תרגיל זיקוק ב-3 דקות
קח/י את ה-tool_result הגולמי הבא (5 שורות דמה), וכתוב/כתבי סיכום של שורה אחת שמחליף אותו ב-context:
[tool: apollo_enrich]
{"lead_id": "gh-4821", "endpoint": "/api/v2/people/search", "status": 200,
"company_size": "enterprise", "email": "tal@acme.com", "title": "VP Eng",
"linkedin": "linkedin.com/in/tal-acme", "github_stars": 1240,
"last_active": "2024-11-03"}
כתוב/כתבי את שורת הסיכום שלך כאן (בכתב יד / טיוטה): ___
בדיקה: האם הסיכום שלך מכיל את שם ה-endpoint (/api/v2/people/search)? את lead_id? את העובדה שזה enterprise? אם חסר אחד מהם — יש לך בדיוק את הבאג שה-compaction האוטומטי עושה: הוא "מבין את הנתונים" אבל מאבד את הפרטים שה-harness עוד יצטרך. סיכום טוב: "apollo_enrich(/api/v2/people/search) → gh-4821: tal@acme.com, VP Eng, enterprise, 1240 stars — נשמר בסבב 7."
⚡ עשה עכשיו (4 דקות)
קח/י קטע אמיתי מלוג של ריצה קודמת — רצף של 5–6 הודעות וtool results. כתוב/כתבי בעצמך סיכום של שתי שורות שמחליף אותן. ואז המבחן: סגור/סגרי את הקטע המקורי והסתכל/הסתכלי רק על הסיכום. האם הוא מכיל כל decision וכל deliverable מהקטע? אם פספסת אחד — תיקנת בדיוק את הבאג שה-compaction האוטומטי עושה כל הזמן.
⚠️ טעות נפוצה: להחזיר tool results ענקיים ישר ל-context
tool שמחזיר קובץ שלם, dump של DB, או JSON של 50KB — ואתה דוחף את כל זה ל-tool_result ומשם ל-context. קריאה אחת כזו יכולה לשרוף יותר context מ-20 סבבי שיחה. וגרוע מזה: היא מקרבת אותך לסף ה-83.5% בקפיצה אחת, ומפעילה compaction על כל ההיסטוריה הטובה שלך. הכלל: tool results כבדים נשמרים ל-store חיצוני, וב-context חוזר רק reference קצר ("התוצאה נשמרה ב-./scratch/db_dump_42.json, 1,204 שורות, עמודות: id, name, email").
Scratchpad — להוציא state כבד מה-context
ה-scratchpad (פנקס עבודה / working memory) הוא הפתרון לקטגוריה השלישית. במקום להחזיק חומר גולמי כבד ב-context, שומרים אותו ל-store חיצוני — קובץ, KV store, או דאטהבייס — ומחזירים ל-context רק reference: כתובת + מטא-דאטה קצר. הסוכן יודע שהמידע קיים ואיפה הוא, ויכול לבקש tool שיקרא חלק ממנו אם צריך — אבל החומר עצמו לא חונק את החלון.
# scratchpad.py — להוציא tool results כבדים מה-context
import json, hashlib, pathlib
SCRATCH = pathlib.Path("./scratch")
SCRATCH.mkdir(exist_ok=True)
THRESHOLD_CHARS = 4_000 # מעל זה — שומרים החוצה במקום ב-context
def maybe_offload(tool_name, result_text):
if len(result_text) <= THRESHOLD_CHARS:
return result_text # קטן — נשאר ב-context
key = hashlib.sha1(result_text.encode()).hexdigest()[:10]
path = SCRATCH / f"{tool_name}_{key}.txt"
path.write_text(result_text, encoding="utf-8")
lines = result_text.count("\n") + 1
# מחזירים ל-context רק reference קצר במקום 50KB
return (f"[OFFLOADED to {path} — {len(result_text):,} chars, "
f"{lines:,} lines. Use read_scratch('{path}', start, end) "
f"to read a slice.]")
הרעיון: maybe_offload עוטף כל tool_result לפני שהוא נכנס ל-context. אם הוא קטן — עובר כמו שהוא. אם הוא כבד — נשמר לדיסק וחוזר reference. נדרש גם tool משלים read_scratch(path, start, end) שמאפשר לסוכן לקרוא פלח אם הוא באמת צריך.
כשה-agent קיבל reference ל-offloaded file, הוא יכול לבקש פלח ספציפי דרך tool זה:
def read_scratch(path: str, start: int = 0, end: int = 50) -> str:
"""Read a slice of an offloaded scratchpad file.
Args:
path: Path as returned by maybe_offload() reference string.
Must be under SCRATCH directory (validated below).
start: First line to return (0-indexed, inclusive).
end: Last line to return (exclusive). Max 100 lines per call.
Returns:
The requested lines as a string, or an error message.
Security: path must resolve inside SCRATCH — never use user-controlled
data as path component. The sha1-based filenames from maybe_offload()
are safe; never replace them with user-supplied strings.
"""
p = pathlib.Path(path).resolve()
if not str(p).startswith(str(SCRATCH.resolve())):
return "[ERROR: path outside scratch directory — rejected]"
if end - start > 100:
end = start + 100 # enforce max slice size
lines = p.read_text(encoding="utf-8").splitlines()
slice_lines = lines[start:end]
return "\n".join(slice_lines) + f"\n[lines {start}–{end} of {len(lines)}]"
ה-read_scratch מתועד כ-tool בתוך ה-harness, כך שהמודל יכול לבקש "read_scratch('./scratch/apollo_enrichment_3a7f.txt', 0, 20)" ולקבל רק את 20 השורות הרלוונטיות — בלי לטעון 50KB לתוך ה-context. ה-max 100 lines per call מבטיח שגם tool זה עצמו לא יהפוך לבעיית context.
ה-offload הזה הוא טעימה ממה שפרק 4 ("Tools, MCP ו-Governance") מעמיק: tools עם structured output מחזירים מראש רק את מה שצריך, ו-MCP מאפשר לחבר store חיצוני אמיתי במקום קבצים ב-./scratch. כאן בנינו את העיקרון; שם נחזק אותו.
Threshold-Based Backup — הקוד שמציל את ה-state
זה הלב של הפרק. ה-threshold-backup הוא hook בלולאה: כשה-monitor מסמן 🟡 (חצינו 75%), אנחנו שומרים את כל ה"תמיד-נשמר" ל-store חיצוני — לפני שה-compaction האוטומטי של 83.5% נורה. גם אם הדחיסה העיוורת תמחק decision מההיסטוריה הפעילה, יש לנו עותק בטוח, ואנחנו יכולים להזריק אותו חזרה ל-context בסבב הבא כתזכורת.
# critical_state.py — מה תמיד-נשמר + גיבוי מבוסס-סף
import json, pathlib, time
BACKUP = pathlib.Path("./scratch/critical_state.json")
# "תמיד-נשמר" — מתעדכן לאורך הריצה
critical = {"goal": None, "decisions": [], "deliverables": [], "constraints": []}
def record_decision(text):
critical["decisions"].append({"t": time.time(), "text": text})
def record_deliverable(name, ref):
critical["deliverables"].append({"name": name, "ref": ref})
def backup_critical_state():
BACKUP.write_text(json.dumps(critical, ensure_ascii=False, indent=2),
encoding="utf-8")
return BACKUP
def critical_state_reminder():
# טקסט קצר להזרקה חזרה ל-context אחרי compaction
decisions = "; ".join(d["text"] for d in critical["decisions"])
delivs = "; ".join(f"{d['name']}->{d['ref']}" for d in critical["deliverables"])
return (f"[CRITICAL STATE] goal: {critical['goal']} | "
f"decisions: {decisions} | deliverables: {delivs}]")
# בתוך לולאת ה-agent
backed_up_this_phase = False
for turn in range(max_turns):
response = call_model(messages)
ctx = context_usage(response)
# 🟡 חצינו 75% — גבה state קריטי לפני ה-compaction של 83.5%
if ctx["should_backup"] and not backed_up_this_phase:
backup_critical_state()
backed_up_this_phase = True
print(f"🟡 backed up critical state at {ctx['pct_window']}%")
# 🔴 אחרי ש-compaction אוטומטי קרה — הזרק תזכורת חזרה
if ctx["approaching_compaction"]:
messages.append({"role": "user",
"content": critical_state_reminder()})
backed_up_this_phase = False # אפס לקראת מחזור compaction הבא
# ... המשך הלולאה: parse tool_use, execute, append tool_result ...
שים/י לב לדגל backed_up_this_phase: הוא מונע גיבוי כפול באותו "מחזור". אחרי שה-compaction נורה והזרקנו תזכורת, מאפסים אותו — כי מחזור חדש של מילוי context מתחיל.
⚠️ אבטחת נתיב קובץ ה-backup — לעולם לא עם input של המשתמש
שם הקובץ שאליו נכתב ה-scratchpad (ב-maybe_offload) נוצר מ-sha1 של תוכן ה-result — זו גישה בטוחה. אל תחליף/תחליפי את הגישה הזו בשם שנגזר מ-user input, למשל: f"{tool_name}_{user_query}.txt". שם כזה מאפשר path traversal attack: משתמש שמזין "../../../etc/crontab" כ-query יכול לגרום לקוד לכתוב קובץ ל-path שרירותי. ה-sha1 הוא deterministic ו-collision-resistant ואינו נשלט על ידי המשתמש — השאר אותו.
⚠️ זהירות: התזכורת עצמה צורכת context
הזרקת critical_state_reminder() חזרה ל-context היא חרב פיפיות — היא מוסיפה tokens. לכן היא חייבת להיות קצרה: רק ה-decisions וה-deliverables כ-references, לא החומר המלא. אם ה-reminder שלך מתחיל לתפוח, סימן שאתה מנסה לדחוף יותר מדי "תמיד-נשמר" — חזור לטבלת האסטרטגיה וצמצם מה באמת קריטי.
תרגיל 2 — בנה את ה-threshold-backup
🛠️ תרגיל 2: threshold-backup שמשמר state קריטי לפני compaction
מטרה: מנגנון שכשהשימוש חוצה 75%, שומר decisions ו-deliverables ל-critical_state.json, ויודע להזריק תזכורת חזרה.
שלבים:
- בנה/בני את
critical_state.py(ה-dict,record_decision,record_deliverable,backup_critical_state,critical_state_reminder). - בלולאה, קרא/קראי ל-
record_decisionבכל פעם שהסוכן מקבל החלטה לוגית (אפשר לזהות מילת-מפתח ב-output, או tool ייעודיnote_decision). - חווט/חווטי את ה-backup hook: ב-🟡 שמור, ב-🔴 הזרק תזכורת.
- הרץ/הריצי משימה ארוכה (15+ סבבים) שמכריחה לפחות גיבוי אחד. בדוק/בדקי שהקובץ
critical_state.jsonנכתב ומכיל את ה-decisions.
תוצר נראה: קובץ critical_state.json עם decisions ו-deliverables אמיתיים, ועוד שורת לוג "🟡 backed up critical state at 76.x%". פתח/י את הקובץ והראה/הראי שה-decision מהסבב המוקדם שרד את הריצה.
כך ייראה ה-critical_state.json לאחר שנכתב (דוגמה מייצגת):
{
"goal": "enrich leads from github stars",
"decisions": [
{
"t": 1718800200.1,
"text": "enterprise leads require human approval"
}
],
"deliverables": [],
"constraints": []
}
שים/י לב שה-t הוא Unix timestamp — מאפשר לדעת מתי ה-decision התקבל, שזה שימושי כשמנפים ריצה שנשברה ורוצים להבין אילו decisions נוצרו לפני ה-compaction ואילו אחריו.
בדיקת הצלחה (החזקה): הרץ/הריצי את אותה משימה בלי ה-backup, וגרום/גרמי ל-compaction (הזן tool results כבדים). הוכח/הוכיחי שהסוכן "שכח" decision מוקדם. ואז עם ה-backup — הוכח/הוכיחי שהוא זוכר. ההבדל הזה הוא כל הפרק.
Context Budget per Turn — כמה "מותר" ל-tool result אחד
ראינו שתוצאת tool ענקית מסוכנת. אבל איך גודרים את זה שיטתית? קובעים context budget per turn — תקרה לכמה context מותר לאובייקט בודד לצרוך. הכלל פשוט: כל tool_result שעובר את התקרה (למשל 4,000 tokens) חייב לעבור דרך ה-maybe_offload מהסעיף הקודם. כך אף קריאה בודדת לא יכולה להפיל את הריצה.
🧭 Framework: החלטת context budget ל-tool result
- אם ה-tool result קטן מהתקרה (≤ ~4K tokens) → אז מכניסים אותו ל-context כמו שהוא.
- אם ה-tool result גדול מהתקרה אבל הסוכן צריך את כולו → אז offload ל-scratchpad + reference + tool לקריאת פלחים.
- אם ה-tool result גדול אבל הסוכן צריך רק חלק (למשל 3 שדות מתוך JSON ענק) → אז הוסף שכבת עיבוד ב-tool עצמו שמחזירה רק את הרלוונטי (קישור לפרק 4 — structured output).
- אם אותו tool נקרא שוב עם אותו input → אז החזר מ-cache במקום לצרוך context מחדש (גם זה פרק 4).
אינטראקציה עם max-turns — שני הבלמים יחד
בפרק 2 הכרת את ה-max-turns כבלם הראשון נגד לולאות אינסופיות. עכשיו יש לך בלם שני: ניהול context. חשוב להבין איך הם עובדים יחד, כי הם מגינים מפני שתי סכנות שונות אבל קשורות:
| בלם | מפני מה מגן | איך |
|---|---|---|
| max-turns | לולאה שלא נגמרת — סוכן תקוע שחוזר על אותה פעולה | תקרה קשיחה על מספר הסבבים |
| context management | איבוד state — היסטוריה שנדחסת ונמחקת תחת לחץ | מדידה + backup + offload |
הקשר ביניהם: ריצה ארוכה יותר = יותר turns = יותר לחץ context. ככל שמעלים את max-turns כדי לאפשר משימות מורכבות, כך גדל הסיכוי שתפגוש את סף ה-compaction. לכן השניים לא עצמאיים: כשאתה מאשר ל-harness לרוץ 100 סבבים, אתה חייב ניהול context, אחרת ה-100 הסבבים האלה יגררו compaction עיוור באמצע. הכלל: max-turns גבוה ⟸⟹ ניהול context חזק. לא מעלים אחד בלי השני.
⚡ עשה עכשיו (3 דקות)
בדוק/בדקי ב-harness שלך: מה ה-max-turns הנוכחי? עכשיו שאל/י: אם הסוכן באמת ירוץ עד הסוף — האם ה-context-monitor יגיע ל-🔴? אם כן, ה-backup שלך חייב לעבוד. אם ה-max-turns נמוך מספיק שלעולם לא תגיע ל-🟡 — אולי אתה מגביל את הסוכן יותר מדי. רשום/רשמי את שתי המגבלות זו לצד זו.
מדידה לפני אופטימיזציה — אל תסבך לפני שבדקת
זה אולי הסעיף החשוב ביותר, וקל לפספס אותו בהתלהבות מהקוד. אל תבנה compaction strategy מתוחכמת לפני שמדדת כמה context הריצה הטיפוסית שלך באמת צורכת. הרבה בונים מסבכים את ה-harness עם distillation, offload ו-backup — בשביל משימות שבכלל לא מתקרבות ל-75%. זו הנדסת-יתר: קוד מורכב יותר, יותר באגים, אפס תועלת.
הסדר הנכון:
- מדוד — הרץ 3–5 משימות טיפוסיות עם ה-context-monitor (תרגיל 1). רשום את ה-
pct_windowהמקסימלי בכל אחת. - סווג — אם הריצות הטיפוסיות נשארות מתחת ל-50%, אתה לא צריך כלום מעבר ל-monitor. אם הן נוגעות ב-70%+, אתה צריך backup. אם tool בודד קופץ אותך ל-80%, אתה צריך offload.
- בנה רק את מה שהמדידה דורשת — לא יותר.
"Measure, then optimize." ה-context-monitor (תרגיל 1) הוא תמיד הצעד הראשון. ה-backup, ה-offload וה-distillation נכנסים רק כשהמדידה מראה שצריך אותם. בנייה הפוכה — אופטימיזציה לפני מדידה — היא בדיוק ה-bloat שהקורס הזה נלחם בו מפרק 1.
🧭 Framework: מתי אתה לא צריך threshold-backup
לא כל harness צריך את כל הרכיבים. הנה שלושה תרחישים שבהם compaction אוטומטי הוא בסדר גמור — ולא צריך backup מבוקר:
- אם הריצה קצרה (פחות מ-15 סבבים) ועל-פי המדידה לא מגיעה ל-75% → אז compaction אוטומטי הוא רשת ביטחון מספקת; אל תסבך.
- אם אין decisions דינמיים — כל הכללים מוגדרים מראש ב-system prompt ולא משתנים תוך כדי ריצה → אז ה-compaction יסכם היסטוריה אבל לא ימחק כלום קריטי (הכל כבר ב-system prompt שלא נדחס).
- אם כל ה-state (decisions, deliverables) נשמר לדיסק אחרי כל סבב — ב-pipeline אחר, לא רק בזמן backup → אז גם אם compaction מוחק מה-context, יש לך עותק מחוץ לו.
השורה התחתונה: threshold-backup פותר בעיה של decisions שנוצרים דינמית ולא נשמרים אחרת. אם הבעיה הזו לא קיימת אצלך — לא צריך אותו.
תרגיל 3 — טבלת אסטרטגיית ה-context שלך
🛠️ תרגיל 3: כתוב את טבלת אסטרטגיית ה-context למשימה אמיתית שלך
מטרה: מסמך החלטה שממפה כל סוג מידע במשימה האמיתית שלך לאחת משלוש הקטגוריות — תמיד-נשמר / מסוכם / נזרק.
שלבים:
- בחר/י משימה אמיתית שה-harness שלך ירוץ עליה (lead-enrichment, documentation agent, code-fixer — מה שרלוונטי לך).
- רשום/רשמי את כל סוגי המידע שיצטברו ב-context: ה-goal, decisions אפשריים, deliverables, tool results צפויים (ואילו מהם כבדים).
- שייך/שייכי כל אחד לקטגוריה, וכתוב/כתבי שורת נימוק אחת: "למה כאן ולא בקטגוריה אחרת".
- סמן/סמני את ה-tool results הכבדים שצריכים offload, ואת ה-decisions שחייבים record_decision.
תוצר נראה: טבלה (קובץ MD או גיליון) עם 3 עמודות — פריט / קטגוריה / נימוק — לפחות 8 שורות. זו לא תיאוריה: זו המפה שלפיה תחווט את ה-record_decision וה-offload בקוד שלך.
בדיקת הצלחה: עבור/עברי על הטבלה ושאל/י על כל שורה ב"תמיד-נשמר": "אם זה יימחק ב-compaction, האם הסוכן ישבור את המשימה?" אם התשובה "לא" — זה לא באמת תמיד-נשמר; הורד/הורידי אותו ל"מסוכם".
תרגיל 4 — מבחן עומס: שבור והצל
🛠️ תרגיל 4: מבחן עומס מקצה-לקצה — הוכח שה-context management עובד
מטרה: להוכיח, בריצה אחת מתועדת, שכל שלושת הרכיבים (monitor + backup + offload) עובדים יחד ומונעים איבוד state.
ערכת פתיחה (Starter Kit) — נדרשת לפני ההרצה: העתק/י לתיקיית הפרויקט את שני המודולים שכבר מופיעים בפרק: scratchpad.py (עם maybe_offload + read_scratch, סעיף "Scratchpad" למעלה) ו-critical_state.py (עם record_decision, record_deliverable, backup_critical_state, סעיף "Threshold-Based Backup"). בלעדיהם שתי הריצות (א' ו-ב') לא יוכלו להיערך.
שלבים:
- משימת stress מוכנה (copy-paste): goal = "מפה/י את כל קבצי ה-README של 3 ריפו (repo_a, repo_b, repo_c — 22 קבצים, ~58K תווים), זהה/י endpoints לא-מתועדים, ובסבב 5 קבל/י החלטה: docs באנגלית בלבד". הוסף/י tool
dump_routesשמחזירroutes.jsonבגודל ~12K תווים בסבב 12 — זו הקפיצה שדוחפת מעל 83.5%. - הרצה א' (baseline): בטל/י הגנה בשורה-אחת-כל-פעם: בלולאה החלף/י
if ctx["should_backup"]ב-if False, וב-scratchpad.pyהחלף/יTHRESHOLD_CHARS = 4_000ב-10_000_000. הרץ/הריצי. תעד/תעדי שהסוכן "שכח" את ה-decision מסבב 5 (חזר עליו / סתר אותו). - הרצה ב' (מוגן): החזר/י את שני השורות לערכן המקורי. וודא/י ש-
record_decision("docs באנגלית בלבד")נקרא בסבב 5 (למשל keyphrase: אם ה-output מכיל "החלטה:" או "decision:"). הרץ/הריצי את אותה משימה בדיוק. תעד/תעדי שהסוכן זכר את ה-decision והשלים נכון. - שמור/שמרי את שני הלוגים ואת
critical_state.jsonכהוכחה.
תוצר נראה: שני לוגים זה לצד זה — "בלי הגנה: שכח" מול "עם הגנה: זכר" — פלוס קובץ ה-state. זה ה-capstone של הפרק: הוכחה חיה שניהול ה-context שינה את תוצאת הריצה.
בדיקת הצלחה: אם ב-baseline הסוכן לא שכח — המשימה שלך לא מספיק כבדה; הגדל/הגדילי tool results עד ש-compaction באמת נורה (🔴 ב-monitor) וה-decision נמחק.
שגרת עבודה — איך לעבוד עם context בכל harness מעכשיו
🔄 שגרת ה-Context של ה-Harness
בכל פעם שאתה בונה או מרחיב harness שירוץ ריצות לא-טריוויאליות, עבור על השגרה הזו לפי הסדר:
- חווט monitor קודם. לפני כל אופטימיזציה — context-monitor שמדפיס אחוז שימוש בכל turn. רץ עיוור = מתכון לאסון.
- מדוד 3–5 ריצות טיפוסיות. רשום את ה-
pct_windowהמקסימלי. זה קובע מה אתה בכלל צריך לבנות. - הגדר את "תמיד-נשמר". goal, decisions, deliverables, constraints — ארבעת אלה מקבלים record מפורש.
- קבע context budget per turn. כל tool result מעל התקרה עובר offload ל-scratchpad.
- חווט threshold-backup ב-75%. מקדים את ה-compaction האוטומטי של 83.5%.
- בדוק עם stress test. baseline (שכח) מול מוגן (זכר). אם אין הבדל — או שהמשימה קלה מדי, או שה-backup לא עובד.
- סנכרן עם max-turns. העלית max-turns? חזק את ניהול ה-context. הם זוג, לא יחידים.
השגרה הזו תחזור איתך לכל פרק הבא: ב-ch6 כל subagent הוא session נפרד עם ה-context שלו, וב-ch7 ה-memory store בנוי על אותם עקרונות.
רוצה לבדוק שכל הרכיבים עובדים בלי לחכות לריצה אמיתית ארוכה? הוסף/י ל-harness שלך --context-stress flag: כשהוא פעיל, הלולאה מוסיפה dummy tool_result של 10,000 תווים בסבב 3. הציפייה: בסבב 4 אתה רואה דגל 🟡 (כי ה-10K characters קפצו אותך מעל 75%), וקובץ ה-backup נכתב לדיסק. אם הדגל לא מגיע — בדוק/בדקי שה-BACKUP_AT threshold מחובר נכון לפונקציית ה-backup. זה בדיקה ב-2 דקות שחוסכת הפתעות ב-production.
🎯 Just One Thing
אם תיקח/י דבר אחד מהפרק הזה, שיהיה זה: חלון ה-context שלך קטן ממה שאתה חושב, וה-compaction מוחק היסטוריה בלי לשאול. לכן — תמיד תמדוד לפני שתבנה, ותמיד תגבה את ה"תמיד-נשמר" ב-75%, לפני שה-SDK דוחס ב-83.5%. monitor → backup → רק אז אופטימיזציה. כל השאר בפרק הוא פרטים על המשפט הזה.
בדוק את עצמך
✅ 5 שאלות לבדיקה עצמית
- מספרים: חלון של 200K tokens — כמה זמין בפועל ל-history, ובאיזה אחוז שימוש נורה compaction אוטומטי? (רמז: נכֵּה את ה-buffer, ואז זכור את הסף.)
- סיבה: למה ה-compaction האוטומטי מסוכן יותר מסתם "איבוד מקום"? מה ההבדל בין "נגמר ה-context" ל"נמחק decision קריטי"?
- החלטה: tool מחזיר JSON של 60KB. מה אתה עושה איתו, ולמה לא לדחוף אותו ישר ל-
tool_result? - תזמון: למה ה-threshold-backup רץ ב-75% ולא ב-83.5%? מה היה קורה אם היינו מגבים בדיוק על הסף?
- אינטגרציה: העלית את
max-turnsמ-20 ל-80 כדי לאפשר משימה מורכבת. איזה רכיב חייב להתחזק יחד איתו, ולמה?
תשובות מקוצרות: (1) ~167K זמין (200K פחות 33K buffer); compaction ב-83.5% מהחלון המלא. (2) "נגמר context" עוצר ריצה בצורה גלויה; "נמחק decision" משאיר את הריצה רצה אבל שבורה — הסוכן ממשיך בלי הזיכרון ואתה מגלה מאוחר. (3) offload ל-scratchpad + reference; דחיפה ישירה שורפת context בקפיצה אחת ומפעילה compaction על כל ההיסטוריה הטובה. (4) כדי להקדים את הדחיסה העיוורת — גיבוי בדיוק על הסף עלול לרוץ אחרי שה-SDK כבר התחיל לדחוס. (5) ניהול context — יותר turns = יותר לחץ context; max-turns גבוה בלי ניהול context = compaction עיוור באמצע ריצה.
טעויות נפוצות — סיכום
⚠️ שלוש הטעויות שהורגות ניהול context
- להניח שכל החלון זמין ל-history. מתעלמים מ-33K ה-buffer, מתכננים מול 200K, והריצה נדחסת הרבה לפני — איבדת state בלי לדעת. הנגד: תכנן מול ה-history הזמין בפועל (חלון פחות buffer).
- לסמוך על compaction אוטומטי כברירת מחדל. הוא lossy ועיוור; decision קריטי נמחק באמצע ריצה ואתה מגלה רק כשהסוכן "שוכח". הנגד: threshold-backup מבוקר ב-75% שמקדים את הדחיסה.
- להחזיר tool results ענקיים ישר ל-context. קובץ שלם / DB dump ב-
tool_resultשורף את התקציב בקריאה אחת ומפעיל compaction על הכל. הנגד: offload ל-scratchpad + reference, עם context budget per turn.
סיכום הפרק וגשר לפרק הבא
📝 מה למדנו בפרק 3
- חלון ה-context קטן ממה שנדמה. ה-Claude Agent SDK שומר ~33K tokens כ-Compaction Buffer ל-output ול-safety — ה-history הזמין בפועל הוא החלון פחות ה-buffer.
- ה-Compaction Buffer Trap. compaction אוטומטי נורה ב-~83.5% מהחלון, והוא lossy ועיוור — הוא יכול למחוק decision קריטי או deliverable חלקי באמצע ריצה, והסוכן ממשיך לרוץ "שכוח".
- מדידה לפני הכל. ה-context-monitor (token accounting מתוך
usage) הוא הצעד הראשון — רואים את אחוז השימוש בכל turn לפני שמסבכים כל אופטימיזציה. - אסטרטגיית 3 קטגוריות. תמיד-נשמר (goal/decisions/deliverables/constraints) / מסוכם (distillation של ה"איך") / נזרק ל-scratchpad (tool results כבדים → reference).
- threshold-backup מבוקר. ב-75% — לפני ה-compaction של 83.5% — שומרים את ה"תמיד-נשמר" ל-store חיצוני ומזריקים תזכורת חזרה. כך הדחיסה העיוורת לא יכולה למחוק מה שכבר גובה.
- שני בלמים יחד. max-turns (נגד לולאות) וניהול context (נגד איבוד state) הם זוג — מעלים אחד, מחזקים את השני.
🧵 הגשר לפרק 4 — Tools, MCP ו-Governance
עכשיו ל-harness שלך יש עיניים (monitor), זיכרון בטוח (backup) ו-משמעת context (offload + budget). אבל שכבת ה-tools עצמה עדיין שברירית: ה-offload שבנינו הוא פתרון גס מבוסס-קבצים, וה-tool results נכנסים כטקסט חופשי בלי schema.
בפרק 4 נחזק בדיוק את זה: structured JSON output עם retry דטרמיניסטי (שגם מקטין את הצורך ב-offload, כי ה-tool מחזיר רק את הרלוונטי), חיבור מקורות חיצוניים אמיתיים דרך MCP (במקום קבצים ב-./scratch), ושערי governance דטרמיניסטיים (Faramesh/FPL) שמונעים פעולות הרסניות. ה-context budget per turn שהגדרנו כאן הופך שם ל-policy אכיף. נתראה בפרק הבא.
צ'קליסט הפרק
סמן/סמני כל פריט כשהשלמת אותו. הפרק לא "נגמר" עד שכל התיבות מסומנות:
- הבנתי שחלון ה-context הזמין ל-history הוא החלון פחות ~33K buffer, לא החלון המלא.
- אני יכול/ה להסביר מה זה Compaction Buffer Trap ולמה compaction אוטומטי הוא lossy ועיוור.
- בניתי
context_monitor.pyעםcontext_usage()שמחזיר input/output/אחוזים/דגלים. - חיווטתי את ה-monitor ללולאה מפרק 2 וראיתי דגלי 🟢/🟡/🔴 בריצה אמיתית.
- מדדתי 3–5 ריצות טיפוסיות ורשמתי את ה-
pct_windowהמקסימלי לפני שבניתי אופטימיזציה. - הגדרתי את ארבעת ה"תמיד-נשמר": goal, decisions, deliverables, constraints.
- כתבתי את טבלת אסטרטגיית ה-context (תמיד-נשמר / מסוכם / נזרק) למשימה אמיתית (תרגיל 3).
- בניתי
scratchpad.pyעםmaybe_offloadשמוציא tool results כבדים ל-store חיצוני ומחזיר reference. - קבעתי context budget per turn והגדרתי תקרה ל-tool result בודד.
- בניתי
critical_state.pyעם threshold-backup שרץ ב-75% (לפני 83.5%). - חיווטתי את ה-backup hook ללולאה: שמירה ב-🟡, הזרקת תזכורת ב-🔴.
- הרצתי stress test (תרגיל 4): baseline "שכח" מול מוגן "זכר" — ושמרתי את שני הלוגים.
- וידאתי ש-
critical_state.jsonנכתב ומכיל decisions שרדו את ה-compaction. - סנכרנתי את ה-
max-turnsעם ניהול ה-context — לא העליתי אחד בלי השני. - אני מוכן/ה לפרק 4: ה-context management הזה הוא הבסיס ל-structured tools, MCP ו-governance.