🧵 חוט הפרויקט
קודם (פרק 1 — "עליית ה-Harness Engineering"): פירקת את ה-harness ל-5 רכיביו ומיפית את נוף הכלים. עכשיו אתה יודע ש-loop, state, tool execution, context ו-constraints הם שכבות נפרדות שאתה בונה.
כאן (פרק 2): בונים את הרכיב הראשון והגרעיני — ה-loop. לולאת agent מפורשת עם state ו-tool execution. זה ה-skeleton שכל שאר הפרקים יוסיפו אליו שכבות.
אחר כך (פרק 3 — "קיר ה-Context"): הלולאה שתבנה כאן תקרוס כשה-context יתמלא. בפרק הבא נלמד את ה-Compaction Buffer ונבנה ניהול context שמונע איבוד היסטוריה באמצע ריצה.
מה תדע לעשות בסוף הפרק
- להקים סביבת Claude Agent SDK (התקנה, API key auth, hello-agent) ולהריץ task ראשון מקצה לקצה.
- לכתוב לולאת agent מפורשת מאפס: receive goal → call model → parse tool calls → execute → feed results → repeat-until-stop, עם max-turns limit.
- לנהל את ה-message state בין iterations (system, user, assistant, tool_result) ולהסביר מה נכנס לכל סבב ומה לא.
- להפעיל את ה-built-in tools (file, bash, search) דרך ה-SDK, ולחבר tool מותאם ראשון שהסוכן קורא לו בריצה אמיתית.
- לזהות את שלושת תנאי העצירה של הלולאה ולהבחין בין "תשובה סופית" ל"עצירה כפויה" — ולמה ההבחנה הזו קריטית לקוד במעלה הזרם.
- לבנות seam בין הלולאה לקריאה למודל — נקודת ההזרקה שכל השכבות הבאות (context, governance, tracing) ישתמשו בה.
מה צריך לפני שמתחילים
- פרק 1 של הקורס — הבנת הפרדיגמה ו-5 רכיבי ה-harness.
- הבנה מעשית מה זה agent ולולאת agent (ברמת ai-agents-guide ch13 — multi-agent patterns עם SDKs מוכנים).
- נוחות ב-Terminal: הרצת פקודות, ניווט, עריכת קבצים, וניהול env vars (משתני סביבה).
- יכולת לקרוא ולכתוב Python בסיסי (הדוגמאות כאן ב-Python; ה-SDK קיים גם ב-TypeScript).
- חשבון Anthropic עם API key — נדרש כבר בפרק הזה. חשוב מ-15 ביוני 2026 חלות מגבלות שונות על תוכניות subscription; נסביר בהמשך למה לאוטומציה צריך API key.
מה תייצר בפרק הזה (Deliverables)
- סביבת Claude Agent SDK עובדת + hello-agent — מריץ task ומחזיר תוצאה אמיתית מהמודל.
harness.py— לולאת agent מפורשת בפחות מ-150 שורות: goal → loop(call model → run tools → feed back) → stop, עם max-turns.- tool מותאם ראשון מחובר ל-harness (למשל
get_time/read_file) שהסוכן קורא לו בריצה אמיתית — ואתה רואה את ה-tool_use וה-tool_result עוברים בלולאה. - seam מוגדר בין הלולאה לקריאה למודל (
call_model), שישמש כנקודת הזרקה בפרקים 3-6.
מה אנחנו בונים, ולמה מאפס
מטרת הפרק: תוך 90 דקות תכתוב לולאת agent מפורשת בפחות מ-150 שורות, תחבר אליה 2 tools מותאמים (get_time, read_file), תטפל ב-3 תנאי עצירה (תשובה סופית / tool_use / max-turns), ותריץ אותה על goal אמיתי: "מה השעה עכשיו, ומה כתוב בקובץ ./notes.txt?"
בפרק 1 ראית את הטענה היבשה: LangChain בנו מחדש את ה-harness בלבד — אותו מודל, ללא אימון מחדש — וקפצו ב-Terminal-Bench 2.0 מ-52.8% ל-66.5%. 14 נקודות, רק מהתוכנה שעוטפת את המודל. הטענה הגדולה של הקורס היא ש-~70% מהביצועים חיים מחוץ למודל. עכשיו הגיע הרגע לגעת בלב של אותם 70%: הלולאה.
רוב המפתחים שכבר הריצו agent דרך SDK מוכן חיים בתחושה מטעה. הם כתבו משהו כמו query("תקן את הבאג הזה"), זה רץ, וזה עבד. אבל הלולאה עצמה — מה קורה בין השליחה לתשובה — היא קופסה שחורה עבורם. ולכן כשהסוכן נתקע ב-6 קריאות על אותו tool, או כשה-context נמחק באמצע ריצה, או כש-subagent אחד שורף את כל ה-tokens — אין להם איפה לפתוח את המכסה. הפרק הזה פותח את המכסה.
נבנה את הלולאה בשתי גרסאות, בכוונה:
- גרסה א' — הלולאה הגלויה (raw): נכתוב את הלולאה ידנית מעל ה-Messages API של Anthropic. כאן רואים כל חוליה: tool_use, tool_result, צבירת ה-state. זו לא דרך מסובכת יותר — זו הדרך לראות מה באמת קורה.
- גרסה ב' — הלולאה דרך ה-SDK: נראה איך ה-
query()generator של ה-Claude Agent SDK עושה את אותו דבר בשבילך, מתי כדאי להישען עליו, ומתי בכל זאת רוצים את הלולאה המפורשת כדי לשלוט בה.
אגב המילה "דק" בכותרת — היא לא מקרית, והיא לא ניגוד ל"מקצועי". thin harness פירושו harness שמכיל בדיוק את מה שאתה צריך ולא יותר: לולאה שאתה מבין, tools שאתה הגדרת, state שאתה רואה. ההפך מ-thin הוא heavy framework — שכבת abstraction שמבטיחה "לעשות הכל" ובתמורה מסתירה ממך את הלולאה, מנפחת את התלויות, וכשמשהו משתבש מותירה אותך עיוור. בפרק 1 ראינו את שלוש הטעויות שהורגות harness ביתי, וכולן נובעות מאותו שורש: לוותר על השליטה בלולאה. הפרק הזה הוא ה-antidote — אנחנו לוקחים את השליטה בחזרה, שורה אחר שורה.
נקודה חשובה על ה-scope: אנחנו לא בונים מודל, ולא משכפלים את ה-API. ה-Messages API וה-Claude Agent SDK עושים את העבודה הכבדה — שליחת הבקשה, קבלת התשובה, פורמט ה-tool calls. מה שאנחנו בונים זה את התזמורת סביבם: מי קורא למודל, מתי, עם איזה state, מה עושים עם ה-tool calls, ומתי עוצרים. זו בדיוק ההגדרה של agent harness מפרק 1 — השכבה שעוטפת את ה-LLM ומנהלת state, tool execution, context ו-constraints. בפרק הזה אנחנו בונים את שני הראשונים (state + tool execution); השניים האחרים מגיעים בפרקים 3 ו-4.
התקנה, auth, ו-hello-agent ראשון
נתחיל מהבסיס: סביבה עובדת. ה-Claude Agent SDK (לשעבר Claude Code SDK) הוא הספרייה הרשמית של Anthropic ב-Python וב-TypeScript שחושפת את לולאת ה-agent, ניהול ה-context, ותשתית הרצת ה-tools. הספרייה עצמה open-source וחינמית; אתה משלם רק על ה-tokens שהמודל צורך, בתעריפי ה-API הרגילים מקור: course.research.json.
שלב 1 — סביבה וירטואלית והתקנה
# צור תיקיית פרויקט נקייה
mkdir thin-harness && cd thin-harness
# סביבה וירטואלית — שומרת על ה-venv דק (זוכרים את ה-bloat מעל 1GB מפרק 1?)
python3 -m venv .venv
source .venv/bin/activate # ב-Windows: .venv\Scripts\activate
# שתי הספריות שנשתמש בהן
pip install anthropic # ה-Messages API — ללולאה הגלויה (גרסה א')
pip install claude-agent-sdk # ה-SDK הרשמי — ללולאה דרך ה-SDK (גרסה ב')
שתי הספריות, ביחד, תופסות עשרות מגה-בייט בודדים — לא ג'יגה. זו בדיוק הנקודה של thin harness: אנחנו לא גוררים framework כבד עם מאות תלויות. שתי החבילות האלה הן כל מה שצריך כדי לבנות harness שלם בקורס הזה.
טעות נפוצה: לקפוץ ישר ל-framework כבד
המפתה הראשון הוא pip install crewai או שכבות ה-abstraction הכבדות של LangChain "כי הן עושות הכל". המחיר: venv שמתנפח מעל 1GB, אפס observability, וסוכן שלולף 6+ פעמים על אותו tool בלי שתדע איפה לעצור אותו course.research.json — beginner_mistakes. אנחנו בונים את הלולאה בעצמנו בדיוק כדי לראות כל קריאה ולשלוט בה.
שלב 2 — API key auth (ולא subscription)
ה-auth הנכון לריצות אוטומטיות הוא API key — מפתח שמייצרים ב-Anthropic Console. שמור אותו ב-env var ולא בקוד:
# הגדרת ה-API key כמשתנה סביבה (לא בקוד! לא ב-git!)
export ANTHROPIC_API_KEY="sk-ant-..."
# בדיקה שזה נטען
echo $ANTHROPIC_API_KEY | head -c 12 # אמור להדפיס: sk-ant-...
# (אופציונלי) הוסף ל-~/.zshrc או ~/.bashrc כדי שייטען אוטומטית בכל טרמינל חדש
echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.zshrc
מסגרת החלטה: API key מול subscription auth
אם אתה מריץ ריצה ידנית, חד-פעמית, אינטראקטיבית בטרמינל שלך → אז subscription auth (התחברות עם חשבון Claude) מספיק.
אם אתה בונה harness שרץ אוטומטית — בלולאה, ב-cron, ב-CI, או כל דבר שמריץ tasks בלי שאתה יושב מולו → אז חובה API key auth.
למה זה קריטי דווקא עכשיו: מ-15 ביוני 2026, תוכניות subscription של Claude עוברות ל-Agent SDK credit חודשי ייעודי שמפריד בין שימוש אינטראקטיבי לאוטומציה course.research.json — key_2026_updates. ריצה אוטומטית על subscription auth תחטוף rate limits פתאומיים באמצע — והסוכן שלך ייעצר בלי הסבר ברור. כל ה-harness בקורס הזה מניח API key auth.
שלב 3 — hello-agent: ה-task הראשון מקצה לקצה
לפני שבונים לולאה, נוודא שה-pipe עובד: בקשה אחת למודל, תשובה אחת. זו הגרסה הכי פשוטה — קריאה בודדת ל-Messages API, בלי tools ובלי לולאה:
import os
from anthropic import Anthropic
client = Anthropic() # קורא אוטומטית את ANTHROPIC_API_KEY מהסביבה
resp = client.messages.create(
model="claude-sonnet-4-6", # מודל עדכני ויציב (2026)
max_tokens=512,
system="אתה עוזר תמציתי. ענה בעברית, במשפט אחד.",
messages=[
{"role": "user", "content": "מהי לולאת agent, בקצרה?"}
],
)
# ה-content הוא רשימה של blocks; הבלוק הראשון הוא טקסט
print(resp.content[0].text)
print("---")
print(f"input_tokens={resp.usage.input_tokens} output_tokens={resp.usage.output_tokens}")
הרצה: python hello_agent.py. הפלט נראה בערך כך:
לולאת agent היא מחזור שבו המודל מבקש פעולות, ה-harness מריץ אותן ומחזיר תוצאות, עד שהמשימה הושלמה.
---
input_tokens=34 output_tokens=29
שים לב לשורה האחרונה. כל תגובה מה-API מחזירה usage עם input_tokens ו-output_tokens. זו הנקודה שבה מתחיל ה-token accounting — בפרק 3 נשען על המספרים האלה כדי לדעת מתי ה-context מתקרב ל-compaction. כבר עכשיו, הרגל את העין לקרוא אותם בכל ריצה.
⚡ Do Now
הרץ את hello_agent.py שלך עכשיו. אם קיבלת AuthenticationError — ה-env var לא נטען (הרצת export בטרמינל אחר?). אם קיבלת NotFoundError על שם המודל — בדוק את שם המודל מול ה-docs הרשמיים; שמות מודלים זזים. רשום לעצמך כמה output_tokens קיבלת — תשווה בהמשך.
⚡ Do Now — שיחת המשך
שנה את hello_agent.py לשיחה דו-סבבית: שמור את resp.content בתוך messages בתור assistant, הוסף הודעת user חדשה ("תן דוגמה קצרה"), ושלח שוב. תראה שהמודל "ממשיך" את השיחה — ועכשיו אתה יודע למה: אתה החזקת את ה-state, לא הוא.
המודל הוא stateless — ה-harness הוא ה-state
זו התובנה היחידה שאם תפנים אותה, כל השאר מתבהר. המודל לא זוכר כלום בין קריאות. כל messages.create() הוא קריאה עצמאית לחלוטין. אין "שיחה" שנשמרת בצד של Anthropic בין הקריאות שלך. אם אתה רוצה שהמודל "יזכור" מה קרה בסבב הקודם — אתה חייב לשלוח לו מחדש את כל ההיסטוריה בכל קריאה.
במילים אחרות: המודל stateless, וה-harness הוא ה-state. ה"זיכרון" של הסוכן הוא לא תכונה של המודל — הוא מבנה נתונים אצלך, רשימת ה-messages שאתה צובר ושולח שוב ושוב. זו הסיבה שניהול ה-state והניהול של ה-context (פרק 3) הם הליבה של הנדסת ה-harness, לא תוספת.
זה נשמע טכני אבל יש לזה השלכה מעשית עצומה, ושווה לעצור עליה. כשהשתמשת ב-agent SDK מוכן ושאלת אותו שאלת המשך, נדמה היה שהוא "זוכר" את השיחה. הוא לא. מה שקרה הוא שה-SDK, מאחורי הקלעים, שמר את כל ההיסטוריה ושלח אותה מחדש — בדיוק כמו שאנחנו עומדים לעשות ידנית. ה"זיכרון" שחווית היה תמיד עבודה של ה-harness, לא של המודל. ברגע שאתה מפנים את זה, שתי שאלות שנראו מסתוריות הופכות לטריוויאליות: "למה הסוכן שכח מה הוא עשה לפני 30 סבבים?" — כי ה-harness דחס או זרק את ההיסטוריה הזו (פרק 3). "למה כל ריצה מתחילה מאפס?" — כי ה-state חי רק בתוך הריצה; אין store קבוע שמחזיק אותו בין ריצות (פרק 7).
וזה גם מסביר למה אתה משלם על כל ההיסטוריה בכל סבב. אם בסבב ה-20 ה-state מכיל 15,000 tokens של היסטוריה, אתה משלם input על כל 15,000 ה-tokens האלה — שוב — בכל קריאה. זה לא באג, זו המהות: המודל חייב לראות את כל ההקשר כדי להמשיך, ואין לו דרך אחרת לקבל אותו חוץ מ-ששתשלח אותו שוב. לכן ה-cost של ריצת agent ארוכה הוא לא ליניארי אלא ריבועי בקירוב — ככל שההיסטוריה גדלה, כל סבב נוסף יקר יותר מקודמו. ההבנה הזו היא הבסיס לכל ה-cost-control שנבנה בקורס, מ-context budgets (פרק 3) ועד subagent isolation (פרק 6).
סבב 1 שולח: [system, user("בנה X")]. המודל עונה ומבקש tool. סבב 2 שולח: [system, user("בנה X"), assistant(tool_use), user(tool_result)] — כלומר את כל הסבב הקודם פלוס התוצאה החדשה. סבב 3 גדול עוד יותר. ההיסטוריה תופחת בכל iteration — וזה בדיוק למה היא בסוף נגמרת ומגיעים לקיר ה-context.
ארבעת סוגי ה-messages
ה-state של הסוכן בנוי מ-4 סוגי הודעות. הבן את התפקיד של כל אחת:
| תפקיד | מה זה | מי כותב |
|---|---|---|
system | תצורת ה-harness: מי הסוכן, מה מותר, איזה כללים. לא חלק מ-messages — פרמטר נפרד. | אתה (פעם אחת) |
user | ה-goal ההתחלתי, ואחר כך — מיכל ל-tool_result בכל סבב. | אתה / ה-harness |
assistant | תגובת המודל: טקסט, או בקשת tool_use, או שניהם. | המודל |
tool_result | תוצאת הרצת ה-tool. נשלחת בתוך הודעת user, ומקושרת ל-tool_use דרך tool_use_id. | ה-harness |
טעות נפוצה: לשכוח להחזיר tool_result אחרי tool_use
אם המודל החזיר בלוק tool_use ואתה לא שולח בסבב הבא הודעת user עם tool_result תואם (לפי tool_use_id) — ה-API ייכשל, או שהמודל ייכנס ל-state שבור ויחזור על הקריאה. כל tool_use חייב tool_result תואם, באותו סדר. זה החוזה הבסיסי של הלולאה, ו-50% מהבאגים בלולאות raw נובעים מהפרתו.
שים לב לדבר עדין שמבלבל מתחילים: ה-tool_result נשלח בתוך הודעת role: "user", לא role: "tool". מבחינת ה-API, "המשתמש" (כלומר ה-harness שלך, שמדבר בשם המשתמש) הוא זה שמספק למודל את תוצאות ה-tools. זה הגיוני אם חושבים על זה: המודל ביקש משהו (assistant), והצד השני של השיחה — ה-harness — מספק את התשובה (user). הזרימה תמיד מתחלפת: assistant מבקש, user מספק, assistant ממשיך. שמירה על ההתחלפות הזו היא מה שמחזיק את ה-state תקין.
מה נכנס ל-state ומה לא — החוזה המדויק
הסעיף הזה הוא המקום שבו רוב המתחילים מתבלבלים, אז נעבור עליו לאט. לא כל מה שקורה בלולאה נכנס ל-state. הנה החוזה המדויק:
| אירוע | נכנס ל-state? | איפה? |
|---|---|---|
| ה-goal ההתחלתי | ✅ כן | הודעת user ראשונה |
| תגובת המודל (טקסט +/או tool_use) | ✅ כן | הודעת assistant — תמיד |
| תוצאת ה-tool | ✅ כן | הודעת user עם tool_result block |
| ה-system prompt | ❌ לא | פרמטר נפרד, לא חלק מ-messages |
| ה-print-ים של ה-debug | ❌ לא | רק סטדאאוט, לא חלק מהקריאה למודל |
| מטא-דאטה (turn counter, costs) | ❌ לא | רק אצלך, לא נשלח למודל |
| Errors של ה-handler | ⚠️ תלוי | אם תוצאה → כן, כ-tool_result עם ERROR ב-content. אם קריסה → ה-state מושחת |
ההבחנה האחרונה חשובה: שגיאה בתוך ה-tool צריכה לחזור למודל, לא לקרוס את הלולאה. כשה-tool נכשל, ה-harness צריך לתפוס את החריגה, להפוך אותה למחרוזת תיאורית, ולהחזיר אותה ב-tool_result — כך המודל רואה את הכישלון, מבין למה, ויכול לנסות אחרת. אם במקום זה הלולאה קורסת — המודל לעולם לא יודע שהקריאה נכשלה, וה-state מושחת. ב-harness.py שלנו השתמשנו בדפוס הזה (try/except שמחזיר "ERROR: ...") — זה לא מנהג טוב, זה דרישת חוזה.
מסגרת החלטה: הזוגיות של ה-state
אם אתה סופר את המספר הודעות ב-state בכל סבב → אז הוא תמיד אי-זוגי לפני קריאה למודל, וזוגי אחרי שהמודל עונה + ה-harness מחזיר results.
אם הוא לא מתנהג כך → אז חסרה הודעה: כנראה tool_result שלא נשלח, או assistant שלא נשמר.
בדיקה מהירה: assert len(messages) % 2 == 1 לפני כל call_model. אם זה נופל — יש לך באג ברגע שהוא קורה, לא בריצה הבאה.
⚡ Do Now
קח את hello_agent.py ושנה אותו לשיחת המשך ידנית: אחרי התשובה הראשונה, הוסף את resp.content כהודעת assistant ל-messages, הוסף הודעת user חדשה ("תן דוגמה"), ושלח שוב. ראית שהמודל "המשיך"? עכשיו אתה יודע בדיוק למה — אתה החזקת את ה-state, לא הוא. זה כל הסוד.
הלולאה הגלויה — בונים אותה שורה אחר שורה
עכשיו הלב של הפרק. נבנה את הלולאה המפורשת מעל ה-Messages API. אני אפרק אותה לשלושה חלקים: (1) הגדרת tool, (2) ה-handler שמריץ אותו, (3) הלולאה עצמה. בסוף יהיה לך harness.py שלם בפחות מ-150 שורות.
חלק 1 — מגדירים tool (schema)
tool הוא הצהרה שאומרת למודל: "יש לך יכולת כזו, ואלה הפרמטרים שלה". זה JSON schema. נתחיל ב-tool פשוט ובטוח — get_time, שמחזיר את השעה הנוכחית, ו-read_file שקורא קובץ:
import os
import json
import datetime
from anthropic import Anthropic
client = Anthropic()
MODEL = "claude-sonnet-4-6"
# === הגדרת ה-tools (schema) — מה שהמודל "רואה" ===
TOOLS = [
{
"name": "get_time",
"description": "מחזיר את התאריך והשעה הנוכחיים. אין פרמטרים.",
"input_schema": {
"type": "object",
"properties": {},
"required": [],
},
},
{
"name": "read_file",
"description": "קורא קובץ טקסט ומחזיר את תוכנו. השתמש כשצריך לראות תוכן קובץ.",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "הנתיב לקובץ"},
},
"required": ["path"],
},
},
]
שים לב: ה-description הוא לא קישוט. זה ה-prompt היחיד שהמודל מקבל על מתי ואיך להשתמש ב-tool. description גרוע = המודל יקרא ל-tool הלא נכון או בזמן הלא נכון. זו הנדסת harness בזעיר אנפין.
מסגרת החלטה: description של tool — טוב מול גרוע
description טוב אומר: מה ה-tool עושה, מתי לקרוא לו, ומתי לא. הוא מתייחס למקרי-גבול (מה עושים אם הקובץ לא קיים) ולקונבנציות (path יחסי מול מוחלט).
description גרוע אומר: מה ה-tool עושה במשפט אחד, בלי הקשר. דוגמה גרועה: "קורא קובץ." דוגמה טובה: "קורא קובץ טקסט ומחזיר את תוכנו (עד 4000 תווים). השתמש כשצריך לראות תוכן קובץ; אל תשתמש לקבצים בינאריים או גדולים מדי."
אם המודל קורא ל-tool שלא רצית, או לא קורא ל-tool שרצית → אז הבעיה היא ב-description, לא בקוד.
חלק 2 — ה-handler שמריץ את ה-tool בפועל
ה-schema אומר למודל מה קיים. ה-handler הוא הקוד שלך שבאמת מריץ את הפעולה כשהמודל מבקש אותה. זה הצד שלך של החוזה:
# === הרצת ה-tool בפועל — הצד שלך של החוזה ===
def run_tool(name: str, args: dict) -> str:
"""מקבל שם tool וארגומנטים, מחזיר מחרוזת תוצאה."""
if name == "get_time":
return datetime.datetime.now().isoformat(timespec="seconds")
if name == "read_file":
path = args.get("path", "")
try:
with open(path, "r", encoding="utf-8") as f:
# מגבילים גודל — קובץ ענק יכול להפיל את ה-context (פרק 3!)
return f.read(4000)
except Exception as e:
# מחזירים את השגיאה כתוצאה — המודל יכול להתאושש ממנה
return f"ERROR reading {path}: {e}"
return f"ERROR: unknown tool '{name}'"
read_file קורא רק 4000 תווים — כי tool שמחזיר 50KB ישר ל-context יכול להפיל ריצה לבדו (נחזור לזה בפרק 3). שנית, שגיאה לא מפילה את הלולאה — אנחנו מחזירים אותה כתוצאה, והמודל יכול לקרוא אותה ולנסות אחרת. שתי ההחלטות האלה הן כבר harness engineering.
חלק 3 — הלולאה עצמה
וזה הרגע. הלולאה המפורשת. קרא אותה לאט — היא הגרעין של כל הקורס:
def run_agent(goal: str, max_turns: int = 8) -> str:
"""לולאת agent מפורשת: goal → call model → run tools → feed back → stop."""
system = (
"אתה סוכן הרץ בתוך harness דק. "
"כשצריך מידע חיצוני (שעה, קובץ) — קרא ל-tool. "
"כשהמשימה הושלמה — החזר תשובה סופית בלי לקרוא ל-tool."
)
# ה-state: רשימת ההודעות שצוברת לאורך הריצה. זה ה"זיכרון".
messages = [{"role": "user", "content": goal}]
for turn in range(1, max_turns + 1):
print(f"\n--- turn {turn}/{max_turns} ---")
# 1) קריאה למודל — שולחים את כל ה-state מחדש (המודל stateless)
resp = client.messages.create(
model=MODEL,
max_tokens=1024,
system=system,
tools=TOOLS,
messages=messages,
)
print(f" stop_reason={resp.stop_reason} "
f"in={resp.usage.input_tokens} out={resp.usage.output_tokens}")
# 2) שומרים את תגובת המודל ל-state (assistant turn)
messages.append({"role": "assistant", "content": resp.content})
# 3) האם המודל ביקש tools? אם לא — זו התשובה הסופית. עצור.
if resp.stop_reason != "tool_use":
final = "".join(b.text for b in resp.content if b.type == "text")
print(f" STOP: final answer reached")
return final
# 4) המודל ביקש tool(s). מריצים כל אחד ואוספים tool_result blocks.
tool_results = []
for block in resp.content:
if block.type == "tool_use":
print(f" tool_use: {block.name}({json.dumps(block.input)})")
result = run_tool(block.name, block.input)
print(f" tool_result: {result[:80]}")
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id, # קישור ל-tool_use!
"content": result,
})
# 5) מחזירים את התוצאות כהודעת user — והלולאה ממשיכה
messages.append({"role": "user", "content": tool_results})
# 6) נגמרו ה-turns בלי תשובה סופית — תנאי עצירה שני (הבלם!)
return f"[max-turns={max_turns} reached without final answer]"
if __name__ == "__main__":
answer = run_agent("מה השעה עכשיו, ומה כתוב בקובץ ./notes.txt?")
print("\n=== FINAL ===")
print(answer)
זהו. פחות מ-150 שורות, כולל ה-tools. בוא נעבור על מה שקורה, כי כל מספר כאן יחזור בהמשך הקורס:
- שורה (1): בכל סבב אנחנו שולחים את כל
messagesמחדש. זה ה-stateless-בפעולה. ה-state גדל בכל iteration. - שורה (3):
stop_reasonהוא הסיגנל. אם הוא"tool_use"— המודל רוצה שנריץ tool. כל ערך אחר (בדרך כלל"end_turn") — זו התשובה הסופית, ואנחנו עוצרים. - שורה (4): תגובה אחת יכולה להכיל כמה בלוקי
tool_use. מריצים את כולם, אוספים את כל ה-results. - שורה (5): מחזירים את כל ה-
tool_resultבהודעתuserאחת — והלולאה חוזרת לראש. - שורה (6): ה-
for turn in range(...)הוא ה-max-turns. אם הגענו לכאן — הלולאה לא הסתיימה לבד, והבלם עצר אותה. זה הקו שמפריד בין harness ל-לולאה אינסופית.
שתי החלטות עיצוב נוספות שכדאי לשים לב אליהן, כי הן יחזרו לאורך הקורס. ראשית, ה-print-ים. הם לא debug זמני שנמחק אחר כך — הם ה-observability הפרימיטיבי של הלולאה. בפרק 5 נחליף אותם ב-tracing אמיתי (Langfuse), אבל העיקרון זהה: אתה חייב לראות כל סבב, כל tool call, וכל token count. harness שאתה לא רואה הוא harness שאתה לא יכול לדבג. שנית, סדר הצעדים מחייב: קודם מוסיפים את תגובת המודל ל-state (שורה 2), ורק אחר כך מריצים tools (שורה 4). אם תהפוך את הסדר — תריץ tool אבל לא תשמור את הבקשה שהובילה אליו — תפר את החוזה של tool_use↔tool_result והלולאה תישבר בסבב הבא.
ויש כאן עיקרון שקט אבל מהותי: הלולאה לא "מבינה" את המשימה. היא לא יודעת אם הסוכן מתקדם או תקוע, אם התשובה נכונה או שגויה. כל מה שהיא יודעת זה לתרגם stop_reason להחלטה: tool_use → הרץ והמשך, אחר → עצור. כל ה"חוכמה" יושבת במודל ובתוכן ה-state; ה-harness הוא המנגנון הטיפש והאמין שמריץ את המחזור בבטחה. זו תכונה, לא חיסרון — harness דטרמיניסטי ופשוט הוא harness שאפשר לסמוך עליו, לדבג אותו, ולהוסיף לו שכבות. כשנגיע ל-governance בפרק 4, נראה למה דווקא חלקים שהם דטרמיניסטיים (gates, ולא שיקול דעת של LLM שני) הם מה שהופך harness לבטוח.
כשהמודל קורא ל-read_file({"path": "./notes.txt"}), ה-handler שלנו מריץ את הקוד הבא ומחזיר את המחרוזת:
with open("./notes.txt", "r", encoding="utf-8") as f:
return f.read(4000)
# → 'ה-harness עובד. פרק 2.\n' (24 תווים — נכנסים ב-4000 התווים הראשונים)
את המחרוזת הזו המודל רואה ב-tool_result בסבב הבא, ומשתמש בה כדי לענות ל-goal "מה כתוב בקובץ".
תרגיל 1 — הרץ את הלולאה וקרא את הלוג
הרכבת harness.py: הקובץ מורכב מ-3 החלקים שלמעלה, בסדר הזה בקובץ אחד: חלק 1 (imports + TOOLS עם get_time ו-read_file) → חלק 2 (run_tool handler) → חלק 3 (run_agent + הבלוק if __name__ == "__main__ בסוף). צור גם notes.txt עם שורת טקסט (למשל ה-harness עובד. פרק 2.) והרץ:
echo "ה-harness עובד. פרק 2." > notes.txt
python harness.py
הפלט שתראה (output נראה לעין):
--- turn 1/8 ---
stop_reason=tool_use in=312 out=48
tool_use: get_time({})
tool_result: 2026-06-19T14:22:07
--- turn 2/8 ---
stop_reason=tool_use in=389 out=61
tool_use: read_file({"path": "./notes.txt"})
tool_result: ה-harness עובד. פרק 2.
--- turn 3/8 ---
stop_reason=end_turn in=441 out=39
STOP: final answer reached
=== FINAL ===
השעה כעת 14:22, ובקובץ notes.txt כתוב: "ה-harness עובד. פרק 2.".
מה לשים לב: שני סבבי tool ואז עצירה. ראה איך in= (input_tokens) גדל מ-312 ל-389 ל-441 — זה ה-state התופח, שחור על גבי לבן. זה בדיוק ה-context שיתמלא בפרק 3.
אם הסוכן עצר ב-turn 1 בלי לקרוא tools — ה-description שלך לא משכנע מספיק, או שהמודל החליט שהוא יודע את התשובה. נסה goal שמחייב tool: "קרא את ./notes.txt והחזר את התוכן בדיוק".
⚡ Do Now
הוסף שורת print(f" total_messages={len(messages)}") בתחילת כל turn, והרץ שוב. ראה איך מספר ה-messages גדל מ-1 ל-3 ל-5. כל זוג (assistant + user/tool_result) מוסיף 2. זה ה-state. כתוב לעצמך: בריצה של 8 turns מלאים, כמה הודעות יצטברו?
⚡ Do Now — ניסוי counter
הפעל את הלולאה עם max_turns=3 ו-goal שמחייב 5 כלי עבודה שונים (למשל "קרא את notes.txt, אחר כך קרא את hello_agent.py, אחר כך..."). ראה איך הלולאה נעצרת באמצע עם הסטטוס [max-turns=3 reached]. זה לא failure — זה ה-belt-and-suspenders של ה-harness. עכשיו תכפיל את max_turns ותראה שה-same logic משלים.
max-turns — הבלם הראשון, ולמה הוא לא אופציונלי
שים לב שהגדרנו max_turns: int = 8 ולא השארנו את הלולאה פתוחה. זה לא קישוט. זה הבלם הראשון של כל harness, וזו הטעות הכי נפוצה בבניית לולאה ביתית.
הסיבה פשוטה ומפחידה: סוכן יכול להיתקע. תאר tool שנכשל — נניח read_file על קובץ שלא קיים. המודל מקבל את השגיאה, "חושב" שאולי הוא טעה בנתיב, מנסה נתיב אחר, נכשל שוב, מנסה שוב... בלי בלם, הוא יכול לקרוא לאותו tool 6+ פעמים ברצף, כל פעם שורף tokens, עד שכל ה-budget נגמר — בלי שום תוצאה course.research.json — gotchas.
וזה לא תרחיש קצה נדיר. זה אחד הכשלים הנפוצים ביותר ב-harnesses ביתיים, ובדיוק אחד הסימפטומים שמחקר ה-2026 מציין כשהוא מתאר למה frameworks כבדים נכשלים: "סוכנים שלולפים 6+ פעמים על אותה קריאת tool" בשילוב "אפס observability". שני אלה הולכים יד ביד — בלי בלם הסוכן לולף, ובלי observability אתה מגלה את זה רק כשמגיע החשבון. ה-max-turns פותר את הצד הראשון: גם אם הסוכן תקוע, הלולאה תיעצר אחרי מספר ידוע של סבבים, ואתה תדע בדיוק כמה זה עלה לך במקרה הגרוע.
איך בוחרים את הערך? זו החלטת harness, לא קסם. לוקחים את ה-budget לריצה (נניח אתה מוכן לשלם עד $0.50 לריצת agent), מחלקים בעלות הממוצעת לסבב, ומקבלים תקרה. למשימות פשוטות 5-8 turns מספיקים בשפע; למשימות מורכבות עם 10+ tool calls — 15-20. הכלל: max-turns תמיד נמוך מספיק כדי שהמקרה הגרוע (כל הסבבים מבוזבזים) עדיין יהיה עלות שאתה מוכן לבלוע. אם המקרה הגרוע כואב — התקרה גבוהה מדי.
טעות נפוצה: לבנות לולאה בלי max-turns
זו ה-#1 בלולאות ביתיות. while True: בלי תקרה הוא לא "לולאת agent" — הוא פצצת tokens מתוזמנת. הסוכן נתקע על tool שנכשל, חוזר עליו שוב ושוב, ושורף את כל ה-budget בלי תוצאה. תקרת iterations היא תנאי קיום, לא feature. בפרק 5 נוסיף עליה circuit-breaker חכם שמזהה את הלולאה 2-3 סבבים לפני שמגיעים ל-max-turns — אבל max-turns הוא קו ההגנה הבסיסי שתמיד חייב להיות.
טעות נפוצה: להחזיר partial text כ"תשובה" אחרי max-turns
גרסה עדינה יותר של אותה טעות. הלולאה נעצרה, אבל במקום להחזיר סטטוס [max-turns reached] — הקוד מחזיר את הטקסט האחרון של המודל. הבעיה: קוד במעלה הזרם לא יודע להבחין בין "הסוכן ענה סופית" ל"הסוכן נקטע באמצע משפט". תוצאה: תשובה חלקית ממשיכה לזרום במורד הצינור — ל-subagent-הבא, לדיסק, ל-API חיצוני. החזר תמיד marker ברור.
מסגרת החלטה: שלושת תנאי העצירה
לסוכן יש שלוש דרכים לסיים סבב. כל harness חייב לטפל בשלושתן:
אם stop_reason != "tool_use" (בדרך כלל end_turn) → אז זו תשובה סופית. עצור והחזר אותה. זה הסיום הרצוי.
אם המודל ביקש tool_use → אז זה לא סיום. הרץ, החזר תוצאה, המשך ללולאה.
אם נגמרו ה-turns בלי תשובה סופית → אז זו עצירה כפויה (הבלם). החזר סטטוס שגיאה ברור — לא תשובה מזויפת. ה-harness חייב להבחין בין "סיימתי" ל"נעצרתי באמצע".
הנקודה האחרונה במסגרת קריטית ולעיתים קרובות מתפספסת. כשה-harness נעצר ב-max-turns, אסור לו להחזיר את הטקסט החלקי האחרון כאילו הוא התשובה. למה? כי קוד אחר במעלה הזרם (או subagent-על בפרק 6) עלול לקחת את ה"תשובה" החלקית הזו ולהמשיך לבנות עליה — להעביר אותה הלאה, לשמור אותה, לשלוח אותה ב-email. עצירה כפויה היא כשל, והיא צריכה להיראות כמו כשל — סטטוס מובחן (כמו ה-[max-turns reached] שהחזרנו) שקוד למעלה יכול לזהות ולטפל בו, ולא טקסט שמתחזה לתוצאה תקינה. ההבחנה בין "הצלחה" ל"עצירה" היא הזרע ל-failure-recovery שנבנה בפרק 5.
⚡ Do Now
שנה את ה-goal ל-"קרא את הקובץ ./does-not-exist.txt ותקן אותו" והורד את max_turns ל-3. הרץ. ראה איך הסוכן מנסה את ה-tool שוב ושוב — וה-max-turns תופס אותו. עכשיו ספור: בלי הבלם, כמה סבבים זה היה רץ? כמה tokens זה היה שורף? רשום את המספר — זה בדיוק מה ש-max-turns חסך לך.
אותה לולאה דרך ה-Claude Agent SDK
עכשיו, אחרי שראית את הלולאה הגלויה, נראה מה ה-Claude Agent SDK עושה בשבילך. ה-SDK עוטף את אותה לולאה בדיוק — קריאה למודל, הרצת tools, החזרת results, repeat — מאחורי query() generator. כל סבב שאתה רואה ב-raw קורה גם כאן, פשוט מתחת למכסה:
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions
async def main():
options = ClaudeAgentOptions(
system_prompt="אתה סוכן הרץ בתוך harness דק. השתמש ב-tools כשצריך.",
max_turns=8, # אותו בלם — מובנה ב-SDK
allowed_tools=["Read", "Bash"], # built-in tools של ה-SDK
)
# query() הוא async generator: הוא מריץ את הלולאה ומזרים אירועים
async for message in query(
prompt="מה כתוב בקובץ ./notes.txt? החזר את התוכן בדיוק.",
options=options,
):
# כל message הוא אירוע בלולאה — assistant, tool_use, result...
print(type(message).__name__, "->", message)
asyncio.run(main())
שורות בודדות, והלולאה רצה. ה-SDK נתן לך: את הלולאה, ניהול ה-message state, ה-built-in tools (Read, Bash, Search, ועוד 14+), ואכיפת ה-max_turns. זה הערך שלו — ולכן בקורס הזה אנחנו בונים מעליו, לא במקומו.
שים לב להבדל ב-API עצמו: ה-SDK עובד עם async/await ומחזיר generator שמזרים אירועים, בעוד הלולאה ה-raw שלנו סינכרונית עם for פשוט. זה לא מקרי — ה-streaming הזה הוא מה שיאפשר ל-orchestration רב-סוכני בפרק 6 לעקוב אחרי כמה סוכנים במקביל. כרגע, מה שחשוב להבין הוא שה-query() מריץ בדיוק את אותם 6 צעדים שכתבנו ידנית — build messages, call model, בדוק tool_use, הרץ, החזר, repeat — רק שהוא עושה את זה בקוד ש-Anthropic מתחזקת, נבדק, ומטופל היטב. כשתקרא ל-query(), דמיין את הלולאה ה-raw רצה מתחת. עכשיו אתה לא צריך לדמיין — אתה יודע, כי כתבת אותה.
ובכל זאת — מתי query() לא מספיק? (3 מקרים:) כשאתה צריך להזריק קוד בין הצעדים שה-SDK לא חושף נקודת חיבור אליו. רוצה לבדוק את ה-context ולגבות state לפני כל קריאה למודל (פרק 3)? לעצור tool call מסוכן לפני שהוא רץ (פרק 4)? לספור קריאות חוזרות ולשבור לולאה (פרק 5)? את כל אלה אתה צריך להזריק לתוך הלולאה — ולתוך קופסה שחורה אי אפשר להזריק. אז או שאתה מחזיק לולאה raw, או שאתה עוטף את ה-SDK בשכבה דקה משלך שמיירטת את האירועים. שתי הגישות תקפות, ושתיהן דורשות שתבין את הלולאה — מה שעשית עכשיו.
⚡ Do Now
הרץ את sdk_agent.py ושים לב לשורת ה-ResultMessage בסוף — היא כוללת num_turns ו-total_cost_usd. השווה את num_turns למספר ה-turns שספרת ב-harness.py על אותו goal. זהים? אם כן — הוכחת לעצמך ששתי הלולאות עושות אותו דבר. אם לא — חשוב למה (רמז: built-in tools מול ה-tools שלך עשויים לדרוש מספר סבבים שונה).
מסגרת החלטה: לולאה raw מול query() של ה-SDK
אם אתה רוצה התנהגות agent סטנדרטית עם ה-built-in tools, ואין לך דרישה מיוחדת מהלולאה → אז השתמש ב-query() של ה-SDK. הוא נכון, נבדק, ומתוחזק.
אם אתה צריך לשלוט בתוך הלולאה — להזריק ניהול context משלך (פרק 3), policy gates דטרמיניסטיים (פרק 4), tracing מותאם (פרק 5), או orchestration רב-סוכני (פרק 6) → אז אתה צריך את הלולאה המפורשת, או לעטוף את ה-SDK בשכבה משלך. אי אפשר להזריק לוגיקה לתוך קופסה שחורה.
הגישה של הקורס: מבינים את ה-raw כדי לדעת מה ה-SDK עושה, ואז בונים את שכבות ה-harness שלנו — או מעל הלולאה הגלויה, או כ-wrapper סביב ה-SDK. שתי הדרכים תקפות; מה שלא תקף הוא לקרוא ל-query() ולקוות.
תרגיל 2 — הרץ את שתי הגרסאות, השווה את הפלט
הרץ גם את harness.py (raw) וגם את sdk_agent.py (SDK) על אותו goal. שתיהן יקראו את הקובץ ויחזירו את התוכן — אבל ה-visibility שונה לגמרי.
הפלט מ-sdk_agent.py (output נראה לעין):
SystemMessage -> [init: model=claude-sonnet-4-6, tools=[Read, Bash]]
AssistantMessage -> [tool_use: Read(file_path=./notes.txt)]
UserMessage -> [tool_result: ה-harness עובד. פרק 2.]
AssistantMessage -> [text: בקובץ כתוב: "ה-harness עובד. פרק 2."]
ResultMessage -> [num_turns=2, total_cost_usd=0.0041, duration_ms=2380]
השאלה לרשום: ב-harness.py אתה רואה כל tool_use, כל tool_result, וכל token count כי אתה כתבת את ה-print-ים. ב-sdk_agent.py אתה רואה רק את האירועים ש-ה-SDK בוחר לחשוף. איזה מהם תוכל להזריק בו circuit-breaker מפרק 5? (רמז: רק את זה שאתה מחזיק את הלולאה שלו.) זו בדיוק הסיבה שבנינו raw.
ה-tool המותאם הראשון שלך
ב-harness.py כבר חיברת שני tools (get_time, read_file) — אז למעשה כבר כתבת tools מותאמים. עכשיו נסגור את ההבנה: tool מותאם הוא זוג — schema (מה המודל רואה) + handler (מה הקוד מריץ). זה הכל. נוסיף tool שלישי שמייצר פלט נראה לעין — סופר מילים בקובץ:
# מוסיפים ל-TOOLS:
{
"name": "count_words",
"description": "סופר מילים בקובץ טקסט ומחזיר את המספר. שימושי לדוחות.",
"input_schema": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
}
# מוסיפים ל-run_tool():
if name == "count_words":
try:
with open(args["path"], "r", encoding="utf-8") as f:
return str(len(f.read().split()))
except Exception as e:
return f"ERROR: {e}"
זה הדפוס המלא של כל tool מותאם בקורס, גם המתוחכמים ביותר: schema שמתאר, handler שמריץ, ותוצאה כמחרוזת (או JSON) שחוזרת ללולאה. בפרק 4 נחמיר את זה עם structured output ו-JSON schema enforcement כדי שה-tool לא יחזיר טקסט חופשי שביר — אבל המבנה הבסיסי כבר כאן.
תרגיל 3 — חבר tool מותאם וראה את הסוכן בוחר בו
הוסף את count_words ל-harness.py והרץ עם goal חדש:
python harness.py
# שנה את ה-goal בקובץ ל:
# "כמה מילים יש בקובץ ./notes.txt?"
הפלט (output נראה לעין):
--- turn 1/8 ---
stop_reason=tool_use in=358 out=42
tool_use: count_words({"path": "./notes.txt"})
tool_result: 4
--- turn 2/8 ---
stop_reason=end_turn in=410 out=18
STOP: final answer reached
=== FINAL ===
בקובץ ./notes.txt יש 4 מילים.
הניצחון: לא אמרת למודל "השתמש ב-count_words". ה-description לבדו הספיק כדי שהוא יבחר בו. זו הוכחה חיה שה-tool מחובר נכון ושה-schema-as-prompt עובד. שנה את ה-description ל-משהו עמום ("עושה דברים עם קבצים") והרץ שוב — תראה את המודל מתבלבל. ה-description הוא ה-interface.
⚡ Do Now
כתוב tool רביעי משלך — למשל list_dir שמחזיר את שמות הקבצים בתיקייה (os.listdir). הגדר schema, כתוב handler, הוסף ל-run_tool, ותן לסוכן goal שמחייב אותו. אם הוא קרא לו — הבנת את הדפוס לגמרי. אם לא — שפר את ה-description.
ה-built-in tools של ה-SDK: file, bash, search
ב-harness.py כתבת tools ידנית. ה-Claude Agent SDK מגיע עם 14+ tools מובנים מהקופסה, כך שלמשימות נפוצות אתה לא צריך לכתוב כלום course.research.json — Claude Agent SDK. השלושה המרכזיים:
| Tool | מה הוא נותן לסוכן | סיכון |
|---|---|---|
| File (Read/Write/Edit) | קריאה, כתיבה ועריכה של קבצים במערכת — בלי שתכתוב open() בעצמך. | בינוני — כתיבה/מחיקה של קבצים |
| Bash | הרצת פקודות shell — git, build, test, כל דבר בטרמינל. | גבוה — rm -rf, פקודות הרסניות |
| Search | חיפוש בקבצים ובתוכן (grep/glob) — מציאת קוד ומידע בפרויקט. | נמוך — קריאה בלבד |
שים לב לעמודת הסיכון. ה-Bash נותן לסוכן כוח אדיר — ובדיוק לכן הוא מסוכן. סוכן עם גישת bash ו-hallucination אחד יכול להריץ rm -rf. ב-harness.py שלנו אין עדיין שום שער שעוצר את זה. זו לא רשלנות — זה ה-roadmap: בפרק 4 נבנה policy gate דטרמיניסטי (Faramesh/FPL) שחוסם פעולות הרסניות לפני שהן קורות. כרגע, כשאתה מנסה את ה-built-in Bash, עבוד בתיקיית sandbox.
טעות נפוצה: לתת built-in Bash בלי גבולות
קל מאוד להוסיף allowed_tools=["Bash"] ולשכוח שנתת לסוכן מקלדת מלאה למערכת שלך. עד שנבנה את ה-governance בפרק 4 — הרץ ניסיונות עם built-in Bash רק בתיקיית sandbox נפרדת, רצוי במכונה או container שאתה מוכן לאבד. הסיכון אינו תיאורטי.
mock/dev mode — לבדוק את הלולאה בלי לשרוף tokens
בזמן פיתוח הלולאה, אתה מריץ אותה עשרות פעמים — מתקן print, משנה תנאי עצירה, מסדר את צבירת ה-state. אין שום סיבה לשלם tokens אמיתיים על כל ריצת debug של הלוגיקה. הפתרון: mock mode — מחליפים את הקריאה למודל בתשובה מזויפת קבועה, ובודקים שהלולאה עצמה (הצבירה, ה-stop conditions, ה-tool dispatch) עובדת:
MOCK = os.environ.get("HARNESS_MOCK") == "1"
def call_model(system, messages):
if MOCK:
# תשובה מזויפת: סבב 1 מבקש tool, סבב 2 מסיים
if not any(m["role"] == "user" and isinstance(m["content"], list)
for m in messages):
return _fake_tool_use("get_time", {}) # עוד לא רץ tool — בקש אחד
return _fake_text("מצב mock: הלולאה עבדה.") # כבר רץ tool — סיים
# אמיתי:
return client.messages.create(model=MODEL, max_tokens=1024,
system=system, tools=TOOLS, messages=messages)
(הפונקציות _fake_tool_use ו-_fake_text בונות אובייקטים עם אותם שדות שה-API מחזיר — content, stop_reason, usage — כדי שהלולאה לא תבחין בהבדל.) עכשיו HARNESS_MOCK=1 python harness.py בודק את כל הלוגיקה ב-0 tokens.
call_model שמפרידה בין "הלולאה" ל"הקריאה למודל" — היא נקודת ההזרקה שכל הקורס מנצל. אותו seam שמאפשר mock היום יאפשר מחר להזריק tracing (פרק 5), context management (פרק 3), ו-policy gates (פרק 4). harness טוב בנוי מ-seams.
תרגיל 4 — הרץ את הלולאה ב-mock mode (0 tokens)
הוסף את ה-call_model עם דגל ה-mock, החלף את הקריאה הישירה בלולאה ל-resp = call_model(system, messages), והרץ:
HARNESS_MOCK=1 python harness.py
הפלט (output נראה לעין):
--- turn 1/8 ---
stop_reason=tool_use in=0 out=0 <-- mock: 0 tokens!
tool_use: get_time({})
tool_result: 2026-06-19T14:31:55
--- turn 2/8 ---
stop_reason=end_turn in=0 out=0
STOP: final answer reached
=== FINAL ===
מצב mock: הלולאה עבדה.
הניצחון: in=0 out=0 — אפס tokens, אבל הלולאה השלמה רצה: tool_use, tool_result, צבירת state, ו-stop condition. כך בודקים שינויי לוגיקה במהירות וחינם, ושומרים את ה-tokens האמיתיים לבדיקות end-to-end בלבד.
⚡ Do Now
הרץ HARNESS_MOCK=1 python harness.py על goal שמחייב 5 קריאות tool רצופות. תראה איך ה-mock מחזיר את אותה תשובה בסבב 2, ללא קשר ל-goal. זה ה-side-effect של mock: הוא בודק שהלולאה עובדת, לא שהמודל מבין. כשאתה רוצה לבדוק את המודל — בלי mock.
ה-system prompt הוא config של ה-harness — לא "הוראות"
שים לב מה כתבנו ב-system: "אתה סוכן הרץ בתוך harness דק... כשהמשימה הושלמה — החזר תשובה סופית בלי לקרוא ל-tool." זו לא "אישיות". זו הנדסה. ה-system prompt מגדיר את חוקי הלולאה: מתי לקרוא tools, מתי לעצור, מה ה-constraints. שינוי בו הוא שינוי בהתנהגות ה-harness, בדיוק כמו שינוי בקוד הלולאה.
דוגמה ל-system prompt שמשפר את ההתנהגות — מוסיף כלל עצירה מפורש שמונע מהסוכן "לרחף":
אתה סוכן הרץ בתוך harness דק עם תקרת max-turns.
כללי עבודה:
1. קרא ל-tool (למשל read_file) רק כשחסר לך מידע אמיתי — אל תקרא "ליתר ביטחון".
2. אל תקרא לאותו tool עם אותם args פעמיים — אם נכשל, שנה גישה.
3. ברגע שיש לך מספיק מידע לענות — עצור והחזר תשובה סופית.
4. אם tool מחזיר ERROR — קרא אותו, אל תתעלם, והחלט אם לנסות אחרת או לעצור.
כלל 2 לבדו מקטין דרמטית את הסיכון ללולאות חוזרות — וזה ה-prompt-side של אותה בעיה ש-circuit-breaker יפתור ב-kod בפרק 5. שתי הגנות, שכבות שונות: prompt מבקש יפה, circuit-breaker אוכף בכוח. הנדסת harness טובה משלבת את שתיהן.
מסגרת החלטה: system prompt כקובץ config
אם ה-system prompt שלך משתנה לפי סביבה (dev/staging/prod) או לפי סוג משימה → אז הוצא אותו לקובץ נפרד (prompts/base.txt) וטען אותו בעת הרצה. לא קוד בתוך harness.py.
אם הוא משתנה לפי משתמש (אישיות, סגנון) → אז הוא חלק מה-system, אבל גם אותו כדאי לטעון מחוץ לקוד.
הכלל: system prompt הוא קובץ config של ה-harness, לא מחרוזת קבועה בקוד. שינוי בו לא דורש deploy מחדש — רק reload של הקובץ.
מה הלולאה הזו עדיין לא יודעת
בנית harness עובד. הוא מקבל goal, קורא למודל, מריץ tools, מנהל state, ועוצר. זה הישג אמיתי — רוב המפתחים שמשתמשים ב-agents אף פעם לא ראו את הלולאה מבפנים. אבל הלולאה הזו עדיין שברירית, ובכוונה. הנה בדיוק מה היא לא יודעת — וזה ה-roadmap של הקורס:
| החולשה של הלולאה כרגע | מה יקרה | הפרק שמתקן |
|---|---|---|
| ה-state גדל בלי גבול | בריצה ארוכה ה-context יתמלא, ה-compaction יירה, והיסטוריה קריטית תימחק באמצע | פרק 3 — קיר ה-Context |
| tools מחזירים טקסט חופשי | parsing שביר; ברגע שהפורמט זז, ה-harness נשבר. ואין שער על Bash | פרק 4 — Tools, MCP, Governance |
| אין observability | כשהסוכן "נתקע" אין לך איפה לראות; print-ים לא מספיקים לצוות | פרק 5 — Observability & Recovery |
| סוכן יחיד בלבד | משימות שדורשות parallelism או הפרדת אחריות חונקות סוכן בודד | פרק 6 — Multi-Agent Orchestration |
| אין זיכרון בין ריצות | כל ריצה מתחילה מאפס; הסוכן חוזר על אותן טעויות | פרק 7 — Memory & Dreaming |
כל אחת מהשכבות האלה נכנסת בתוך ה-seam שכבר יצרנו — הפונקציה call_model והלולאה המפורשת. זו הסיבה שבנינו אותם בעצמנו: אי אפשר להזריק שכבות לתוך קופסה שחורה.
🔧 שגרת עבודה: לבנות ולשנות לולאת agent בבטחה
בכל פעם שאתה משנה לוגיקה בלולאה (תנאי עצירה, צבירת state, tool dispatch), עבוד לפי הסדר הזה — הוא חוסך tokens ובאגים:
- קודם mock. הרץ
HARNESS_MOCK=1ובדוק שהלוגיקה (צבירה, stop, dispatch) עובדת ב-0 tokens. - קרא את הלוג. ודא שכל
tool_useמקבלtool_resultתואם, ושמספר ה-messages גדל כצפוי (זוגות). - ריצת אמת אחת קצרה. max-turns נמוך (2-3), goal פשוט, API key auth. ודא ש-
stop_reasonו-usageנראים תקינים. - בדוק את הבלם. תן goal שמחייב לולאה (tool שתמיד נכשל) וודא שה-max-turns תופס — לא ש"מתישהו ייעצר".
- רק אז הרץ end-to-end מלא. ה-tokens האמיתיים נשמרים לרגע שהלוגיקה כבר מאומתת.
⭐ Just One Thing
אם תיקח רק דבר אחד מהפרק הזה: המודל הוא stateless — ה-harness הוא ה-state, וה-loop עם max-turns הוא הלב שלו. כל "זיכרון" של הסוכן הוא רשימת ה-messages שאתה צובר ושולח מחדש בכל סבב; וכל harness שיש בו tools חייב max-turns, אחרת הוא לא לולאת agent — הוא פצצת tokens. הפנם את שני אלה, וכל שאר הקורס הוא רק שכבות שנכנסות לתוך הלולאה הזו.
בדוק את עצמך — 5 שאלות
- למה כל קריאה למודל שולחת מחדש את כל ההיסטוריה? מה היה קורה אם היינו שולחים רק את ההודעה האחרונה?
- מה ההבדל בין
stop_reason == "tool_use"ל-stop_reason == "end_turn", ואיך כל אחד משפיע על ההחלטה אם להמשיך את הלולאה? - למה
tool_resultחייבtool_use_id, ומה נשבר אם תחזיר tool_result בלי id תואם, או בכלל לא? - הרצת ריצה אוטומטית על subscription auth ב-20 ביוני 2026 וקיבלת rate limit פתאומי. מה הסיבה ומה התיקון?
- מתי תבחר בלולאה raw מעל הצורך ב-
query()של ה-SDK? תן דוגמה אחת קונקרטית מה-roadmap של הקורס.
סיכום הפרק
- המודל stateless, ה-harness הוא ה-state. אין זיכרון בצד Anthropic; ה"זיכרון" הוא רשימת ה-
messagesשאתה צובר ושולח מחדש בכל קריאה. לכן ה-state גדל בכל turn — וזה הזרע לקיר ה-context בפרק 3. - הלולאה היא 6 צעדים: build messages → call model → בדוק
stop_reason→ אם tool_use הרץ tools ואסוףtool_result→ החזר ל-state → repeat, עד תשובה סופית או max-turns. פחות מ-150 שורות. - 4 סוגי messages:
system(config),user(goal + tool_results),assistant(תגובה + tool_use),tool_result(מקושר ב-tool_use_id). כל tool_use חייב tool_result תואם. - max-turns הוא הבלם הראשון — לא feature, אלא תנאי קיום. בלעדיו סוכן נתקע יכול לקרוא לאותו tool 6+ פעמים ולשרוף את כל ה-budget.
- tool מותאם = schema + handler. ה-
descriptionהוא ה-interface; הוא לבדו קובע אם המודל יבחר ב-tool הנכון בזמן הנכון. - ה-Claude Agent SDK עוטף את אותה לולאה ב-
query()עם built-in tools (file/bash/search) ו-max_turns מובנה — בונים מעליו, לא במקומו, כדי לשמור על שליטה בלולאה. - API key auth חובה לאוטומציה — מ-15 ביוני 2026 ריצות אוטומטיות על subscription יחטפו rate limits; mock mode חוסך tokens בפיתוח; ה-system prompt הוא config של ה-harness.
🌉 גשר לפרק הבא — "קיר ה-Context": ראית בלוג של תרגיל 1 איך input_tokens גדל מ-312 ל-389 ל-441 בשלושה סבבים בלבד. עכשיו דמיין ריצה של 40 סבבים. ה-state התופח הזה יגיע לקיר: ה-Claude Agent SDK שומר 33K tokens ל-output buffer, ו-compaction אוטומטי נורה ב-83.5% מהחלון — ואז ההיסטוריה נדחסת בלי שתבחר מה לשמר. בפרק 3 נמדוד את ה-context בזמן ריצה ונבנה threshold-backup שמשמר state קריטי לפני שה-compaction מוחק אותו. הלולאה שבנית כאן היא בדיוק המקום שבו ניהול ה-context הזה נכנס.
✅ צ'קליסט השלמת הפרק
סמן כל פריט. אם משהו לא מסומן — חזור לסעיף המתאים לפני שתעבור לפרק 3.
- התקנתי
anthropicו-claude-agent-sdkב-venv נקי, ו-ANTHROPIC_API_KEYמוגדר כ-env var. ↪ סקשן setup, שלבים 1-2 - הרצתי
hello_agent.pyוקיבלתי תשובה אמיתית + שורתusageעם input/output tokens. ↪ סקשן setup, שלב 3 - אני יכול להסביר במשפט למה המודל stateless ולמה ה-harness הוא שמחזיק את ה-state. ↪ סקשן stateless + שאלה 1 ב"בדוק את עצמך"
- כתבתי/הרצתי את
harness.py— לולאה מפורשת בפחות מ-150 שורות עם max-turns. ↪ סקשן loop, חלקים 1+2+3 + תרגיל 1 - אני מזהה בלוג את ה-
tool_use, ה-tool_result, ואת ה-tool_use_idשמקשר ביניהם. ↪ פלט תרגיל 1 (turn 1-2) - ראיתי בעיניי את
input_tokensגדל מסבב לסבב (312→389→441 ב-3 סבבים). ↪ פלט תרגיל 1 (in=312, in=389, in=441) - הבנתי את שלושת תנאי העצירה: תשובה סופית, tool_use (להמשיך), max-turns (עצירה כפויה). ↪ סקשן maxturns, מסגרת "שלושת תנאי העצירה"
- חיברתי tool מותאם משלי (schema + handler) וראיתי את הסוכן בוחר בו לפי ה-description. ↪ סקשן custom-tool + תרגיל 3 (count_words)
- הרצתי את הלולאה גם דרך
query()של ה-SDK והשוויתי visibility מול ה-raw. ↪ סקשן sdk-loop + תרגיל 2 - הרצתי את הלולאה ב-mock mode (
HARNESS_MOCK=1) וראיתיin=0 out=0. ↪ סקשן dev-mode + תרגיל 4 - אני מבין למה Bash מובנה מסוכן בלי policy gate, ועד פרק 4 אני מריץ אותו רק ב-sandbox. ↪ סקשן builtin, warning + טבלת סיכונים
- אני יודע למה לאוטומציה צריך API key auth ולא subscription (מעבר 15 ביוני 2026). ↪ סקשן setup, מסגרת "API key מול subscription" + שאלה 4
- אני יכול לנקוב ב-3 דברים שהלולאה עדיין לא יודעת ואיזה פרק מתקן כל אחד. ↪ סקשן limits, טבלת 5 חולשות
- עניתי על 5 שאלות "בדוק את עצמך" בלי לחזור לטקסט. ↪ בלוק "בדוק את עצמך — 5 שאלות" (לפני סיכום)