2 Foundation · פרק 2 מתוך 8

אנטומיה של Harness דק — בונים את לולאת ה-Agent מאפס

בפרק הקודם הבנת למה ה-harness אחראי ל-~70% מהביצועים. בפרק הזה תבנה אותו במו ידיך: לולאת agent מפורשת בפחות מ-150 שורות, מעל ה-Claude Agent SDK — שמקבלת goal, קוראת למודל, מריצה tools, מנהלת state בין סבבים, ועוצרת בתנאי ברור. בלי framework כבד. עם הבנה מלאה של כל iteration.

🧵 חוט הפרויקט

קודם (פרק 1 — "עליית ה-Harness Engineering"): פירקת את ה-harness ל-5 רכיביו ומיפית את נוף הכלים. עכשיו אתה יודע ש-loop, state, tool execution, context ו-constraints הם שכבות נפרדות שאתה בונה.

כאן (פרק 2): בונים את הרכיב הראשון והגרעיני — ה-loop. לולאת agent מפורשת עם state ו-tool execution. זה ה-skeleton שכל שאר הפרקים יוסיפו אליו שכבות.

אחר כך (פרק 3 — "קיר ה-Context"): הלולאה שתבנה כאן תקרוס כשה-context יתמלא. בפרק הבא נלמד את ה-Compaction Buffer ונבנה ניהול context שמונע איבוד היסטוריה באמצע ריצה.

מה תדע לעשות בסוף הפרק

מה צריך לפני שמתחילים

מה תייצר בפרק הזה (Deliverables)

  1. סביבת Claude Agent SDK עובדת + hello-agent — מריץ task ומחזיר תוצאה אמיתית מהמודל.
  2. harness.py — לולאת agent מפורשת בפחות מ-150 שורות: goal → loop(call model → run tools → feed back) → stop, עם max-turns.
  3. tool מותאם ראשון מחובר ל-harness (למשל get_time / read_file) שהסוכן קורא לו בריצה אמיתית — ואתה רואה את ה-tool_use וה-tool_result עוברים בלולאה.
  4. 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 קודם, אם ה-SDK עושה את זה? כי אי אפשר להנדס harness שאתה לא מבין. כל שכבה בקורס — ניהול context (פרק 3), governance (פרק 4), recovery (פרק 5) — נכנסת בתוך הלולאה הזו. אם הלולאה היא קופסה שחורה, אין לאן להכניס אותן. בונים raw כדי לדעת איפה כל בורג, ואז יודעים בדיוק מה ה-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 — סביבה וירטואלית והתקנה

terminal
# צור תיקיית פרויקט נקייה
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 ולא בקוד:

terminal
# הגדרת ה-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 ובלי לולאה:

hello_agent.py
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. הפלט נראה בערך כך:

output
לולאת 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 שקורא קובץ:

harness.py — (1) הגדרות ה-tools
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 הוא הקוד שלך שבאמת מריץ את הפעולה כשהמודל מבקש אותה. זה הצד שלך של החוזה:

harness.py — (2) הרצת ה-tools
# === הרצת ה-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 — הלולאה עצמה

וזה הרגע. הלולאה המפורשת. קרא אותה לאט — היא הגרעין של כל הקורס:

harness.py — (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. בוא נעבור על מה שקורה, כי כל מספר כאן יחזור בהמשך הקורס:

שתי החלטות עיצוב נוספות שכדאי לשים לב אליהן, כי הן יחזרו לאורך הקורס. ראשית, ה-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 על notes.txt — מה חוזר בפועל

כשהמודל קורא ל-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.) והרץ:

terminal
echo "ה-harness עובד. פרק 2." > notes.txt
python harness.py

הפלט שתראה (output נראה לעין):

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 קורה גם כאן, פשוט מתחת למכסה:

sdk_agent.py
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 נראה לעין):

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 שלישי שמייצר פלט נראה לעין — סופר מילים בקובץ:

harness.py — הוספת 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 חדש:

terminal
python harness.py
# שנה את ה-goal בקובץ ל:
# "כמה מילים יש בקובץ ./notes.txt?"

הפלט (output נראה לעין):

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) עובדת:

