3 Skill-Building · פרק 3 מתוך 8

קיר ה-Context — לנצח את ה-Compaction Buffer ולא לאבד היסטוריה

בפרק הקודם בנית לולאת agent מפורשת שמנהלת state ומריצה tools. עכשיו נגלה את הקיר שמפיל אותה בריצה ארוכה: ה-context window נגמר הרבה לפני שחשבת, ה-SDK דוחס לך את ההיסטוריה בעיוורון, ואתה מגלה שהסוכן "שכח" החלטה קריטית רק כשכבר מאוחר. נלמד למדוד, לגדר, ולהשתלט על זה.

🧵 חוט הפרויקט

מאיפה באנו: בפרק 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.

🎯 מטרות הלמידה

בסוף הפרק הזה תוכל/י:

  1. למדוד את שימוש ה-context בזמן ריצה — input / output / cache / buffer — ולזהות בדיוק מתי הריצה מתקרבת לסף ה-compaction של 83.5%, לפני שהיא חוצה אותו.
  2. להסביר את ה-Compaction Buffer Trap — למה ה-Claude Agent SDK שומר באופן קשיח ~33K tokens ל-output ול-safety, ולמה זה אומר שה-history הזמין בפועל קטן ממה שנדמה.
  3. לממש backup מבוסס-threshold — hook בלולאה שכשהשימוש חוצה סף מקדים (למשל 75%), שומר state קריטי (decisions, deliverables) ל-store חיצוני לפני ש-compaction אוטומטי מוחק אותו.
  4. לעצב אסטרטגיית context management ל-harness: מה תמיד-נשמר, מה מסוכם (distillation), ומה נזרק ל-store חיצוני (scratchpad) — עם compaction מבוקר במקום אוטומטי-עיוור.

📋 דרישות קדם

📦 תוצרים (Deliverables)

בסוף הפרק יהיו לך שלושה רכיבים חדשים שמתחברים ישירות ל-harness.py מפרק 2:

  1. context-monitor — רכיב שמדווח בכל turn את אחוז השימוש ב-context (לפי input/output tokens מתגובת ה-SDK), ומסמן בצבע מתי מתקרבים ל-83.5%.
  2. threshold-backup — מנגנון שכשהשימוש חוצה סף מקדים (75% כברירת מחדל), משמר state קריטי — decisions, deliverables, constraints — ל-store חיצוני (קובץ JSON) לפני שה-compaction האוטומטי של 83.5% נורה.
  3. טבלת אסטרטגיית context — מסמך החלטה: מה תמיד-נשמר / מה מסוכם / מה נזרק ל-store חיצוני, עם נימוק לכל קטגוריה — כדי שה-compaction יהיה מבוקר, לא עיוור.

למה ריצה ארוכה נופלת מקיר שלא ראית

קונספטמוטיבציה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 הקודמים. הנה (דוגמה מייצגת) איך זה מרגיש בשטח:

המספרים האלה הם המחשה, לא מדידה מדויקת של מקרה ספציפי — אבל הצורה הזו חוזרת בכל harness שלא מנהל context. השקט בהתחלה הוא בדיוק מה שמשטה בך. כשהבעיה מתפרצת היא כבר עברה את נקודת האל-חזור באותה ריצה.

חשבון ה-tokens בזמן אמת — מה קורה בסבב 5 ובסבב 26

כדי שהדינמיקה תהיה ממשית ולא מופשטת, בוא נעשה את החשבון מספרית על דוגמה מייצגת של משימת lead-enrichment — שירוצו 30 סבבים (בחלון של 200K):

סבבsystem_prompthistorytool_resultsסך-הכל% חלוןסטטוס
52,400 tok8,200 tok12,000 tok22,60011.3%🟢 בטוח
152,400 tok28,000 tok65,000 tok95,40047.7%🟢 נוח, אבל גדל
252,400 tok42,000 tok108,000 tok152,40076.2%🟡 backup רץ
262,400 tok45,000 tok120,000 tok167,40083.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 שלי הוא ___".

~33K

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

למה ה-buffer הזה קיים בכלל? כי המודל חייב מקום ליצור את התשובה. אם תמלא את כל החלון ב-input, לא יישאר מקום ל-output — והקריאה תיכשל או תיחתך באמצע. ה-SDK מגן עליך מזה בכך שהוא שומר את ה-33K מראש. הבעיה היא לא שה-buffer קיים — הבעיה היא שרוב הבונים לא יודעים שהוא קיים, ומגלים אותו רק כשהריצה כבר איבדה היסטוריה.

