7 Integration · פרק 7 מתוך 8

זיכרון ו-Dreaming — שיפור עצמי אסינכרוני בלי לשרוף מיליוני tokens

ה-harness שלך כבר רץ, מתזמר subagents ותופס כשלים. אבל הוא סובל מ-amnesia: כל ריצה מתחילה מאפס וחוזרת על אותן טעויות. בפרק הזה נחבר לו זיכרון מתמשך, נפעיל את Claude Dreaming לקונסולידציה אסינכרונית — ונרסן את ה-token burn לפני שהוא יבלע מיליוני tokens ברקע בשקט.

🧵 חוט הפרויקט

בפרק הקודם (6 — Orchestration רב-סוכני) בנית supervisor loop שמפצל 2-3 subagents עם max-turn limits ו-budget caps, והרצת Claude Agent Teams מקבילים. עכשיו יש לך צוות שעובד — אבל הוא שכחן: כל ריצה מתחילה מאפס.

בפרק הזה (7) אתה נותן לצוות זיכרון: memory store מובנה שריצות כותבות אליו וקוראות ממנו, ו-Claude Dreaming שמקנסל את הזיכרון הזה ברקע — כך שהצוות לומד מטעויותיו במקום לחזור עליהן.

בפרק הבא (8 — צוות שעובד לבד) תחבר את הכל — supervisor, context, tools, gates, observability, recovery וה-memory מהפרק הזה — ל-harness שלם ל-production, ותחליט in-process מול hosted.

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

📋 דרישות קדם

📦 תוצרים — מה תיצור/י בפרק הזה

  1. memory store מובנה — schema של שלוש קטגוריות (decisions / failures / learned patterns) עם read/write מתוך ה-harness loop, וב-store נפרד מה-context (קובץ JSON / SQLite / KV).
  2. Dreaming job מתוזמן — קונפיגורציה שמריצה curation אסינכרונית על transcripts עם scope ותקציב מוגדרים, עטופה מאחורי abstraction עם fallback.
  3. מדידת before/after — טבלה שמראה ירידה בחזרה-על-טעויות ועלייה באחוז הצלחה אחרי הפעלת Dreaming, עם עלות לריצה.
  4. self-improvement דל — מנגנון failure-summary תקופתי שמזריק patterns ל-system prompt בלי ה-feature ה-hosted, כ-fallback ל-preview.

למה harness בלי זיכרון חוזר על אותן טעויות

נתחיל מהבעיה, כי היא לא תאורטית. ה-harness שבנית עד עכשיו הוא stateless בין ריצות. בתוך ריצה אחת יש לו message-state (פרק 2) ו-context management (פרק 3) — אבל ברגע שהריצה נגמרת, הכל מתאדה. ה-decision החכם שהסוכן קיבל בריצה של אתמול? נעלם. ה-failure שתפסת יפה עם ה-structured failure record מפרק 5? נכתב ל-trace ב-Langfuse — ואז הסוכן הבא חוזר עליו בדיוק.

זה ההבדל בין stateless (חסר מצב) ל-stateful (בעל מצב) ברמת ה-מערכת, לא ברמת הריצה. המודל עצמו תמיד היה stateless — זו הסיבה שבפרק 2 בנינו harness שמחזיק את ה-state. עכשיו אנחנו מרחיבים את אותו עיקרון מ-"זיכרון בתוך ריצה" ל-"זיכרון בין ריצות".

פי 6

שיפור בקצב השלמת משימות (task completion rate) שדיווחה חברת Harvey אחרי שהפעילה Claude Dreaming לקונסולידציה אסינכרונית של זיכרון. מקור: course.research.json (key_2026_updates). הצוות לא קיבל מודל חזק יותר — הוא קיבל זיכרון.

זה מתחבר ישירות לטענת הליבה של הקורס: 70% מביצועי הסוכן חיים מחוץ למודל (פרק 1). זיכרון הוא דוגמה מובהקת ל-"מחוץ למודל" — אותו Claude, אבל harness שזוכר מנצח harness ששוכח. הפער של פי 6 אצל Harvey הוא בדיוק זה: לא שדרוג מודל, אלא שכבת harness שמצטברת מריצה לריצה.

חשוב להבין למה זה כל כך קל ליפול ל-amnesia. כשבנית את הלולאה בפרק 2, ה-state חי ב-RAM של התהליך — רשימת הודעות בזיכרון. ברגע שהתהליך מסתיים, ה-RAM מתפנה, וה-state נעלם. זו לא תקלה, זו ברירת המחדל. כל מי שבונה harness דק מקבל amnesia בחינם, בלי לבקש. כדי שלא תהיה amnesia, צריך להוסיף שכבה מפורשת שמוציאה state נבחר אל מחוץ לתהליך — בדיוק כמו שבפרק 3 הוצאנו state כבד ל-scratchpad חיצוני כדי לעקוף את ה-compaction. ה-memory store הוא אותו רעיון, מורחב מ-"בין turns" ל-"בין ריצות".

שלוש דרכים שבהן amnesia פוגעת בך, מהזולה ליקרה:

סוג הנזקמה קורה בפועלהעלות
חזרה על failure ידועהסוכן מנסה שוב פעולה שכבר נכשלה אתמול (אותו endpoint שמחזיר 403, אותו פורמט שגוי)tokens של ניסיון כושל + circuit-breaker (ch5) שנדלק שוב
איבוד decision מנצחהסוכן גילה אתמול שכדאי לקרוא ל-tool X לפני tool Y — ושכח. היום הוא מגלה את זה מחדשturns מיותרים בכל ריצה, latency מצטבר
חוסר עקביות בין ריצותאותה משימה מקבלת תשובות שונות בכל פעם כי אין baseline נלמדחוסר אמון, צורך ב-human review שמבטל את האוטונומיה

⚡ עשה/י עכשיו (2 דקות)

פתח/י את ה-traces האחרונים שלך ב-Langfuse (מפרק 5). חפש/י שתי ריצות שונות שבהן הסוכן נכשל באותו tool מאותה סיבה. מצאת? זה בדיוק ה-failure שזיכרון היה מונע. רשום/י לעצמך: כמה tokens עלתה החזרה השנייה?

נקודה אחרונה לפני שנבנה: זיכרון הוא לא תחליף ל-context management (פרק 3), הוא שכבה משלימה. ה-context מחזיק את מה שרלוונטי לריצה הנוכחית; ה-memory store מחזיק את מה שנלמד על פני ריצות. שני המנגנונים נפגשים בנקודת ה-READ: בתחילת ריצה, אתה שולף מה-store הקבוע מעט לקחים ומזריק אותם ל-context הזמני. השאלה הקריטית — וזו השאלה שמפרידה harness שמשתפר מ-harness שקורס — היא כמה מזריקים. נחזור לזה שוב ושוב.

persistent memory store — לא "זיכרון חופשי", אלא מבנה

הטעות הראשונה של כל מי שמתחיל עם memory היא לחשוב עליו כעל "להחזיק את כל ההיסטוריה". זה לא זיכרון, זה אגירה — ובדיוק מה שיפוצץ לך את ה-context (פרק 3). persistent memory store טוב הוא מובנה: יש לו schema, קטגוריות, ושאילתות. אתה לא שומר "מה קרה", אתה שומר "מה נלמד".

שלוש קטגוריות הליבה — בדיוק שלוש, כי יותר זה bloat ופחות זה לא מספיק:

קטגוריהמה נשמרמתי נכתבמתי נקרא
decisionsהחלטה שהתקבלה + הנימוק ("קרא ל-enrich לפני draft כי...")אחרי שהסוכן בחר מסלול שעבדבתחילת משימה דומה
failuresה-structured failure record מ-ch5: input, error, context, turnכש-circuit-breaker / recovery נדלק (ch5)לפני ניסיון tool/מסלול שכבר נכשל
learned patternsתובנה מאוחדת שחוצה הרבה ריצות ("ה-API של Apollo מחזיר rate-limit אחרי 50 קריאות/דקה")רק ע"י Dreaming — לא ע"י ריצה בודדתבתחילת כל משימה רלוונטית

שים/י לב להבחנה הקריטית בשורה האחרונה: decisions ו-failures נכתבים ע"י ריצות בודדות (raw material), אבל learned patterns נכתבים רק ע"י Dreaming — תהליך הקונסולידציה שנראה בהמשך. ריצה בודדת לא יודעת אם משהו הוא pattern או מקרה. רק מבט-על על הרבה transcripts יודע.

למה דווקא שלוש קטגוריות ולא שבע? כי כל קטגוריה נוספת היא עוד החלטה שאתה צריך לקבל בכל write — "האם זה decision או observation או hint?" — ובלבול בקטגוריות הופך את ה-store ללא-ניתן-לשליפה. שלוש קטגוריות ממפות נקי לשלושה מקורות: decisions באים מהצלחות, failures באים מה-recovery layer (ch5), ו-patterns באים מ-Dreaming. אם אתה מתלבט לאיזו קטגוריה משהו שייך — כנראה הוא לא צריך להישמר בכלל. זיכרון טוב הוא קמצן.

