🧵 חוט הפרויקט
בפרק הקודם (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.
🎯 מה תדע/י לעשות בסוף הפרק
- לעצב memory store מובנה ל-harness (decisions, failures, learned patterns) שריצות כותבות אליו וקוראות ממנו בלולאה — בלי לזהם את ה-context.
- להפעיל Claude Dreaming על transcripts וזיכרון כדי לחלץ patterns, לפתור סתירות ולגזום כפילויות — ולכתוב insights מאוחדים בחזרה ל-store מובנה.
- לרסן את ה-Dreaming Token Burn: לקבוע תדירות, scope (כמה transcripts) ותקציב background tokens כך שהקונסולידציה לא תשרוף מיליוני tokens.
- למדוד ROI: להשוות אחוז חזרה-על-טעויות ואחוז הצלחה לפני ואחרי הפעלת Dreaming (case study Harvey: שיפור פי 6).
- לבנות גרסת self-improvement דלה בלי ה-feature ה-hosted — ולעטוף את Dreaming מאחורי abstraction כי הוא ב-research preview.
📋 דרישות קדם
- פרק 1 — מה זה Agent Harness ולמה 70% מהביצועים מחוץ למודל.
- פרק 2 — לולאת agent מפורשת מעל ה-Claude Agent SDK (state, message-state, max-turns).
- פרק 3 — ניהול context מול ה-compaction buffer (33K token buffer, סף 83.5%, threshold-based backup, scratchpad). הזיכרון פה הוא הרחבה ישירה של ה-store החיצוני מ-ch3.
- פרק 5 — observability ו-failure capture. ה-
structured failure recordמ-ch5 הוא חומר הגלם של ה-memory store פה. - פרק 6 — supervisor loop ו-subagents. נדבר על memory משותף בין subagents.
- סביבה: חשבון Anthropic עם API key, ה-Claude Agent SDK מותקן, ו-(אופציונלי, ל-Dreaming) גישת research preview דרך ה-Anthropic Console.
- התקנה חד-פעמית (לפני תרגיל 1): Python 3.10+ (נדרש עבור
dataclassו-str,Enum). כל התרגילים בפרק משתמשים ב-stdlib בלבד (json,dataclasses,enum,re,hashlib) — אין צורך ב-pip installנוסף. אם תרצה/י backend שאינו stdlib (למשל SQLite בפרק 8), הוסף/יpip installרק עבורו.
📦 תוצרים — מה תיצור/י בפרק הזה
- memory store מובנה — schema של שלוש קטגוריות (decisions / failures / learned patterns) עם read/write מתוך ה-harness loop, וב-store נפרד מה-context (קובץ JSON / SQLite / KV).
- Dreaming job מתוזמן — קונפיגורציה שמריצה curation אסינכרונית על transcripts עם scope ותקציב מוגדרים, עטופה מאחורי abstraction עם fallback.
- מדידת before/after — טבלה שמראה ירידה בחזרה-על-טעויות ועלייה באחוז הצלחה אחרי הפעלת Dreaming, עם עלות לריצה.
- 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. עכשיו אנחנו מרחיבים את אותו עיקרון מ-"זיכרון בתוך ריצה" ל-"זיכרון בין ריצות".
שיפור בקצב השלמת משימות (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 / Redis | harness מבוזר, כמה מכונות | 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 פריטים (אחד מכל קטגוריה) וקורא בחזרה רק את הרלוונטיים — עם פלט גלוי.
- צור/י
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']}")
- הרץ/י
python memory_store.py— אתה אמור/ה לראותstore size = 3ושתי שורות (pattern עם weight 3.0 למעלה, failure עם 1.5 אחריו). ה-decision סונן בגללkinds. - צור/י
demo_memory.pyשמוסיףdecisionרביעי עםtask_type="docs", קוראstore.read("lead_enrichment", kinds=["pattern","failure"]), ומדפיס את התוצאה. - ודא/י שה-
decisionשלdocsלא חוזר (סינון לפי task_type עובד), ושה-decisionשל lead_enrichment גם לא (סינון לפי kinds עובד).
פלט גלוי שתראה/י:
משקל, דעיכה ואיכות פריט — מה הופך 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 ישיר?
לפני שאתה כותב פריט זיכרון, החלט מי הכותב הנכון:
- אם זה עובדה מריצה בודדת (decision שעבד, failure ספציפי) אז write ישיר מה-loop — אתה כבר יודע את זה בוודאות.
- אם זה דורש מבט על הרבה ריצות (האם זה חוזר? האם זה סותר משהו?) אז זה עבודה של Dreaming — ריצה בודדת לא יכולה לדעת.
- אם אין לך גישת Dreaming (preview) אז השתמש ב-self-improvement הדל (קטע בהמשך) — סיכום failures תקופתי במקום curation אמיתי.
⚡ עשה/י עכשיו (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 אחת. וזה רץ ברקע, אסינכרוני, כשאתה לא מסתכל. החשבון מתפוצץ בשקט.
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 קשיחה לריצת curation | budget 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 אתה בטוח. בלי האומדן הזה — אתה מגלה את העלות מהחשבון.
שתי החלטות תזמון נוספות ששווה לקבל מודע, לא בדיפולט:
- incremental מול full:
only_since_last=Trueאומר לסקור רק transcripts שנוספו מאז ה-curation הקודם, לא את כל ההיסטוריה בכל פעם. זה הופך את ה-burn מ"גדל ללא הגבלה ככל שהפרויקט מזדקן" ל"קבוע פחות או יותר לכל ריצה". בלי זה, ככל שתצבור transcripts, כל ריצת Dreaming תתייקר — וזו הפתעה לא נעימה אחרי חודש. - scope_filter ממוקד: אם יש לך כמה task_types, curation על כולם ביחד מערבב הקשרים ומבזבז. עדיף ריצת Dreaming אחת לכל task_type חשוב — קטנה, ממוקדת, וזולה. ה-patterns שייצאו יהיו חדים יותר כי הם לא מדוללים ברעש ממשימות אחרות.
וקריטי: תזמון 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
מטרה: כלי שמראה לך, לפני הפעלה, מה ההבדל בעלות בין "אחרי כל ריצה" ל-"שבועי מרוסן" — עם פלט מספרי גלוי.
- כתוב/י פונקציה
monthly_burn(runs_per_day, transcripts_per_dream, dreams_per_week)שמחזירה tokens חודשי. - הרץ/י שני תרחישים: (א) Dreaming after-each-run — 10 ריצות/יום, כל אחת מקנסלת 100 transcripts. (ב) מרוסן — שבועי, 30 transcripts.
- הוסף/י
token_budgetcap והראה/י כמה ה-cap חוסך בתרחיש (א). - תרגם/י ל-USD (לפי מחיר token משוער — דוגמה מייצגת, אמת/י מול המחירון הרשמי) ול-ILS עם VAT (הערת local_market_notes ב-research).
פלט גלוי שתראה/י:
גישה ל-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 דל
- אם יש לך גישת preview, scale גדול (מאות+ transcripts), וצורך בפתרון סתירות אמיתי אז Dreaming מנוהל — שווה את ה-token budget.
- אם אתה ב-production מוקדם, רוצה אפס תלות ב-preview, או שה-failures שלך פשוטים וחוזרים אז self-improvement דל מספיק — ויציב.
- אם אתה לא בטוח אז התחל דל (אפס סיכון freshness), עבור ל-Dreaming רק כשהדל מפסיק לתפוס patterns מורכבים. תמיד מאחורי ה-abstraction.
🛠️ תרגיל 3 — abstraction אחד, שני implementations
מטרה: ממשק Consolidator אחד עם שני מימושים (Dreaming-stub ו-poor-man's), והחלפה ביניהם בשורה אחת — עם פלט שמוכיח שה-harness לא יודע מי רץ.
- הגדר/י interface:
class Consolidator: def consolidate(self, store, task_type) -> int(מחזיר כמה patterns נוצרו). - מימוש 1:
PoorMansConsolidatorמהקוד למעלה. - מימוש 2:
DreamingConsolidator— stub שמדפיס"would call Claude Dreaming API (preview)"ונופל ל-fallback אם אין API key (זה ה-fallback בפעולה). - הרץ/י עם flag
--use-dreamingשמחליף בין השניים, על אותו store, והדפס/י כמה patterns נוצרו.
פלט גלוי שתראה/י:
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 משתלם לפרויקט שלי?
- אם חיסכון-בריצות (פחות חזרות × tokens/חזרה × ריצות/חודש) גדול מ עלות-curation החודשית אז Dreaming משתלם — הפעל/י.
- אם הם קרובים אז עבור/י לתדירות נמוכה יותר או scope קטן יותר עד שהמאזן חיובי בבירור.
- אם עלות ה-curation גדולה מהחיסכון אז רד/י ל-self-improvement הדל — אותו ערך, אפס background burn.
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.
🔄 שגרת עבודה: מחזור הזיכרון השבועי
הפוך/י את זה לשגרה קבועה, לא לאירוע חד-פעמי:
- יומי (אוטומטי): ה-harness כותב decisions ו-failures ל-store בכל ריצה. אפס מאמץ ידני.
- שבועי (מתוזמן): Dreaming/consolidate רץ פעם בשבוע על ה-scope המוגבל. בדוק/י את ה-token budget שלא חרג.
- שבועי (5 דק' ידני): פתח/י את ה-Langfuse dashboard, השווה/י את שלוש המטריקות לשבוע הקודם. ירידה בחזרות? עלייה בהצלחה?
- חודשי: הרץ/י
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 על מקרה מבויים — עם פלט שמראה בדיוק איפה הכשל.
- הרץ/י את
seed_buggy_store.py(המצורף מטה) — הוא יוצר store עם 12 פריטים, אחד מהם pattern אמיתי שאמור לעזור במשימה, אבל מוסתר בגלל באג. - הרץ/י
read("lead_enrichment", kinds=["pattern"], limit=5)ובדוק/י אם ה-pattern האמיתי חוזר. - אם לא — הפעילו
DEBUG_MEMORY=1וחפשו את הבאג (רמז: task_type אחד שונה באות גדולה/קטנה). - תקנו, הריצו שוב, וודאו שה-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
]
פלט גלוי שתראה/י (אחרי תיקון):
PII ופרטיות — מה לא לשמור ב-store
נושא שרוב המדריכים מדלגים עליו: memory store הוא מאגר נתונים. אם הסוכן שלך רץ על נתוני לקוחות (שמות, אימיילים, כתובות, טלפונים), אתה צריך להחליט לפני שה-write הראשון קורה מה באמת נכנס ל-store. ברירת המחדל — "הכל נכתב" — היא הסכנה.
הנה הקטגוריות שכמעט תמיד לא שייכות ב-store:
| קטגוריה | דוגמה | למה לא לשמור |
|---|---|---|
| PII ישיר | שם מלא, אימייל, טלפון, כתובת | GDPR/חוקי פרטיות; דליפה = קנס |
| Secrets ו-tokens | API 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 — עם פלט שמראה בדיוק איפה הבעיה.
- כתוב/י
pii_scan(store)שעובר על כל הפריטים ומחפש דפוסי PII (email, phone, token, card). - הרץ/י על store מזוהם לדוגמה (5 פריטים, 2 מהם עם PII שלא עבר redact).
- הדפס/י אילו פריטים בעייתיים, באיזה קטגוריה, והצע/י תיקון (איזה תבנית redact חסרה).
- הרץ/י
redact()על הפריטים הבעייתיים, וודא/י שהסריקה השנייה נקייה.
פלט גלוי שתראה/י:
💡 Just One Thing
אם תיקח/י דבר אחד מהפרק הזה: זיכרון הוא מבנה, לא אגירה — ו-curation היא תהליך מתוקצב, לא חינמי. store מובנה עם read מסונן (top-N לפי task_type) נותן לסוכן ללמוד בלי לזהם את ה-context; budget cap על Dreaming נותן לו להשתפר בלי לשרוף מיליוני tokens ברקע. זוכר מה שצריך, שוכח מה שלא, ומשלם רק על מה ששווה.
בדיקה עצמית
✅ שבע שאלות — ענה/י לפני שתמשיך/י לפרק 8
- שלוש קטגוריות הזיכרון: מהן, ומי כותב כל אחת — ריצה בודדת או Dreaming? (רמז: learned patterns שונה מהשתיים האחרות.)
- ה-token burn: כמה transcripts ריצת Dreaming אחת יכולה לסקור, ולמה זה הופך תזמון "אחרי כל ריצה" למסוכן במיוחד?
- שלושת בקרי ה-burn: מנה/י אותם. איזה מהם הוא בעצם ה-budget cap מפרק 6 בתחפושת?
- read מסונן: למה
store.read()מחזיר top-5 לפי task_type ולא את כל ה-store? איזו טעות מ-ch3 זה מונע? - freshness: Dreaming ב-research preview. מהם שני הצעדים שאתה נוקט כדי שזה לא ישבור production (abstraction + מה עוד)?
- debugging: הסוכן לא משתמש ב-pattern שאתה בטוח שהוא ב-store. מהם שני הצעדים הראשונים לאבחון? (רמז: אחד מהם הוא DEBUG_MEMORY.)
- PII: ה-structured failure record מ-ch5 כותב את ה-error verbatim. למה זו דליפה, ואיזה שלב חובה להוסיף לפני ה-write?
סיכום וגשר לפרק הבא
📝 מה בנית בפרק הזה
- memory store מובנה — שלוש קטגוריות (decisions / failures / learned patterns), עם dedup לפי key ו-read מסונן top-N. שיתוף ברמת storage, בידוד ברמת context.
- read/write בלולאה — READ מזריק 5 שורות רלוונטיות ל-system prompt; WRITE רושם decisions שעבדו ו-failures שנתפסו (חומר הגלם מ-ch5).
- איכות פריט ודעיכה — weight משקף כמה pattern מאומת וחוזר; decay עם half-life של 60 יום; archive אחרי 180 יום ללא שימוש.
- canonicalization של failures — נירמול חתימות שגיאה (regex על IDs, IPs, timestamps) כדי ש-dedup ו-Dreaming יראו patterns אמיתיים.
- Claude Dreaming — תהליך רקע אסינכרוני שמחלץ patterns, פותר סתירות, גוזם כפילויות וכותב insights מאוחדים בחזרה. ה-aggregator שהופך failures גולמיים ל-patterns מזוקקים.
- ריסון ה-Dreaming Token Burn — שלושה בקרים: תדירות (לא after-each-run), scope (cap על transcripts), ותקציב background tokens (budget cap קשיח). עד 100 transcripts = מיליוני tokens.
- self-improvement דל — fallback ב-50 שורות מאחורי abstraction אחד, יציב לחלוטין, ל-production מוקדם או כשאין preview.
- memory debt — מטריקה תפעולית שמודדת רעש מול signal; debt>0.5 = ה-store מפריע יותר ממה שהוא עוזר, הפעל decay + archive.
- debugging & memory audit — DEBUG_MEMORY=1 לוג, task_type consistency, weight ordering; תרגיל 4 מתרגל איתור באג אמיתי.
- PII ופרטיות — redact בכתיבה ובקריאה; store זוכר עקרונות לא ערכים; סריקה תקופתית ל-PII (תרגיל 5).
- מדידת 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. אם פריט לא מסומן — חזור/י לקטע הרלוונטי.
- הגדרתי
memory_store.pyעם שלוש קטגוריות (decision / failure / pattern) ו-schema ברור. - ה-
write()עושה dedup אוטומטי לפי key — אותו לקח לא נכתב פעמיים. - ה-
read()מחזיר top-N מסונן לפי task_type — לא את כל ה-store (מונע זיהום context מ-ch3). - חיברתי read בתחילת המשימה (5 שורות ל-system) ו-write בסופה (decisions + failures מ-ch5) — תרגיל 1 רץ עם פלט גלוי.
- אני מבין/ה שאת ה-
learned patternsכותב רק Dreaming, לא ריצה בודדת. - אני יודע/ת מה Dreaming עושה: pattern extraction, contradiction resolution, dedup, consolidated write-back.
- הפנמתי את ה-Dreaming Token Burn: עד 100 transcripts = מיליוני tokens, רץ ברקע.
- הגדרתי שלושת בקרי ה-burn: תדירות מרוסנת + scope cap + token budget קשיח.
- בניתי מחשבון burn (תרגיל 2) שמשווה after-each-run מול שבועי, עם USD/ILS+VAT.
- עטפתי את Dreaming מאחורי abstraction עם fallback ל-self-improvement דל (תרגיל 3).
- בניתי גרסת self-improvement דלה (poor_mans_dream) שעובדת בלי preview.
- הגדרתי memory משותף בין subagents כ-"store משותף, read מסונן" — לא context משותף.
- הגדרתי שלוש מטריקות ROI (חזרה-על-טעויות, הצלחה, עלות) מ-Langfuse, כולל עלות ה-curation עצמו.
- אימתתי את זמינות ומחיר Dreaming מול הדף הרשמי
managed-agents/dreams(research preview). - הגדרתי weight scoring + decay (half-life 60 יום) + archive (180 יום) כדי שה-store לא ייצבור רעש.
- הוספתי canonicalization ל-failure signatures (regex על IDs, IPs, timestamps) לפני write.
- הוספתי DEBUG_MEMORY=1 לוג ל-READ כדי לאבחן במהירות "למה הסוכן לא משתמש ב-pattern".
- תרגלתי memory audit (תרגיל 4) ומצאתי pattern שהוסתר בגלל task_type case-sensitivity.
- הוספתי
redact()לפני כל write ל-store (אימייל, טלפון, secrets, cards). - הרצתי
pii_scan()על ה-store הקיים (תרגיל 5) ואישרתי 0 דליפות. - הגדרתי memory_health() וסף debt<0.5; תיעדתי מה עושים כשחורגים.
- קבעתי שגרת עבודה שבועית: כתיבה יומית אוטומטית, curation שבועי, בדיקת מטריקות 5 דק', ROI + memory_health חודשי.
מילון מונחים — פרק 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.