harness.py — דגל mock לפיתוח
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.

למה זה harness engineering ולא רק "נוחות" ה-seam שיצרנו — הפונקציה 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), והרץ:

terminal
HARNESS_MOCK=1 python harness.py

הפלט (output נראה לעין):

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 שמשפר את ההתנהגות — מוסיף כלל עצירה מפורש שמונע מהסוכן "לרחף":

system prompt כ-harness config (דוגמה מייצגת)
אתה סוכן הרץ בתוך 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 ובאגים:

  1. קודם mock. הרץ HARNESS_MOCK=1 ובדוק שהלוגיקה (צבירה, stop, dispatch) עובדת ב-0 tokens.
  2. קרא את הלוג. ודא שכל tool_use מקבל tool_result תואם, ושמספר ה-messages גדל כצפוי (זוגות).
  3. ריצת אמת אחת קצרה. max-turns נמוך (2-3), goal פשוט, API key auth. ודא ש-stop_reason ו-usage נראים תקינים.
  4. בדוק את הבלם. תן goal שמחייב לולאה (tool שתמיד נכשל) וודא שה-max-turns תופס — לא ש"מתישהו ייעצר".
  5. רק אז הרץ end-to-end מלא. ה-tokens האמיתיים נשמרים לרגע שהלוגיקה כבר מאומתת.

⭐ Just One Thing

אם תיקח רק דבר אחד מהפרק הזה: המודל הוא stateless — ה-harness הוא ה-state, וה-loop עם max-turns הוא הלב שלו. כל "זיכרון" של הסוכן הוא רשימת ה-messages שאתה צובר ושולח מחדש בכל סבב; וכל harness שיש בו tools חייב max-turns, אחרת הוא לא לולאת agent — הוא פצצת tokens. הפנם את שני אלה, וכל שאר הקורס הוא רק שכבות שנכנסות לתוך הלולאה הזו.

בדוק את עצמך — 5 שאלות

  1. למה כל קריאה למודל שולחת מחדש את כל ההיסטוריה? מה היה קורה אם היינו שולחים רק את ההודעה האחרונה?
  2. מה ההבדל בין stop_reason == "tool_use" ל-stop_reason == "end_turn", ואיך כל אחד משפיע על ההחלטה אם להמשיך את הלולאה?
  3. למה tool_result חייב tool_use_id, ומה נשבר אם תחזיר tool_result בלי id תואם, או בכלל לא?
  4. הרצת ריצה אוטומטית על subscription auth ב-20 ביוני 2026 וקיבלת rate limit פתאומי. מה הסיבה ומה התיקון?
  5. מתי תבחר בלולאה raw מעל הצורך ב-query() של ה-SDK? תן דוגמה אחת קונקרטית מה-roadmap של הקורס.

סיכום הפרק

  1. המודל stateless, ה-harness הוא ה-state. אין זיכרון בצד Anthropic; ה"זיכרון" הוא רשימת ה-messages שאתה צובר ושולח מחדש בכל קריאה. לכן ה-state גדל בכל turn — וזה הזרע לקיר ה-context בפרק 3.
  2. הלולאה היא 6 צעדים: build messages → call model → בדוק stop_reason → אם tool_use הרץ tools ואסוף tool_result → החזר ל-state → repeat, עד תשובה סופית או max-turns. פחות מ-150 שורות.
  3. 4 סוגי messages: system (config), user (goal + tool_results), assistant (תגובה + tool_use), tool_result (מקושר ב-tool_use_id). כל tool_use חייב tool_result תואם.
  4. max-turns הוא הבלם הראשון — לא feature, אלא תנאי קיום. בלעדיו סוכן נתקע יכול לקרוא לאותו tool 6+ פעמים ולשרוף את כל ה-budget.
  5. tool מותאם = schema + handler. ה-description הוא ה-interface; הוא לבדו קובע אם המודל יבחר ב-tool הנכון בזמן הנכון.
  6. ה-Claude Agent SDK עוטף את אותה לולאה ב-query() עם built-in tools (file/bash/search) ו-max_turns מובנה — בונים מעליו, לא במקומו, כדי לשמור על שליטה בלולאה.
  7. 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.