החשבון בפועל — שלוש שורות שכל בונה harness חייב לדעת בעל-פה

בוא נעשה את החשבון על חלון של 200K tokens (התאם/י למודל שלך — חלקם 200K, חלקם יותר):

שלבחישובתוצאה
חלון מלאנתון200,000 tok
פחות Compaction Buffer200,000 − 33,000167,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%

קונספטסכנהCompaction

עכשiו שיש לך את מבנה החלון, בוא נראה מה קורה כשהוא מתמלא. ה-Claude Agent SDK לא מחכה שהחלון יתפוצץ. הוא מפעיל compaction אוטומטי כשהשימוש נוגע ב-~83.5% מהחלון (מקור: course.research.json → gotchas). ברגע הזה ה-SDK לוקח את ההיסטוריה, מסכם אותה — ומחליף הרבה הודעות בייצוג דחוס יותר. המטרה טובה: לפנות מקום כדי שהריצה תמשיך. הבעיה: אתה לא בחרת מה לשמר.

83.5%

סף השימוש בחלון שבו נורה 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:

ה-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 אוטומטי לעבוד, ומתי לקחת שליטה

Token Accounting — למדוד context בזמן ריצה

מעשימדידהקוד

אי אפשר לנהל מה שלא מודדים. השלב הראשון הוא להפסיק לרוץ עיוור: בכל turn, נשלוף מתגובת ה-SDK את ספירת ה-tokens האמיתית. תגובת המודל כוללת שדה usage עם המספרים: input_tokens, output_tokens, ולעיתים cache_read_input_tokens ו-cache_creation_input_tokens. אלה הנתונים שמהם נחשב את אחוז השימוש.

דוגמה מייצגת — context-monitor בסיסי (Python)

הקוד ממחיש את העיקרון; ודאו את שמות השדות מול ה-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() ונדפיס שורת מצב. זו השכבה הראשונה: עיניים. בלי לשנות שום התנהגות — רק לראות.

דוגמה מייצגת — חיווט ה-monitor ללולאה
# בתוך לולאת ה-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 על הסף.

דוגמה מייצגת — פונקציית progress bar ויזואלי

מעבר לאחוז מספרי, אפשר להפוך את השימוש ל-ויזואלי ומיידי — פס התקדמות 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%.

שלבים:

  1. צור/צרי קובץ context_monitor.py עם הקבועים (CONTEXT_WINDOW, OUTPUT_BUFFER=33000, COMPACT_AT=0.835, BACKUP_AT=0.75) ועם הפונקציה context_usage(response).
  2. חווט/חווטי אותה ללולאה מפרק 2: אחרי כל call_model, חשב/י את ה-usage והדפס/הדפיסי שורת מצב עם דגל 🟢/🟡/🔴.
  3. הרץ/הריצי משימה אמיתית שגוזרת לפחות 8 סבבים (למשל: "קרא 3 קבצים, סכם כל אחד, ואז כתוב סיכום-על מאוחד").
  4. אם ה-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 מובנה — כדי שהמודל "יזכור" את מה שנמחק:

אסטרטגיית 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 — צריך רק את הכתובת אליו

תמיד-נשמר: ארבעת הדברים שלעולם לא נזרקים

הקטגוריה הקריטית ביותר. ארבעה סוגי מידע שאם הם נמחקים — הריצה נשברת:

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 פשוט מבוסס-קובץ
# 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) שמאפשר לסוכן לקרוא פלח אם הוא באמת צריך.

דוגמה מייצגת — read_scratch: קריאת פלח מה-scratchpad

כשה-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.

💡 קישור קדימה לפרק 4

ה-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 בסבב הבא כתזכורת.

דוגמה מייצגת — threshold-backup hook
# 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}]")
דוגמה מייצגת — חיווט ה-backup ללולאה
# בתוך לולאת ה-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, ויודע להזריק תזכורת חזרה.