ה-schema בקוד

נגדיר schema מינימלי וברור. דוגמה מייצגת ב-Python (ה-store עצמו יכול להיות קובץ JSON, SQLite, או KV — לא משנה כרגע):

# memory_store.py  — דוגמה מייצגת
from dataclasses import dataclass, asdict
from enum import Enum
import json, time, hashlib

class MemoryKind(str, Enum):
    DECISION = "decision"
    FAILURE  = "failure"
    PATTERN  = "pattern"   # נכתב רק ע"י Dreaming

@dataclass
class MemoryItem:
    kind: MemoryKind
    task_type: str          # למשל "lead_enrichment" — מפתח השליפה
    content: str            # התובנה עצמה, קצרה
    evidence: str           # מאיפה היא באה (trace_id / turn)
    created_at: float
    weight: float = 1.0     # Dreaming יעלה/יוריד לפי כמה זה חזר

    def key(self) -> str:
        h = hashlib.sha1(f"{self.kind}:{self.task_type}:{self.content}".encode())
        return h.hexdigest()[:12]

class MemoryStore:
    def __init__(self, path="memory.json"):
        self.path = path
        self._load()

    def _load(self):
        try:
            self.items = json.load(open(self.path))
        except FileNotFoundError:
            self.items = {}

    def write(self, item: MemoryItem):
        self.items[item.key()] = asdict(item)   # dedup לפי key
        json.dump(self.items, open(self.path, "w"), ensure_ascii=False, indent=2)

    def read(self, task_type: str, kinds=None, limit=5):
        rows = [i for i in self.items.values() if i["task_type"] == task_type]
        if kinds:
            rows = [i for i in rows if i["kind"] in kinds]
        rows.sort(key=lambda i: i["weight"], reverse=True)
        return rows[:limit]     # רק ה-top-N הרלוונטיים — לא הכל

שתי החלטות עיצוב חשובות בקוד הזה. ראשית, key() עושה dedup אוטומטי — אותו לקח לא נכתב פעמיים. שנית, read() מחזיר limit=5 top-N לפי weight, לא את כל הזיכרון. זה הסעיף שמונע את הטעות הקלאסית של "לזרוק את כל ה-store ל-context".

איזה backend? JSON, SQLite, או KV

שים/י לב שב-schema למעלה ה-store הוא קובץ JSON — וזה בכוונה. אתה לא צריך vector database ביום הראשון. רוב מי שמתחיל עם memory קופץ ישר ל-embeddings ול-semantic search, ומגלה חודש אחרי שהוא בנה תשתית מסובכת כדי לשלוף 5 שורות. בחר/י backend לפי נפח, לא לפי אופנה:

backendמתי מתאיםאיך שולפיםמגבלה
JSON fileעד ~כמה אלפי פריטים, harness יחידטעינה לזיכרון + סינון לפי task_typeטעינה מלאה בכל read; לא concurrent-safe
SQLiteעשרות אלפי פריטים, כתיבות מ-subagents מקביליםSELECT ... WHERE task_type=? ORDER BY weightעדיין מקומי; לא מבוזר
KV / Redisharness מבוזר, כמה מכונותkey לפי task_type → list ממויןתפעול נוסף; overkill ל-MVP
vector DBשליפה סמנטית כשאין task_type ברורembedding של ה-goal → nearest patternsמורכבות + עלות embeddings; דחה/י עד שמוכח שצריך

הכלל: task_type הוא ה-index שלך, לא embedding. כל עוד אתה יודע מראש לאיזה סוג משימה הריצה שייכת (וברוב ה-harnesses אתה יודע — אתה זה שמפעיל אותה), שליפה לפי task_type מדויקת, זולה ודטרמיניסטית. vector search שמור/י לרגע שבו אתה לא יודע את הקטגוריה מראש. ה-syllabus וה-research מזכירים שלמפתח ישראלי יש כאן נקודה נוספת: עברית עשירה מורפולוגית, ואם בכל זאת תלך לכיוון retrieval סמנטי על תוכן עברי — השתמש/י ב-tokenizer או lemmatizer מותאם-עברית, אחרת ה-context יתנפח וה-retrieval ייכשל (local_market_notes ב-research).

read/write בלולאה — בלי לזהם את ה-context

איפה בלולאה (פרק 2) משולב הזיכרון? בשתי נקודות בלבד:

# בתוך ה-harness loop — דוגמה מייצגת
def run_task(goal, task_type, store: MemoryStore):
    # 1) READ — בתחילת המשימה, מזריקים patterns + failures רלוונטיים בלבד
    lessons = store.read(task_type, kinds=["pattern", "failure"], limit=5)
    memory_block = "\n".join(f"- {l['content']}" for l in lessons)
    system = BASE_SYSTEM + (f"\n\nלקחים מריצות קודמות:\n{memory_block}" if lessons else "")

    result = agent_loop(goal, system_prompt=system)   # הלולאה מפרק 2

    # 2) WRITE — בסוף, רושמים decision שעבד / failure שנתפס
    if result.succeeded and result.key_decision:
        store.write(MemoryItem(MemoryKind.DECISION, task_type,
                    result.key_decision, result.trace_id, time.time()))
    for f in result.captured_failures:        # מ-ch5
        store.write(MemoryItem(MemoryKind.FAILURE, task_type,
                    f.summary, f.trace_id, time.time()))
    return result

שים/י לב: ה-READ מזריק 5 שורות לכל היותר ל-system prompt — לא מסמך. זה ההבדל בין memory שמשפר ל-memory שמפוצץ context. נחזור לזה בקטע ה-frameworks.

למה ל-system prompt ולא ל-user message? כי לקחים נלמדים הם config של ה-harness, לא חלק מהמשימה — בדיוק כמו שבפרק 2 ראינו שה-system prompt הוא חלק מהנדסת ה-harness, לא רק "הוראות". לקח כמו "ה-API של Apollo דורש header מסוים" הוא ידע מערכתי שמלווה את הסוכן לאורך כל הריצה, לא בקשה חד-פעמית. הזרקה ל-system גם שורדת טוב יותר את ה-compaction (פרק 3): ברוב ה-harnesses ה-system prompt מוגן מדחיסה, בעוד הודעות user ישנות נדחסות ראשונות. כלומר ה-READ ל-system לא רק נכון לוגית — הוא גם עמיד יותר.

⚡ עשה/י עכשיו (2 דקות)

קח/י את ה-system prompt הנוכחי של ה-harness שלך וספור/י כמה tokens הוא תופס (בערך: מילים × 1.5 לעברית מעורבת). עכשיו תכנן/י: כמה לקחים אתה מוכן להוסיף לו לפני שזה "כבד מדי"? אם אין לך מספר בראש — קבע/י אחד עכשיו (למשל 5 לקחים / ~150 tokens). המספר הזה הוא ה-limit שלך.

⚠️ טעות נפוצה: לזרוק את כל ה-memory store ל-context של כל ריצה

"בוא נזריק את כל מה שהסוכן למד אי-פעם, ליתר ביטחון." זה בדיוק מה שמפיל אותך: ה-store גדל לאלפי שורות, אתה דוחף אותן ל-context בתחילת כל ריצה, מאיץ את ה-compaction (פרק 3, סף 83.5%) — וגרוע מכך, מזהם את ההחלטות עם לקחים לא רלוונטיים. הסוכן "זוכר" יותר מדי ומחליט גרוע יותר. הנגד: read() עם task_type ו-limit — רק ה-patterns הרלוונטיים למשימה הנוכחית, top-5.

🛠️ תרגיל 1 — בנה/י memory store ותראה/י write→read עובד

מטרה: store מובנה שכותב 3 פריטים (אחד מכל קטגוריה) וקורא בחזרה רק את הרלוונטיים — עם פלט גלוי.

  1. צור/י memory_store.py עם ה-starter הבא (מינימום הכרחי; key() ו-dedup מוסברים בהמשך):
# memory_store.py — starter מינימלי לתרגיל 1 (מוכן לרוץ).
import json, time, hashlib
from dataclasses import dataclass, asdict
from enum import Enum

class MemoryKind(str, Enum):
    DECISION = "decision"; FAILURE = "failure"; PATTERN = "pattern"

@dataclass
class MemoryItem:
    kind: MemoryKind; task_type: str; content: str
    evidence: str = ""; created_at: float = 0.0; weight: float = 1.0
    def key(self) -> str:
        return hashlib.sha1(f"{self.kind}:{self.task_type}:{self.content}".encode()).hexdigest()[:12]

class MemoryStore:
    def __init__(self, path="memory.json"):
        self.path = path
        try: self.items = json.load(open(self.path))
        except FileNotFoundError: self.items = {}
    def add(self, item: MemoryItem):                  # add — ה-API שתרגיל 1 דורש
        self.items[item.key()] = asdict(item)
        json.dump(self.items, open(self.path,"w"), ensure_ascii=False, indent=2)
        return item.key()
    write = add                                       # alias — הפרק משתמש גם בשם הזה
    def read(self, task_type, kinds=None, limit=5):
        rows = [i for i in self.items.values() if i["task_type"] == task_type]
        if kinds: rows = [i for i in rows if i["kind"] in kinds]
        rows.sort(key=lambda i: i["weight"], reverse=True)
        return rows[:limit]

if __name__ == "__main__":
    s = MemoryStore()
    s.add(MemoryItem(MemoryKind.DECISION, "lead_enrichment", "קרא enrich לפני draft", "t1", time.time(), 2.0))
    s.add(MemoryItem(MemoryKind.FAILURE,  "lead_enrichment", "Apollo enrich 403 בלי X-Version header", "t2", time.time(), 1.5))
    s.add(MemoryItem(MemoryKind.PATTERN,  "lead_enrichment", "enrich לפני draft חוסך turn", "t3", time.time(), 3.0))
    print(f"store size = {len(s.items)}")
    for h in s.read("lead_enrichment", kinds=["pattern","failure"]):
        print(f"  [{h['kind']}|w={h['weight']}] {h['content']}")
  1. הרץ/י python memory_store.py — אתה אמור/ה לראות store size = 3 ושתי שורות (pattern עם weight 3.0 למעלה, failure עם 1.5 אחריו). ה-decision סונן בגלל kinds.
  2. צור/י demo_memory.py שמוסיף decision רביעי עם task_type="docs", קורא store.read("lead_enrichment", kinds=["pattern","failure"]), ומדפיס את התוצאה.
  3. ודא/י שה-decision של docs לא חוזר (סינון לפי task_type עובד), ושה-decision של lead_enrichment גם לא (סינון לפי kinds עובד).

פלט גלוי שתראה/י:

$ python demo_memory.py wrote 4 items, store size = 4 read(lead_enrichment, [pattern,failure]) → 2 items: [failure] Apollo enrich endpoint מחזיר 403 ללא API-Version header [pattern] להריץ enrich לפני draft — חוסך turn אם אין נתונים ✓ docs decision filtered out (task_type) ✓ lead_enrichment decision filtered out (kind)

משקל, דעיכה ואיכות פריט — מה הופך pattern לשימושי

עד עכשיו ראינו איך לאחסן פריט זיכרון. אבל לא כל פריט שווה את אותו משקל. weight ב-schema הוא השדה הכי חשוב בכל ה-store, והוא זה שמפריד בין זיכרון שמחדד את הסוכן לזיכרון שמרעיש אותו. הנה הכללים שהופכים pattern לשימושי:

קריטריוןלמה זה משנהאיזה weight זה מצדיק
ספציפי ל-tool/endpoint קונקרטי"Apollo enrich צריך header X" עוזר יותר מ-"API calls לפעמים נכשלים"גבוה (3+)
חוזר על פני הרבה ריצותDreaming יעלה weight ככל שיותר transcripts מאשריםעולה ל-5+
מנוסח כפעולה, לא כתיאור"קרא enrich לפני draft" עדיף על "הזמנים משתנים"מקבל boost ב-curation
בר-אימותיש evidence (trace_id) שמראה שזה קרהנשמר; בלי evidence — מסומן candidate למחיקה
ישן ולא-בשימושפריט שלא נקרא 60+ יום כנראה לא רלוונטי יותרדועך לאפס, מועבר לארכיון

הכלל שאתה רוצה להפנים: pattern טוב הוא pattern שאתה יכול לבדוק. אם אתה לא יכול להצביע על trace_id ולהגיד "הנה איפה שזה קרה", הפריט לא ראוי ל-store. ה-store הוא לא יומן — הוא תובנה מאומתת.

מנגנון דעיכה מומלץ (decay), בדוגמה מייצגת:

# decay.py — אופציונלי, מופעל ב-curation. דוגמה מייצגת
import time
HALF_LIFE_DAYS = 60        # אחרי 60 יום בלי שימוש, weight יורד לחצי
ARCHIVE_AFTER  = 180       # אחרי חצי שנה בלי שימוש → ארכיון (לא מחיקה)

def decay(weight: float, last_used: float, now: float) -> float:
    days_unused = (now - last_used) / 86400
    factor = 0.5 ** (days_unused / HALF_LIFE_DAYS)
    return round(weight * factor, 3)

def should_archive(weight: float, last_used: float, now: float) -> bool:
    days_unused = (now - last_used) / 86400
    return (days_unused > ARCHIVE_AFTER) and (weight < 0.5)

החצי-חיים (half-life) הוא המודל הנכון לזיכרון סוכן, ולא "תאריך תפוגה קשיח" — כי לקחים לא מתיישנים ביום מסוים, הם דועכים בהדרגה. אחרי חודשיים ללא שימוש, weight=2 יורד ל-1. אחרי חצי שנה, ל-0.5 — ואז הוא מועבר לארכיון, לא נמחק (תמיד אפשר לחפש בו). הסיבה לדעיכה: pattern שאף ריצה לא הזדקקה לו ב-60 יום כנראה לא רלוונטי, ועדיף לפנות מקום לחדשים.

⚠️ טעות נפוצה: "אני לא אמחק כלום — אולי זה יצטרך יום אחד"

store שלא מדלל לעולם הופך ל-archeological dig — מאות פריטים, רובם לא רלוונטיים, read() מחזיר את הישנים במקום את הרלוונטיים, וה-READ לסוכן מקבל זבל במקום signal. הנגד: דעיכה עם half-life + ארכיון. מחיקה הרסנית (אין דרך חזרה), ארכיון בטוח (אפשר לחפש). ה-store שלך הוא לא מוזיאון — הוא כלי עבודה.

canonicalization של failure signatures

בעיה שלא רואים עד שמתחילים לספור failures: אותו כשלון מופיע ב-20 וריאציות קלות של אותו טקסט. "API returned 403", "Got 403 from API", "HTTP 403 Forbidden", "403" — כולם אותו דבר, אבל ה-key שלך (sha1 על ה-content) יוצר 4 פריטים נפרדים, וה-Dreaming רואה "4 failures שונים" במקום "failure אחד שחוזר 20 פעמים".

הפתרון: canonicalize לפני שמכניסים ל-content. בערך:

# canonicalize.py — נרמול signatures לפני כתיבה
import re

def failure_signature(error_text: str) -> str:
    """מחזיר חתימה יציבה של failure — בלי זמנים, IDs, מספרי שורה"""
    s = error_text.lower()
    s = re.sub(r"0x[0-9a-f]+", "<HEX>", s)             # כתובות זיכרון
    s = re.sub(r"\b\d{4,}\b", "<NUM>", s)               # מספרים ארוכים (IDs, ports)
    s = re.sub(r"\b\d{1,3}(?:\.\d{1,3}){3}\b", "<IP>", s)  # כתובות IP
    s = re.sub(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}", "<TS>", s)  # timestamps
    s = re.sub(r"\s+", " ", s).strip()
    return s[:120]   # חתוך — רק החלק המבדיל

עכשיו "Apollo 403 at 10.0.0.5" ו-"apollo 403 at 192.168.1.1" מצטמצמים לאותה חתימה, ה-key זהה, ה-dedup עובד, ו-Dreaming רואה failure אחד שחוזר במקום עשרים. בלי canonicalization, ה-store שלך יציג "המון נושאים" כשבעצם יש מעטים, וה-patterns ש-Dreaming יחלץ יהיו רועשים. זו דוגמה קטנה לכלל גדול: ב-store של זיכרון, עקביות של ה-input קובעת את איכות ה-output.

⚡ עשה/י עכשיו (3 דקות)

קח/י 5 שגיאות אמיתיות מ-Langfuse (או המצא/י 5). הרץ/י אותן דרך failure_signature() בראש. כמה signatures ייחודיות יצאו? אם יצאו 5 — אין לך עדיין כפילויות. אם יצאו 2-3 — יש לך pattern שמחכה להיכתב.

Claude Dreaming — מה זה ומה הוא עושה בפועל

עד עכשיו ה-store שלנו צובר decisions ו-failures גולמיים. אבל מי הופך 100 failures גולמיים ל-pattern אחד מנוסח? כאן נכנס Claude Dreaming.

לפי course.research.json (feature: "Claude Dreaming", Research Preview שהושק ב-6 במאי 2026 בכנס Code with Claude 2026): Dreaming הוא תהליך רקע אסינכרוני (asynchronous background process) ל-Claude Managed Agents, שסוקר transcripts של sessions קודמים ו-memory stores — בזמן שהסוכן לא רץ. השם לא מקרי: כמו שינה אצל בני אדם מקנסלת זיכרונות מהיום, Dreaming מקנסל את הזיכרון של הסוכן ברקע.

מה Dreaming עושה בפועל, לפי המקור — ארבע פעולות:

פעולהמה זה אומרדוגמה
pattern extraction
חילוץ patterns
מזהה התנהגות חוזרת על פני הרבה transcripts"ב-12 ריצות, קריאה ל-Apollo נכשלה ב-403 כשחסר header" → pattern
contradiction resolution
פתרון סתירות
שני decisions סותרים → מכריע לפי עדויותריצה אחת אמרה "enrich קודם", אחרת "draft קודם" → מכריע לפי מי הצליח יותר
dedup
גזימת כפילויות
מאחד פריטים זהים/דומים לאחד עם weight גבוהאותו לקח שנרשם 8 פעמים → פריט אחד, weight=8
consolidated insights write-back
כתיבת תובנות מאוחדות
כותב את התוצאה בחזרה ל-memory store מובנהה-pattern החדש נכנס ל-store, מוכן לקריאה בריצה הבאה

הנקודה החשובה: Dreaming הוא אופרטור על ה-memory store. ה-harness שלך כותב גולמי (decisions, failures) במהלך ריצות; Dreaming רץ אחר כך, מזקק את הגולמי ל-learned patterns, וכותב בחזרה. הריצה הבאה קוראת patterns מזוקקים — לא רעש.

contradiction resolution — דוגמה מוחשית

מבין ארבע הפעולות, פתרון סתירות הוא הכי לא-טריוויאלי, אז שווה לראות אותו על דוגמה. נניח שה-store צבר את שני ה-decisions הבאים מ-12 ריצות שונות של lead_enrichment:

decisionנרשם ב-תוצאת הריצה
"קרא ל-enrich לפני draft"7 ריצות6 הצליחו, 1 נכשלה
"קרא ל-draft לפני enrich"5 ריצות2 הצליחו, 3 נכשלו

ריצה בודדת לא יכולה להכריע — היא ראתה רק את עצמה. אבל Dreaming, שמסתכל על כל ה-12 ביחד, רואה את התמונה: enrich-קודם מצליח ב-86% מהמקרים מול 40% ל-draft-קודם. הוא פותר את הסתירה: כותב pattern יחיד — "enrich לפני draft (86% הצלחה על פני 7 ריצות)" עם weight גבוה, ומסמן את ה-decision הנגדי כ-deprecated. בריצה הבאה, ה-READ שולף את ה-pattern המנצח בלבד. זה ההבדל בין store שצובר רעש סותר ל-store שמתכנס לאמת.

שים/י לב למה זה חייב להיות Dreaming ולא הלולאה: ההכרעה דורשת ספירה על פני transcripts רבים — בדיוק מה שריצה בודדת לא רואה. זו אותה הבחנה כמו ב-framework של dedup: מבט-על מול מבט-נקודה.

🧭 Framework: האם הזיכרון הזה צריך Dreaming או write ישיר?

לפני שאתה כותב פריט זיכרון, החלט מי הכותב הנכון:

⚡ עשה/י עכשיו (3 דקות)

קח/י 3 failures מה-store של תרגיל 1 (או המצא/י). בלי קוד — רק על נייר: איזה מהם הוא מקרה בודד (write ישיר) ואיזה נראה כמו pattern שחוזר (עבודת Dreaming)? ההבחנה הזו היא כל ההבדל בין store מזוהם ל-store מזוקק.

ה-Dreaming Token Burn — הסכנה השקטה

עכשיו החלק שאי אפשר לדלג עליו. לפי course.research.json (gotchas): "The 'Dreaming' Token Burn: Asynchronous background memory curation runs Claude over up to 100 past session transcripts, burning millions of tokens."

תעצור/י על המספר הזה. ריצת Dreaming אחת סוקרת עד 100 transcripts. transcript של session אמיתי הוא לא קצר — הוא יכול להיות עשרות אלפי tokens. 100 כאלה × עיבוד ע"י Claude = מיליוני tokens לריצת curation אחת. וזה רץ ברקע, אסינכרוני, כשאתה לא מסתכל. החשבון מתפוצץ בשקט.

~100

transcripts שריצת Dreaming אחת יכולה לסקור. כל transcript = עשרות אלפי tokens. מקור: course.research.json (gotchas + topic_risk_flags). זה הכוח של Dreaming — וגם בדיוק מקור ה-token burn.

הסיבה שזה מסוכן יותר מ-cost explosion של subagents (פרק 6): שם ראית את ה-tokens נשרפים בזמן אמת, בריצה שאתה מסתכל עליה. כאן זה רקע. תזמנת Dreaming "אחרי כל ריצה כדי שתמיד יהיה חכם" — ופתאום, כל ריצה רגילה גוררת ריצת curation של מיליוני tokens. עשר ריצות ביום = עשר ריצות curation = עשרות מיליוני tokens ביום שאתה לא רואה עד החשבון.

⚠️ טעות נפוצה: לתזמן Dreaming אחרי כל ריצה "כדי שתמיד יהיה חכם"

זו הטעות שתעלה לך הכי הרבה. curation על עד 100 transcripts בכל פעם שריצה מסתיימת שורפת מיליוני tokens ברקע — לא פעם, אלא בכל ריצה. הנגד: מתזמנים בתדירות מרוסנת (לילי/שבועי, לא after-each-run) עם scope מוגבל (כמה transcripts, ולא תמיד 100) ותקציב background tokens מוגדר מראש. רוב הערך של Dreaming מגיע מ-batch גדול מדי פעם, לא מ-curation מתמיד.

שלושת בקרי ה-burn

ריסון ה-burn מתבסס על שלושה בקרים — תזכור/י אותם כשלישייה, כי כל אחד לבד לא מספיק:

בקרמה הוא שולטהמלצת ברירת מחדל (דוגמה מייצגת)
תדירות (frequency)כל כמה זמן Dreaming רץלילי או שבועי — לא after-each-run
scopeכמה transcripts נכללים בריצהרק חדשים-מאז-הריצה-האחרונה, מוגבל ל-cap (למשל 30), לא תמיד 100
תקציב background tokensתקרת tokens קשיחה לריצת curationbudget cap שעוצר את ה-curation אם חרג — בדיוק כמו max-turns בפרק 2/6

שים/י לב כמה זה דומה למה שכבר בנית: תקציב background tokens הוא ה-budget cap מפרק 6, רק שמופעל על curation במקום על subagent. אותו עיקרון של harness engineering — שום תהליך לא רץ בלי תקרה. אם הפנמת בפרק 6 שכל subagent חייב max-turns ו-budget cap, אז Dreaming הוא פשוט עוד "subagent" — היקר ביותר — וחל עליו אותו חוק.

ובכל זאת, יש כאן הבדל מהותי אחד ש-makes it worse: subagent רץ בקדמת הבמה, בריצה שאתה מסתכל עליה, ואם הוא בורח אתה רואה את ה-latency. Dreaming רץ ברקע, מנותק מכל ריצה ספציפית. אין מי ש"מרגיש" שהוא יקר עד שמגיע החשבון. לכן שני בקרים מתוך השלושה — תדירות ו-scope — קיימים לא רק כדי לחסוך, אלא כדי למנוע מהעלות להיות בלתי-נראית. אתה לא יכול לסמוך על תחושה כשהתהליך אסינכרוני.

# dreaming_config.py — ריסון ה-burn. דוגמה מייצגת
# שים לב: ה-API המדויק ב-research preview ועלול להשתנות — ראה fallback בהמשך
from datetime import datetime, timedelta

DREAMING = {
    "schedule":        "0 3 * * 0",     # cron: כל יום ראשון 03:00 — לא after-run
    "max_transcripts": 30,              # scope cap, לא 100
    "only_since_last": True,            # רק transcripts חדשים
    "token_budget":    2_000_000,       # תקרה קשיחה לריצת curation אחת
    "scope_filter":    {"task_type": "lead_enrichment"},  # לקנסל ממוקד
}

def estimate_burn(num_transcripts, avg_tokens_per_transcript=40_000):
    """אומדן גס לפני שמפעילים — דוגמה מייצגת"""
    est = num_transcripts * avg_tokens_per_transcript
    capped = min(est, DREAMING["token_budget"])
    return est, capped

שים/י לב לפונקציה estimate_burn: לפני שאתה מפעיל Dreaming, אתה אומד כמה זה יעלה. 30 transcripts × 40K ≈ 1.2M tokens. עם budget cap של 2M אתה בטוח. בלי האומדן הזה — אתה מגלה את העלות מהחשבון.

שתי החלטות תזמון נוספות ששווה לקבל מודע, לא בדיפולט:

וקריטי: תזמון Dreaming הוא לא after-each-run, אבל גם לא "פעם בכמה שאזכר". זה cron קבוע — לילי או שבועי — כי curation לא-מתוזמן הוא או תכוף מדי (burn) או נדיר מדי (ה-store מתיישן וה-patterns לא מתעדכנים). ה-"0 3 * * 0" בקוד למעלה אומר "כל ראשון ב-03:00", שעה שבה ממילא אין עומס — זו נקודת התזמון הקלאסית לעבודת רקע כבדה.

⚡ עשה/י עכשיו (2 דקות)

חשב/י בראש: אם הפרויקט שלך מייצר 5 transcripts ביום, וכל אחד ~40K tokens — כמה ייסקרו בריצת Dreaming שבועית עם only_since_last=True? (תשובה: ~35 transcripts × 40K ≈ 1.4M tokens). עכשיו בלי only_since_last, אחרי 3 חודשים: ~450 transcripts. רואה/ה למה ה-incremental לא אופציונלי?

🛠️ תרגיל 2 — מחשבון Dreaming burn עם budget cap

מטרה: כלי שמראה לך, לפני הפעלה, מה ההבדל בעלות בין "אחרי כל ריצה" ל-"שבועי מרוסן" — עם פלט מספרי גלוי.

  1. כתוב/י פונקציה monthly_burn(runs_per_day, transcripts_per_dream, dreams_per_week) שמחזירה tokens חודשי.
  2. הרץ/י שני תרחישים: (א) Dreaming after-each-run — 10 ריצות/יום, כל אחת מקנסלת 100 transcripts. (ב) מרוסן — שבועי, 30 transcripts.
  3. הוסף/י token_budget cap והראה/י כמה ה-cap חוסך בתרחיש (א).
  4. תרגם/י ל-USD (לפי מחיר token משוער — דוגמה מייצגת, אמת/י מול המחירון הרשמי) ול-ILS עם VAT (הערת local_market_notes ב-research).

פלט גלוי שתראה/י:

$ python dream_calc.py תרחיש א — after-each-run (10/יום × 100 transcripts): ללא cap: ~1,200,000,000 tokens/חודש ⚠️ עם cap 2M לריצה: ~600,000,000 tokens/חודש (300 ריצות capped) תרחיש ב — שבועי מרוסן (4/חודש × 30 transcripts): ~4,800,000 tokens/חודש ✓ יחס: תרחיש א יקר פי ~250 מתרחיש ב המלצה: שבועי מרוסן. את ה-burn של תרחיש א אי אפשר להצדיק.

גישה ל-Dreaming — אזהרת freshness

לפי course.research.json: Dreaming הוא Research Preview (הושק מאי 2026), הגישה "must be requested via the Anthropic Console", והוא "consumes background tokens for curation runs". ה-syllabus מסמן את פרק 7 כ-freshness_sensitive: pricing, מגבלות GA וה-API מתפתחים.

⚠️ טעות נפוצה: לבנות production על Dreaming כאילו הוא GA

Dreaming הוא research preview — ה-API, המחיר ומגבלות הגישה עלולים להשתנות בהתראה קצרה. אם תקבע את הקוד שלך לחתימות ה-preview, השדרוג הבא ישבור אותך. הנגד: עטוף/י את Dreaming מאחורי abstraction דקה (interface אחד: consolidate(transcripts) → patterns), עם fallback ל-self-improvement הדל (הקטע הבא). כך אם ה-feature משתנה או לא זמין, ה-harness ממשיך לעבוד — פחות חכם, אבל חי. תמיד אמת/י זמינות ומחיר מול הדף הרשמי platform.claude.com/docs/en/managed-agents/dreams לפני הסתמכות.

self-improvement בלי Dreaming — הגרסה הדלה

מה אם אין לך גישת preview, או שאתה לא רוצה תלות ב-feature לא יציב? יש self-improvement דל שאתה בונה לבד ב-50 שורות — לא חזק כמו Dreaming, אבל מספיק להרבה מקרים, ויציב לחלוטין.

הרעיון: במקום ש-Claude יסקור 100 transcripts מלאים, ה-harness שלך מסכם תקופתית את ה-failures מה-store (שכבר מובנים מ-ch5), מקבץ אותם לפי דמיון, וכותב את הנפוצים כ-patterns. זה לא "dreaming" אמיתי — אין resolution של סתירות מתוחכם — אבל הוא תופס את הזכייה הגדולה: failures שחוזרים.

# poor_mans_dream.py — fallback ל-Dreaming. דוגמה מייצגת
from collections import Counter

def consolidate(store: MemoryStore, task_type: str, min_repeats=3):
    """גרסה דלה: מזהה failures חוזרים והופך אותם ל-patterns.
       אין סקירת transcripts מלאים — רק ה-store המובנה מ-ch5."""
    failures = store.read(task_type, kinds=["failure"], limit=1000)
    # קיבוץ גס לפי content מנורמל (חתימת שגיאה)
    counts = Counter(f["content"][:80] for f in failures)
    for signature, n in counts.items():
        if n >= min_repeats:                     # חוזר מספיק → pattern
            store.write(MemoryItem(
                MemoryKind.PATTERN, task_type,
                f"חוזר {n} פעמים: {signature}",
                evidence="poor_mans_dream",
                created_at=time.time(),
                weight=float(n)))                # weight = כמה חזר

זה ה-fallback מאחורי ה-abstraction מהקטע הקודם. ה-interface זהה (consolidate(...)), אז המעבר בין "Dreaming אמיתי" ל-"דל" הוא החלפת implementation אחת — ה-harness לא יודע מי מקנסל.

מה הגרסה הדלה לא עושה, וחשוב להיות כן/ה לגביו: היא לא פותרת סתירות חכם (היא רק סופרת חזרות), היא לא סוקרת את ה-transcript המלא (רק את ה-failure records המובנים מ-ch5), והיא לא מזהה patterns חוצי-task_type. במילים אחרות — היא תופסת את ה-"failures שחוזרים", שזו כ-80% מהערך, ומפספסת את ה-20% המתוחכם. לרוב פרויקט מוקדם, 80% הערך ב-50 שורות ובאפס סיכון freshness זו עסקה מצוינת. כשהדל מפסיק לתפוס patterns מורכבים שאתה רואה ידנית ב-Langfuse — זה האות לשדרג ל-Dreaming.

⚡ עשה/י עכשיו (2 דקות)

הסתכל/י על min_repeats=3 בקוד הדל. מה קורה אם תוריד/י ל-1? (כל failure בודד הופך ל-pattern → רעש, store מזוהם). מה אם תעלה/י ל-10? (רק patterns מאוד-חוזרים נתפסים → מפספס לקחים אמיתיים). הסף הזה הוא כיוונון — רשום/י לעצמך באיזה ערך תתחיל/י ולמה.

🧭 Framework: Dreaming מנוהל מול self-improvement דל

🛠️ תרגיל 3 — abstraction אחד, שני implementations

מטרה: ממשק Consolidator אחד עם שני מימושים (Dreaming-stub ו-poor-man's), והחלפה ביניהם בשורה אחת — עם פלט שמוכיח שה-harness לא יודע מי רץ.

  1. הגדר/י interface: class Consolidator: def consolidate(self, store, task_type) -> int (מחזיר כמה patterns נוצרו).
  2. מימוש 1: PoorMansConsolidator מהקוד למעלה.
  3. מימוש 2: DreamingConsolidator — stub שמדפיס "would call Claude Dreaming API (preview)" ונופל ל-fallback אם אין API key (זה ה-fallback בפעולה).
  4. הרץ/י עם flag --use-dreaming שמחליף בין השניים, על אותו store, והדפס/י כמה patterns נוצרו.

פלט גלוי שתראה/י:

$ python consolidate.py # ברירת מחדל: דל [poor-man's] scanned 14 failures → wrote 2 patterns $ python consolidate.py --use-dreaming # אין preview key [dreaming] would call Claude Dreaming API (preview) [dreaming] no preview access → falling back [poor-man's] scanned 14 failures → wrote 2 patterns ✓ same interface, harness unchanged

memory משותף בין subagents — מתי עוזר ומתי מזהם

בפרק 6 בנית supervisor שמפצל subagents, כל אחד session נפרד. שאלה טבעית: האם ה-subagents חולקים memory store אחד, או שלכל אחד יש משלו?

התשובה היא תלוי, וההבחנה חשובה כי memory משותף יכול גם לעזור וגם לזהם:

סיטואציהmemory משותףלמה
שני subagents עושים את אותו סוג משימה (שני enrichers)✓ כן — store משותף לפי task_typeלקח שאחד למד מועיל לשני מיד
subagents עם תפקידים שונים (enricher מול drafter)✗ לא ל-context — כן ל-store, אבל read מסונן לפי task_typeה-drafter לא צריך לקרוא את ה-failures של ה-enricher ל-context שלו
subagent זמני (ephemeral, ch6) לתת-משימה צרה✗ לרוב לא — write בלבד ל-store, בלי readהוא קצר מדי מכדי להרוויח מ-read, אבל ה-failure שלו שווה לשמור

הכלל המנחה: store משותף, read מסונן. כל ה-subagents כותבים לאותו store (כי Dreaming צריך לראות הכל כדי לקנסל), אבל כל subagent קורא רק את ה-patterns שרלוונטיים ל-task_type שלו. זה מחבר ישירות ל-read(task_type, limit) מהקטע הראשון — הסינון הוא מה שמונע זיהום.

⚠️ טעות נפוצה: memory משותף ל-context של כל ה-subagents

"כולם באותו צוות, שכולם יראו הכל." זה מזהם את ה-context של כל subagent בלקחים שלא נוגעים לו, מאיץ compaction (ch3) על כל אחד מהם בנפרד, ושורף tokens × מספר ה-subagents. הנגד: store אחד משותף (write), read מסונן לפי task_type (כל אחד רק את שלו). שיתוף ברמת ה-storage, בידוד ברמת ה-context.

מדידת ROI — האם הזיכרון באמת שיפר?

בנית memory, הפעלת Dreaming (או הדל). השאלה היחידה שחשובה: האם זה עבד? בלי מדידה, "זיכרון" הוא אמונה. עם מדידה, הוא החלטה. וזה מתחבר ישירות ל-observability מפרק 5 — ה-traces ב-Langfuse הם המקור.

שלוש מטריקות before/after שתופסות את כל הסיפור:

מטריקהאיך מודדים (מ-Langfuse, ch5)מה משיפור נראה
אחוז חזרה-על-טעויות% ריצות שנכשלו ב-failure שכבר היה ב-storeירידה — הסוכן מפסיק לחזור על מה שלמד
אחוז הצלחה (task completion)% ריצות שהשלימו את ה-goalעלייה — זו מטריקת ה-Harvey פי 6
עלות לריצה (tokens)token cost attribution מ-ch5, ממוצע לריצהירידה — פחות turns מבוזבזים על חזרות

שים/י לב למלכוד עלות: צריך לכלול את עלות ה-Dreaming עצמו בחישוב. אם Dreaming מוריד 5% מעלות הריצות אבל שורף יותר ב-curation — הפסדת. ה-ROI האמיתי הוא: (חיסכון בריצות) − (עלות curation) > 0. זו בדיוק הסיבה שתרגיל 2 (מחשבון ה-burn) קודם למדידה כאן.

🧭 Framework: האם Dreaming משתלם לפרויקט שלי?

memory debt — מתי ה-store זקוק לניקוי

מטריקה רביעית שלא מדברים עליה מספיק: memory debt — היחס בין פריטים "רועשים" (לא-מאומתים, לא-בשימוש, סותרים) לבין פריטים "מועילים" (מאומתים, נקראים ב-30 יום, חד-משמעיים). ככל שה-debt עולה, ה-READ שלך מתחיל להחזיר רעש במקום signal — בלי שתשים לב. דרך מהירה לאמוד:

# memory_health.py — אומדן בריאות ה-store. דוגמה מייצגת
def memory_health(store: MemoryStore) -> dict:
    items = list(store.items.values())
    n = len(items)
    if n == 0:
        return {"items": 0, "debt_ratio": 0.0, "verdict": "empty"}
    low_weight   = sum(1 for i in items if i["weight"] < 1.0)
    no_evidence  = sum(1 for i in items if not i.get("evidence"))
    old_unused   = sum(1 for i in items if (time.time() - i["created_at"]) > 90*86400)
    debt = (low_weight + no_evidence + old_unused) / (3 * n)
    verdict = "healthy" if debt < 0.2 else ("watch" if debt < 0.5 else "needs cleanup")
    return {"items": n, "debt_ratio": round(debt, 2), "verdict": verdict}

הסף המעשי: debt מעל 0.5 = ה-store מפריע יותר ממה שהוא עוזר. הפעולה: הפעל decay (קטע "משקל, דעיכה ואיכות פריט"), הרץ archive, ובדוק אם ה-READ שלך מחזיר יותר איכות. memory debt היא לא מטריקה יפה — היא סימן אזהרה מוקדם לבעיה שתתפוצץ לך ב-context בלי התראה.

⚡ עשה/י עכשיו (3 דקות)

הרץ/י את memory_health() על ה-store של תרגיל 1. אם ה-verdict הוא "watch" או "needs cleanup" — זה הזמן להוסיף decay. אם "healthy" — מצוין, תדע/י שיש לך baseline.

🔄 שגרת עבודה: מחזור הזיכרון השבועי

הפוך/י את זה לשגרה קבועה, לא לאירוע חד-פעמי:

  1. יומי (אוטומטי): ה-harness כותב decisions ו-failures ל-store בכל ריצה. אפס מאמץ ידני.
  2. שבועי (מתוזמן): Dreaming/consolidate רץ פעם בשבוע על ה-scope המוגבל. בדוק/י את ה-token budget שלא חרג.
  3. שבועי (5 דק' ידני): פתח/י את ה-Langfuse dashboard, השווה/י את שלוש המטריקות לשבוע הקודם. ירידה בחזרות? עלייה בהצלחה?
  4. חודשי: הרץ/י memory_health(). debt מעל 0.5? → הפעל/י decay + archive. חשב/י ROI מלא (חיסכון − עלות curation). אם שלילי — כוונן/י תדירות/scope או רד/י לדל.

בדיקה ואיתור באגים בזיכרון — למה הסוכן לא משתמש ב-patterns

תסריט קלאסי: הרצת Dreaming, הוא יצר patterns, הם ב-store, אבל הסוכן ממשיך לחזור על אותה טעות. מה השתבש? זה הסעיף שחוסך לך שעות של "למה זה לא עובד".

חמש סיבות נפוצות לכך ש-memory לא משפיע, מהנפוצה ביותר לנדירה:

סיבהתסמיןאבחוןתיקון
task_type לא תואםה-pattern קיים אבל read() לא מחזיר אותוהדפס/י את read(task_type, kinds) בלוגבדוק/י שה-task_type של ה-run זהה לזה של ה-pattern (case-sensitive!)
weight נמוך מדיה-pattern בתוצאה, אבל מתחת ל-top-5הדפס/י את ה-scores, חפש/י patterns רועשים עם weight גבוהboost ל-pattern הרלוונטי, או העלה/י את limit
key collision בכתיבהpattern "נכתב" אבל לא מופיע ב-storeהדפס/י len(store.items) לפני/אחרי writeוודא/י ש-key() לא משתנה בין כתיבות (רווחים, אותיות גדולות)
READ לא בתחילת המשימהה-pattern ב-store, ה-read עובד בנפרד, אבל הלולאה לא קוראתלוג של ה-system prompt שנשלח בפועל ל-Claudeודא/י שה-READ קורא לפני הקריאה הראשונה ל-agent_loop
pattern לא ישים למשימה הזוה-pattern נכון אבל לא רלוונטי לקלט הנוכחיהסתכל/י ידנית: האם ה-pattern עונה על הקלט?פצל/י pattern לגרסאות ספציפיות יותר

הצעד הראשון תמיד: לוג של ה-READ. הוסף/י שורה ב-run_task():

if os.getenv("DEBUG_MEMORY"):
    print(f"[mem] task_type={task_type} → {len(lessons)} lessons")
    for l in lessons:
        print(f"  - [{l['kind']}|w={l['weight']}] {l['content'][:60]}")

בלי הלוג הזה, אתה מנחש. איתו, אתה רואה בדיוק מה ה-READ החזיר, והבעיה הופכת לברורה תוך שניות: "ה-pattern בכלל לא נטען", "הוא נטען אבל במקום השלישי", "הוא נטען אבל ל-task_type אחר". רוב באגי ה-memory נופלים בסעיף הראשון — task_type לא תואם — בגלל טעות הקלדה פשוטה או inconsistency בין קוד הכתיבה לקוד הקריאה.

🛠️ תרגיל 4 — Memory audit: מצא/י את ה-pattern המתחבא

מטרה: לתרגל איתור באגים ב-store על מקרה מבויים — עם פלט שמראה בדיוק איפה הכשל.

  1. הרץ/י את seed_buggy_store.py (המצורף מטה) — הוא יוצר store עם 12 פריטים, אחד מהם pattern אמיתי שאמור לעזור במשימה, אבל מוסתר בגלל באג.
  2. הרץ/י read("lead_enrichment", kinds=["pattern"], limit=5) ובדוק/י אם ה-pattern האמיתי חוזר.
  3. אם לא — הפעילו DEBUG_MEMORY=1 וחפשו את הבאג (רמז: task_type אחד שונה באות גדולה/קטנה).
  4. תקנו, הריצו שוב, וודאו שה-pattern עולה ל-top.

seed_buggy_store.py — דוגמה מייצגת (הורד/י בלי לתקן):

items = [
  {"kind":"pattern","task_type":"lead_enrichment","content":"Apollo needs X-Version header","weight":2.0,"evidence":"t1","created_at":time.time()},
  {"kind":"pattern","task_type":"Lead_Enrichment","content":"wrong-case pattern","weight":5.0,"evidence":"t2","created_at":time.time()},  # BUG
  {"kind":"pattern","task_type":"lead_enrichment","content":"noisy low-weight pattern","weight":0.2,"evidence":"t3","created_at":time.time()},
  {"kind":"pattern","task_type":"lead_enrichment","content":"Apollo rate-limits after 50 req/min","weight":3.5,"evidence":"t4","created_at":time.time()},
  # ... 8 more decoys
]

פלט גלוי שתראה/י (אחרי תיקון):

$ DEBUG_MEMORY=1 python read_only.py lead_enrichment pattern [mem] task_type=lead_enrichment → 3 lessons - [pattern|w=3.5] Apollo rate-limits after 50 req/min - [pattern|w=2.0] Apollo needs X-Version header - [pattern|w=0.2] noisy low-weight pattern ✓ expected pattern found in top-3 (weight=3.5) ✓ BUG was: task_type="Lead_Enrichment" (capital L) didn't match

PII ופרטיות — מה לא לשמור ב-store

נושא שרוב המדריכים מדלגים עליו: memory store הוא מאגר נתונים. אם הסוכן שלך רץ על נתוני לקוחות (שמות, אימיילים, כתובות, טלפונים), אתה צריך להחליט לפני שה-write הראשון קורה מה באמת נכנס ל-store. ברירת המחדל — "הכל נכתב" — היא הסכנה.

הנה הקטגוריות שכמעט תמיד לא שייכות ב-store:

קטגוריהדוגמהלמה לא לשמור
PII ישירשם מלא, אימייל, טלפון, כתובתGDPR/חוקי פרטיות; דליפה = קנס
Secrets ו-tokensAPI keys, סיסמאות, headers של authקריאת store → גישה לכל המערכת
תוכן לקוחה-body של אימייל שנשלח, תוכן מסמךהסוכן יכול לדלוף את זה לריצה אחרת
זיהוי פנימיcustomer_id, account_id בריצהמקשר בין ריצות = חוסר בידוד

הכלל: store זוכר עקרונות, לא ערכים. "Apollo needs X-Version header" — כן, זה עיקרון. "הלקוח יוסי כהן מתל אביב שלח אימייל עם API key sk-..." — לא, זה תוכן. אם אתה לא יכול להוציא את המזהים מבלי לאבד את הערך של הלקח — כנראה הלקח עצמו לא שייך ב-store. ה-store שלך הוא לא logs; הוא lessons learned in the abstract.

⚠️ טעות נפוצה: לכתוב את ה-error message המלא לפני סינון

כשהסוכן נכשל, ה-error שלו כולל לפעמים את כל ה-request body — כולל API key, אימייל, וכו'. אם ה-structured failure record מפרק 5 כותב את ה-error verbatim, יש לך דליפה מובנית בתוך ה-harness. הנגד: הוסף/י שלב redaction לפני ה-write: החלף אימיילים ב-<EMAIL>, מספרי טלפון ב-<PHONE>, tokens ב-<SECRET>. חמש דקות של קוד חוסכות חודשים של צרות.

# redact.py — סניטציה לפני כתיבה. דוגמה מייצגת
import re

_PATTERNS = [
    (re.compile(r"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b"), "<EMAIL>"),
    (re.compile(r"\b(?:\+972|0)(?:-?\d){8,10}\b"),                            "<PHONE>"),
    (re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"),                                 "<SECRET>"),
    (re.compile(r"\b\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?\d{4}\b"),                 "<CARD>"),
]

def redact(text: str) -> str:
    for pat, repl in _PATTERNS:
        text = pat.sub(repl, text)
    return text

ולא רק בזמן הכתיבה — גם בזמן הקריאה. אם ה-pattern נכתב נקי אבל ה-system prompt שאתה בונה משרשר אותו עם נתונים גולמיים מהמשימה, ה-PII נכנס בעקיפין. תמיד redact בשני הצדדים — write ו-read.

⚡ עשה/י עכשיו (2 דקות)

הסתכל/י על 5 הפריטים האחרונים ב-store שלך. האם אחד מהם כולל אימייל, טלפון, או token? אם כן — זה הזמן להוסיף redact() לפני ה-write הבא. אם לא — מצוין, אבל תוודא/י שזה בגלל שאתה לא שומר את זה מלכתחילה, לא במקרה.

🛠️ תרגיל 5 — בדיקת PII: תפס/י דליפה לפני שהיא קוראת

מטרה: כלי בדיקה שסורק את ה-store ומסמן פריטים שעברו את ה-redaction — עם פלט שמראה בדיוק איפה הבעיה.

  1. כתוב/י pii_scan(store) שעובר על כל הפריטים ומחפש דפוסי PII (email, phone, token, card).
  2. הרץ/י על store מזוהם לדוגמה (5 פריטים, 2 מהם עם PII שלא עבר redact).
  3. הדפס/י אילו פריטים בעייתיים, באיזה קטגוריה, והצע/י תיקון (איזה תבנית redact חסרה).
  4. הרץ/י redact() על הפריטים הבעייתיים, וודא/י שהסריקה השנייה נקייה.

פלט גלוי שתראה/י:

$ python pii_scan.py memory.json scanning 18 items... ⚠️ item f3a: contains <EMAIL> (1 match): "skyped with yossi@example.com about..." ⚠️ item b12: contains <SECRET> (1 match): "auth=sk-1234567890abcdefghij failed" ✓ 16/18 items clean $ python pii_scan.py memory.json # אחרי redact scanning 18 items... ✓ 18/18 items clean

💡 Just One Thing

אם תיקח/י דבר אחד מהפרק הזה: זיכרון הוא מבנה, לא אגירה — ו-curation היא תהליך מתוקצב, לא חינמי. store מובנה עם read מסונן (top-N לפי task_type) נותן לסוכן ללמוד בלי לזהם את ה-context; budget cap על Dreaming נותן לו להשתפר בלי לשרוף מיליוני tokens ברקע. זוכר מה שצריך, שוכח מה שלא, ומשלם רק על מה ששווה.

בדיקה עצמית

✅ שבע שאלות — ענה/י לפני שתמשיך/י לפרק 8

  1. שלוש קטגוריות הזיכרון: מהן, ומי כותב כל אחת — ריצה בודדת או Dreaming? (רמז: learned patterns שונה מהשתיים האחרות.)
  2. ה-token burn: כמה transcripts ריצת Dreaming אחת יכולה לסקור, ולמה זה הופך תזמון "אחרי כל ריצה" למסוכן במיוחד?
  3. שלושת בקרי ה-burn: מנה/י אותם. איזה מהם הוא בעצם ה-budget cap מפרק 6 בתחפושת?
  4. read מסונן: למה store.read() מחזיר top-5 לפי task_type ולא את כל ה-store? איזו טעות מ-ch3 זה מונע?
  5. freshness: Dreaming ב-research preview. מהם שני הצעדים שאתה נוקט כדי שזה לא ישבור production (abstraction + מה עוד)?
  6. debugging: הסוכן לא משתמש ב-pattern שאתה בטוח שהוא ב-store. מהם שני הצעדים הראשונים לאבחון? (רמז: אחד מהם הוא DEBUG_MEMORY.)
  7. PII: ה-structured failure record מ-ch5 כותב את ה-error verbatim. למה זו דליפה, ואיזה שלב חובה להוסיף לפני ה-write?

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

📝 מה בנית בפרק הזה

  1. memory store מובנה — שלוש קטגוריות (decisions / failures / learned patterns), עם dedup לפי key ו-read מסונן top-N. שיתוף ברמת storage, בידוד ברמת context.
  2. read/write בלולאה — READ מזריק 5 שורות רלוונטיות ל-system prompt; WRITE רושם decisions שעבדו ו-failures שנתפסו (חומר הגלם מ-ch5).
  3. איכות פריט ודעיכה — weight משקף כמה pattern מאומת וחוזר; decay עם half-life של 60 יום; archive אחרי 180 יום ללא שימוש.
  4. canonicalization של failures — נירמול חתימות שגיאה (regex על IDs, IPs, timestamps) כדי ש-dedup ו-Dreaming יראו patterns אמיתיים.
  5. Claude Dreaming — תהליך רקע אסינכרוני שמחלץ patterns, פותר סתירות, גוזם כפילויות וכותב insights מאוחדים בחזרה. ה-aggregator שהופך failures גולמיים ל-patterns מזוקקים.
  6. ריסון ה-Dreaming Token Burn — שלושה בקרים: תדירות (לא after-each-run), scope (cap על transcripts), ותקציב background tokens (budget cap קשיח). עד 100 transcripts = מיליוני tokens.
  7. self-improvement דל — fallback ב-50 שורות מאחורי abstraction אחד, יציב לחלוטין, ל-production מוקדם או כשאין preview.
  8. memory debt — מטריקה תפעולית שמודדת רעש מול signal; debt>0.5 = ה-store מפריע יותר ממה שהוא עוזר, הפעל decay + archive.
  9. debugging & memory audit — DEBUG_MEMORY=1 לוג, task_type consistency, weight ordering; תרגיל 4 מתרגל איתור באג אמיתי.
  10. PII ופרטיות — redact בכתיבה ובקריאה; store זוכר עקרונות לא ערכים; סריקה תקופתית ל-PII (תרגיל 5).
  11. מדידת ROI — before/after על חזרה-על-טעויות, אחוז הצלחה ועלות-לריצה, כולל עלות ה-curation עצמו (case study Harvey: פי 6).

🌉 הגשר לפרק 8 — "צוות שעובד לבד": עכשיו יש לך את כל השכבות — לולאה (ch2), context (ch3), tools+governance (ch4), observability+recovery (ch5), orchestration (ch6) וזיכרון+Dreaming (ch7). בפרק 8 אתה מחבר את הכל ל-harness שלם שמריץ צוות שעובד לבד על משימת end-to-end אמיתית — ומקבל את החלטת ה-production הגדולה: להישאר in-process (Claude Agent SDK, אתה מחזיק את הלולאה והזיכרון) או לעבור ל-hosted (Claude Managed Agents, שם Dreaming הוא feature מובנה). ה-memory store וה-Dreaming שבנית פה הם בדיוק מה שהמטריצה הזו תשקול: מה אתה מנהל לבד מול מה Anthropic מנהלת בשבילך.

צ'קליסט סיום פרק 7

סמן/י כל פריט לפני המעבר לפרק 8. אם פריט לא מסומן — חזור/י לקטע הרלוונטי.

מילון מונחים — פרק 7

persistent memory store
אחסון מובנה (קובץ / SQLite / KV) של פריטי זיכרון ששורדים בין ריצות. בניגוד ל-message-state שחי ב-RAM של תהליך, persistent store נשמר על דיסק ונקרא ע"י ריצות עתידיות.
memory item / פריט זיכרון
רשומה אחת ב-store: kind (decision / failure / pattern), task_type, content, evidence, created_at, weight. היחידה הבסיסית של הזיכרון.
decision (החלטה)
פריט זיכרון שמתעד בחירה שעבדה בריצה בודדת + הנימוק ("קרא ל-enrich לפני draft כי..."). נכתב ע"י הריצה; נקרא בתחילת משימה דומה.
failure (כשל)
פריט זיכרון שמתעד structured failure record מ-ch5 (input, error, context, turn). נכתב כשה-circuit-breaker / recovery נדלק; נקרא לפני ניסיון tool/מסלול שכבר נכשל.
pattern (תובנה מאוחדת)
פריט זיכרון שמתעד תובנה שחוצה הרבה ריצות ("Apollo מחזיר rate-limit אחרי 50 קריאות/דקה"). נכתב רק ע"י Dreaming או poor-man's, לא ע"י ריצה בודדת.
kinds
פרמטר הסינון של read(): רשימת הקטגוריות המותרות (למשל ["pattern","failure"]). ברירת מחדל — כל הקטגוריות. חוסך הזרקת decisions רלוונטיים-למשימה-אחרת ל-context.
task_type
מפתח השליפה של ה-store (למשל "lead_enrichment"). ה-index שבו read() מסנן. חייב להיות עקבי בין write ל-read, אחרת pattern "נעלם".
weight
ניקוד 0.0-∞ שמשקף כמה פריט אמין ורלוונטי. גבוה = שימושי, נמוך = רועש. משפיע על סדר ה-READ (top-N).
decay (דעיכה)
מנגנון שמוריד weight של פריט שלא נקרא זמן רב. בדרך כלל half-life (כל X ימים weight מתחצה) ולא תאריך תפוגה קשיח.
archive (ארכיון)
פריט שעבר decay מתחת לסף אבל לא נמחק. זמין לחיפוש ידני, לא נכלל ב-READ הרגיל. בטוח יותר ממחיקה.
canonicalization
נירמול טקסט (לרוב של failure signatures) לפני כתיבה: החלפת IDs, IPs, timestamps ב-placeholders כדי ש-dedup יזהה "אותו" failure גם אם הוא מופיע בווריאציות.
dedup
מנגנון שמונע כפילויות: אותו key (sha1 של kind+task_type+content) נכתב פעם אחת. ב-Dreaming, dedup גם מאחד patterns דומים לפריט אחד עם weight גבוה.
Claude Dreaming
תהליך רקע אסינכרוני (Research Preview, מאי 2026) ל-Claude Managed Agents שסוקר transcripts ומקנסל זיכרון: pattern extraction, contradiction resolution, dedup, consolidated write-back.
pattern extraction
אחת מארבע פעולות Dreaming: זיהוי התנהגות חוזרת על פני transcripts רבים וכתיבתה כ-pattern יחיד.
contradiction resolution
פעולת Dreaming שבה שני decisions סותרים מוכרעים לפי עדויות (למשל: מי הצליח יותר) — וה-loser מסומן deprecated.
consolidated insights write-back
הכתיבה של patterns מעובדים בחזרה אל ה-memory store המובנה, במקום transcripts גולמיים. זה מה שמאפשר לריצה הבאה לקרוא signal במקום noise.
Dreaming Token Burn
הסיכון שריצת Dreaming תשרוף מיליוני tokens ברקע, אסינכרונית, בלי שתרגיש. ריצה אחת סוקרת עד 100 transcripts × עשרות אלפי tokens כל אחד.
frequency / scope / token_budget (שלושת בקרי ה-burn)
frequency = כל כמה זמן Dreaming רץ; scope = כמה transcripts; token_budget = תקרת tokens קשיחה לריצה. שלושתם יחד חיוניים — אחד לבדו לא מספיק.
only_since_last (incremental curation)
דגל שמורה ל-Dreaming לסקור רק transcripts חדשים מאז הריצה הקודמת, במקום את כל ההיסטוריה. מונע עלות שגדלה ללא הגבלה.
scope_filter
פילטר לפי task_type שמגביל את ה-curation לסוג משימה אחד, כדי שה-patterns שיוצאים לא מעורבבים בין הקשרים.
self-improvement דל (poor-man's dream)
fallback ב-50 שורות שמחליף את Dreaming: סופר failures חוזרים בלבד וכותב אותם כ-patterns. פחות חכם מ-Dreaming, אבל יציב ובלי תלות ב-preview.
abstraction (Consolidator interface)
ממשק consolidate(store, task_type) → int שמאחוריו מסתתרים שני מימושים: DreamingConsolidator ו-PoorMansConsolidator. ה-harness לא יודע מי רץ — וזה מה שמאפשר fallback.
memory debt
מטריקה תפעולית: יחס פריטים רועשים (low weight, no evidence, ישנים) מתוך כלל ה-store. מעל 0.5 = ה-store מפריע יותר ממה שהוא עוזר.
memory health check
פונקציה שמחשבת את ה-memory debt ומחזירה verdict: "healthy" (<0.2), "watch" (0.2-0.5), "needs cleanup" (>0.5). ריצה חודשית מומלצת.
DEBUG_MEMORY
env flag שמדפיס ללוג את תוצאת ה-READ (אילו lessons, איזה weight). כלי אבחון ראשון כש"הסוכן לא משתמש ב-pattern".
redact / PII scan
redact = החלפת PII (email, phone, secret, card) ב-placeholders לפני כתיבה ל-store. PII scan = סריקה תקופתית של ה-store לאיתור דליפות שעברו את ה-redaction.
store משותף / read מסונן
דפוס גישה ל-memory בין subagents (ch6): כולם כותבים לאותו store, אבל כל אחד קורא רק את ה-patterns שרלוונטיים ל-task_type שלו. שיתוף ברמת storage, בידוד ברמת context.
Harvey 6x case study
דיווח של חברת Harvey על שיפור פי 6 באחוז השלמת משימות (task completion) אחרי הפעלת Claude Dreaming לקונסולידציה אסינכרונית. מקור: course.research.json, key_2026_updates.
background tokens
tokens שצורכים תהליכי רקע (כמו Dreaming) בניגוד ל-tokens של ריצה אינטראקטיבית. מתומחרים בנפרד, ולכן ה-budget cap קריטי.
transcript
ההיסטוריה המלאה של session אחד (הודעות, tool calls, תוצאות). קובץ הקלט של Dreaming, לרוב עשרות אלפי tokens.