שלבים:

  1. בנה/בני את critical_state.py (ה-dict, record_decision, record_deliverable, backup_critical_state, critical_state_reminder).
  2. בלולאה, קרא/קראי ל-record_decision בכל פעם שהסוכן מקבל החלטה לוגית (אפשר לזהות מילת-מפתח ב-output, או tool ייעודי note_decision).
  3. חווט/חווטי את ה-backup hook: ב-🟡 שמור, ב-🔴 הזרק תזכורת.
  4. הרץ/הריצי משימה ארוכה (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

אינטראקציה עם 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%. זו הנדסת-יתר: קוד מורכב יותר, יותר באגים, אפס תועלת.

הסדר הנכון:

  1. מדוד — הרץ 3–5 משימות טיפוסיות עם ה-context-monitor (תרגיל 1). רשום את ה-pct_window המקסימלי בכל אחת.
  2. סווג — אם הריצות הטיפוסיות נשארות מתחת ל-50%, אתה לא צריך כלום מעבר ל-monitor. אם הן נוגעות ב-70%+, אתה צריך backup. אם tool בודד קופץ אותך ל-80%, אתה צריך offload.
  3. בנה רק את מה שהמדידה דורשת — לא יותר.
💡 הכלל המקצועי

"Measure, then optimize." ה-context-monitor (תרגיל 1) הוא תמיד הצעד הראשון. ה-backup, ה-offload וה-distillation נכנסים רק כשהמדידה מראה שצריך אותם. בנייה הפוכה — אופטימיזציה לפני מדידה — היא בדיוק ה-bloat שהקורס הזה נלחם בו מפרק 1.

🧭 Framework: מתי אתה לא צריך threshold-backup

לא כל harness צריך את כל הרכיבים. הנה שלושה תרחישים שבהם compaction אוטומטי הוא בסדר גמור — ולא צריך backup מבוקר:

השורה התחתונה: threshold-backup פותר בעיה של decisions שנוצרים דינמית ולא נשמרים אחרת. אם הבעיה הזו לא קיימת אצלך — לא צריך אותו.

תרגיל 3 — טבלת אסטרטגיית ה-context שלך

תרגילתכנוןתוצר נראה

🛠️ תרגיל 3: כתוב את טבלת אסטרטגיית ה-context למשימה אמיתית שלך

מטרה: מסמך החלטה שממפה כל סוג מידע במשימה האמיתית שלך לאחת משלוש הקטגוריות — תמיד-נשמר / מסוכם / נזרק.

שלבים:

  1. בחר/י משימה אמיתית שה-harness שלך ירוץ עליה (lead-enrichment, documentation agent, code-fixer — מה שרלוונטי לך).
  2. רשום/רשמי את כל סוגי המידע שיצטברו ב-context: ה-goal, decisions אפשריים, deliverables, tool results צפויים (ואילו מהם כבדים).
  3. שייך/שייכי כל אחד לקטגוריה, וכתוב/כתבי שורת נימוק אחת: "למה כאן ולא בקטגוריה אחרת".
  4. סמן/סמני את ה-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"). בלעדיהם שתי הריצות (א' ו-ב') לא יוכלו להיערך.

שלבים:

  1. משימת 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%.
  2. הרצה א' (baseline): בטל/י הגנה בשורה-אחת-כל-פעם: בלולאה החלף/י if ctx["should_backup"] ב-if False, וב-scratchpad.py החלף/י THRESHOLD_CHARS = 4_000 ב-10_000_000. הרץ/הריצי. תעד/תעדי שהסוכן "שכח" את ה-decision מסבב 5 (חזר עליו / סתר אותו).
  3. הרצה ב' (מוגן): החזר/י את שני השורות לערכן המקורי. וודא/י ש-record_decision("docs באנגלית בלבד") נקרא בסבב 5 (למשל keyphrase: אם ה-output מכיל "החלטה:" או "decision:"). הרץ/הריצי את אותה משימה בדיוק. תעד/תעדי שהסוכן זכר את ה-decision והשלים נכון.
  4. שמור/שמרי את שני הלוגים ואת critical_state.json כהוכחה.

תוצר נראה: שני לוגים זה לצד זה — "בלי הגנה: שכח" מול "עם הגנה: זכר" — פלוס קובץ ה-state. זה ה-capstone של הפרק: הוכחה חיה שניהול ה-context שינה את תוצאת הריצה.

בדיקת הצלחה: אם ב-baseline הסוכן לא שכח — המשימה שלך לא מספיק כבדה; הגדל/הגדילי tool results עד ש-compaction באמת נורה (🔴 ב-monitor) וה-decision נמחק.

שגרת עבודה — איך לעבוד עם context בכל harness מעכשיו

שגרההרגל

🔄 שגרת ה-Context של ה-Harness

בכל פעם שאתה בונה או מרחיב harness שירוץ ריצות לא-טריוויאליות, עבור על השגרה הזו לפי הסדר:

  1. חווט monitor קודם. לפני כל אופטימיזציה — context-monitor שמדפיס אחוז שימוש בכל turn. רץ עיוור = מתכון לאסון.
  2. מדוד 3–5 ריצות טיפוסיות. רשום את ה-pct_window המקסימלי. זה קובע מה אתה בכלל צריך לבנות.
  3. הגדר את "תמיד-נשמר". goal, decisions, deliverables, constraints — ארבעת אלה מקבלים record מפורש.
  4. קבע context budget per turn. כל tool result מעל התקרה עובר offload ל-scratchpad.
  5. חווט threshold-backup ב-75%. מקדים את ה-compaction האוטומטי של 83.5%.
  6. בדוק עם stress test. baseline (שכח) מול מוגן (זכר). אם אין הבדל — או שהמשימה קלה מדי, או שה-backup לא עובד.
  7. סנכרן עם max-turns. העלית max-turns? חזק את ניהול ה-context. הם זוג, לא יחידים.

השגרה הזו תחזור איתך לכל פרק הבא: ב-ch6 כל subagent הוא session נפרד עם ה-context שלו, וב-ch7 ה-memory store בנוי על אותם עקרונות.

💡 Checklist quick-test: ה-context-stress flag

רוצה לבדוק שכל הרכיבים עובדים בלי לחכות לריצה אמיתית ארוכה? הוסף/י ל-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 שאלות לבדיקה עצמית

  1. מספרים: חלון של 200K tokens — כמה זמין בפועל ל-history, ובאיזה אחוז שימוש נורה compaction אוטומטי? (רמז: נכֵּה את ה-buffer, ואז זכור את הסף.)
  2. סיבה: למה ה-compaction האוטומטי מסוכן יותר מסתם "איבוד מקום"? מה ההבדל בין "נגמר ה-context" ל"נמחק decision קריטי"?
  3. החלטה: tool מחזיר JSON של 60KB. מה אתה עושה איתו, ולמה לא לדחוף אותו ישר ל-tool_result?
  4. תזמון: למה ה-threshold-backup רץ ב-75% ולא ב-83.5%? מה היה קורה אם היינו מגבים בדיוק על הסף?
  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

  1. להניח שכל החלון זמין ל-history. מתעלמים מ-33K ה-buffer, מתכננים מול 200K, והריצה נדחסת הרבה לפני — איבדת state בלי לדעת. הנגד: תכנן מול ה-history הזמין בפועל (חלון פחות buffer).
  2. לסמוך על compaction אוטומטי כברירת מחדל. הוא lossy ועיוור; decision קריטי נמחק באמצע ריצה ואתה מגלה רק כשהסוכן "שוכח". הנגד: threshold-backup מבוקר ב-75% שמקדים את הדחיסה.
  3. להחזיר tool results ענקיים ישר ל-context. קובץ שלם / DB dump ב-tool_result שורף את התקציב בקריאה אחת ומפעיל compaction על הכל. הנגד: offload ל-scratchpad + reference, עם context budget per turn.

סיכום הפרק וגשר לפרק הבא

סיכוםגשר

📝 מה למדנו בפרק 3

  1. חלון ה-context קטן ממה שנדמה. ה-Claude Agent SDK שומר ~33K tokens כ-Compaction Buffer ל-output ול-safety — ה-history הזמין בפועל הוא החלון פחות ה-buffer.
  2. ה-Compaction Buffer Trap. compaction אוטומטי נורה ב-~83.5% מהחלון, והוא lossy ועיוור — הוא יכול למחוק decision קריטי או deliverable חלקי באמצע ריצה, והסוכן ממשיך לרוץ "שכוח".
  3. מדידה לפני הכל. ה-context-monitor (token accounting מתוך usage) הוא הצעד הראשון — רואים את אחוז השימוש בכל turn לפני שמסבכים כל אופטימיזציה.
  4. אסטרטגיית 3 קטגוריות. תמיד-נשמר (goal/decisions/deliverables/constraints) / מסוכם (distillation של ה"איך") / נזרק ל-scratchpad (tool results כבדים → reference).
  5. threshold-backup מבוקר. ב-75% — לפני ה-compaction של 83.5% — שומרים את ה"תמיד-נשמר" ל-store חיצוני ומזריקים תזכורת חזרה. כך הדחיסה העיוורת לא יכולה למחוק מה שכבר גובה.
  6. שני בלמים יחד. 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 אכיף. נתראה בפרק הבא.

צ'קליסט הפרק

צ'קליסטסיום

סמן/סמני כל פריט כשהשלמת אותו. הפרק לא "נגמר" עד שכל התיבות מסומנות: