החלון כ-RAM של ה-turnThe window as the turn's RAM
אם דמיינתם את 200K-token-context כ-"דיסק קשיח גדול שאפשר להעמיס", זה המודל המנטלי הלא נכון. נכון יותר לחשוב עליו כעל RAM של תהליך שחי turn אחד. כל מה שלא ב-RAM הזה — לא קיים בעולמו של ה-LLM ברגע ההסקה.
זה משנה איך מתכננים. שלוש שאלות שאנחנו שואלים על כל בלוק שעולה ל-context:
- מה ה-recall? אם זה לא נמצא, האם המודל יבקש אותו דרך tool? אם כן — לא חייב להיות פנימה תמיד.
- מה ה-relevance? בלוק שמופיע ב-90% מה-turns שייך ל-prompt קבוע (וייהנה מ-cache). בלוק ש-5% — דרך tool.
- מה ה-position? Lost-in-the-middle: מודלים זוכרים פחות טוב את אמצע ה-window. דברים קריטיים בקצוות.
אצלנו, ה-builder של Hive מקבל system prompt עם פקודה אחת מרכזית בסוף, לא בהתחלה — ה-recall שם היה גבוה ב-12% במדידות פנימיות. ההתחלה והסוף יותר "חמים" מהאמצע, ולסיום יש קצת יתרון על ההתחלה כשמדובר בהוראות פעולה.
If you picture a 200K-token context window as a "big disk you can dump into", that is the wrong mental model. Think of it as RAM for a process that lives one turn. Anything not in that RAM does not exist in the model's world at inference time.
That changes how we plan. Three questions we ask about every block we put into context:
- What is the recall? If it is missing, will the model ask for it via a tool? If yes — it does not have to be in the prompt every time.
- What is the relevance? A block used in 90% of turns belongs in a fixed prompt (and benefits from cache). A 5% block belongs behind a tool.
- What is the position? Lost-in-the-middle: models recall the middle of long contexts less reliably. Critical content goes near the edges.
In our builder, the system prompt's central directive sits at the end, not the top — recall on it was 12% higher in our internal measurements. The edges are warmer than the middle, and the end has a small edge over the start when the content is an action directive.
static vs dynamic — שני סוגי contextStatic vs dynamic — two kinds of context
החלוקה החשובה ביותר ב-context: static (זהה בכל turn — system prompt, tool schemas, מסמכי מדיניות) ו-dynamic (משתנה — ההיסטוריה, ה-tool results, ה-state של ה-job).
החלוקה הזו לא אסתטית — היא קובעת cache. Anthropic מאפשר prompt caching על prefix קבוע. אם תכניס נתון משתנה (timestamp, user_id) באמצע ה-system prompt, חתכת את ה-cache לאחר אותה נקודה. כלל ברזל אצלנו: כל הבלוקים הקבועים לפני כל הבלוקים המשתנים.
const messages = await client.messages.create({
model: 'claude-opus-4-6',
system: [
{ type: 'text', text: STATIC_HEADER, cache_control: { type: 'ephemeral' } },
{ type: 'text', text: TOOL_DOC, cache_control: { type: 'ephemeral' } },
{ type: 'text', text: jobSpecificContext }, // dynamic — אחרי ה-cache breakpoints
],
tools,
messages: history,
});בשלושה חודשי פרודקשן ראינו שזה מוריד את עלות ה-input tokens ב-72-86% עבור jobs ארוכים. זו לא אופטימיזציה אזוטרית — זה ההבדל בין סוכן רווחי ללא רווחי.
The most important split in context: static (same every turn — system prompt, tool schemas, policy docs) and dynamic (changes — history, tool results, job state).
The split is not aesthetic — it determines caching. Anthropic supports prompt caching on a stable prefix. Drop a varying value (timestamp, user_id) into the middle of the system prompt and you have just invalidated everything after it. Iron rule: all stable blocks come before all changing blocks.
const messages = await client.messages.create({
model: 'claude-opus-4-6',
system: [
{ type: 'text', text: STATIC_HEADER, cache_control: { type: 'ephemeral' } },
{ type: 'text', text: TOOL_DOC, cache_control: { type: 'ephemeral' } },
{ type: 'text', text: jobSpecificContext }, // dynamic — after the cache breakpoints
],
tools,
messages: history,
});Across three months in production, this drops input-token cost by 72-86% on long jobs. It is not an esoteric optimization — it is the line between a profitable agent and an unprofitable one.
memory tiers — HOT, WARM, COLDMemory tiers — HOT, WARM, COLD
כדי לא להעמיס context, אנחנו מחלקים את כל הזיכרון של הסוכן לשלושה tiers:
- HOT — נטען בכל turn.
memory/core.md, עד 100 שורות. עובדות שאי אפשר בלעדיהן: זהות הסוכן, החוקים שאסור לשבור, מצב נוכחי קריטי. - WARM — נטען לפי הקשר.
memory/daily/YYYY-MM-DD.md,decisions.md. זמין דרך toolload_memory. - COLD — לא נטען לעולם אוטומטית.
memory/archive/. זמין דרך חיפוש סמנטי בלבד.
הטבלה מאחור, memory_chunks, היא חיפוש hybrid: FTS + trigram + vector(1024). חיפוש חוזר עם snippet של 200 tokens, לא עם המסמך המלא. ההבדל בין "תחזיר את הקובץ" ל-"תחזיר את הקטע הרלוונטי" הוא בערך 50x ב-tokens.
-- חיפוש hybrid במשפט אחד
SELECT id, content_snippet,
0.5*ts_rank_cd(tsv, plainto_tsquery('hebrew', $1)) +
0.3*similarity(content, $1) +
0.2*(1 - (embedding <=> $2::vector)) AS score
FROM memory_chunks
WHERE tsv @@ plainto_tsquery('hebrew', $1)
OR content % $1
OR embedding <=> $2::vector < 0.4
ORDER BY score DESC
LIMIT 5;To avoid bloating context, we tier all agent memory:
- HOT — loaded every turn.
memory/core.md, ≤100 lines. Facts the agent cannot run without: its identity, hard rules, current critical state. - WARM — loaded on demand.
memory/daily/YYYY-MM-DD.md,decisions.md. Available through aload_memorytool. - COLD — never loaded automatically.
memory/archive/. Reachable only via semantic search.
The backing store, memory_chunks, is hybrid: FTS + trigram + vector(1024). Search returns a 200-token snippet, not the full document. The gap between "return the file" and "return the relevant span" is roughly 50x in tokens.
-- hybrid search in one statement
SELECT id, content_snippet,
0.5*ts_rank_cd(tsv, plainto_tsquery('english', $1)) +
0.3*similarity(content, $1) +
0.2*(1 - (embedding <=> $2::vector)) AS score
FROM memory_chunks
WHERE tsv @@ plainto_tsquery('english', $1)
OR content % $1
OR embedding <=> $2::vector < 0.4
ORDER BY score DESC
LIMIT 5;compaction — איך מקצרים history בלי לשבור loopCompaction — shrinking history without breaking the loop
גם עם cache, history של 200 turns יקרה ואיטית. ב-build ארוך אצלנו, ה-history יכול להגיע ל-180K tokens. compaction זה התהליך של דחיסת חלקים ישנים תוך שמירה על מה שעדיין רלוונטי.
הכלל הראשון: אסור לשבור את ההצמדה tool_use ↔ tool_result. אם הסרת tool_use, חייב גם להסיר את ה-tool_result התואם, אחרת ה-API יחזיר 400.
function compactHistory(messages, ctx) {
const KEEP_LAST = 12; // הזנב נשאר תמיד
const head = messages.slice(0, -KEEP_LAST);
const tail = messages.slice(-KEEP_LAST);
if (head.length === 0) return messages;
// סיכום של ה-head דרך מודל זול (Haiku)
const summary = synthesizeSummary(head, ctx);
return [
{ role: 'user', content: [{ type: 'text', text: `[compacted history summary]\n${summary}` }] },
{ role: 'assistant', content: [{ type: 'text', text: 'Acknowledged.' }] },
...tail,
];
}שלוש דקויות:
- הסיכום נעשה ע"י Haiku/Sonnet, לא Opus. הוא לא צריך להיות חכם, רק נאמן.
- שומרים את ה-12 הודעות האחרונות בלי שינוי כי שם נמצא state פעיל (tool_use_id-ים שעוד פתוחים).
- compaction צריכה להישלף רק כשמתקרבים ל-cap, לא בכל turn — היא בעצמה עולה tokens.
Even with caching, a 200-turn history is expensive and slow. On a long build ours can reach 180K tokens. Compaction is the process of squashing the old part while keeping what is still useful.
The first rule: never break the tool_use ↔ tool_result pairing. If you drop a tool_use, you must drop its tool_result, or the API returns 400.
function compactHistory(messages, ctx) {
const KEEP_LAST = 12; // the tail is always preserved
const head = messages.slice(0, -KEEP_LAST);
const tail = messages.slice(-KEEP_LAST);
if (head.length === 0) return messages;
// summarize the head with a cheap model (Haiku)
const summary = synthesizeSummary(head, ctx);
return [
{ role: 'user', content: [{ type: 'text', text: `[compacted history summary]\n${summary}` }] },
{ role: 'assistant', content: [{ type: 'text', text: 'Acknowledged.' }] },
...tail,
];
}Three subtleties:
- The summary is produced by Haiku/Sonnet, not Opus. It does not need to be smart, only faithful.
- Keep the last 12 messages untouched — that is where active state lives (tool_use_ids still open).
- Run compaction only when approaching the cap, not every turn — compaction itself costs tokens.
tool_results — לא לשפוך, לשרתTool results — do not dump, serve
tool_result אחד יכול לתפוס יותר tokens מכל ה-system prompt. סוכן שקורא לקובץ של 4MB ומקבל את כולו ל-context — זרק את העבודה של כל מי שעיצב את ה-prompt.
הכללים שאנחנו אוכפים בכל tool שמחזיר נתונים:
- cap קשה על גודל — ברירת מחדל 8K tokens. מעל זה, חיתוך עם הסבר מפורש.
- summary במקום dump — אם המשתמש ביקש
list_files, אל תחזיר 5,000 שורות. החזר 100, כתוב "4,900 more", ותן פילטרים. - פורמט יציב — JSON עם סכמה קבועה, לא prose. זה גם חוסך tokens וגם מאפשר למודל להבין מבנה.
function capToolResult(result, opts = { maxTokens: 8000 }) {
const text = typeof result === 'string' ? result : JSON.stringify(result);
const approxTokens = Math.ceil(text.length / 3.5);
if (approxTokens <= opts.maxTokens) return text;
const cutChars = opts.maxTokens * 3.5;
return text.slice(0, cutChars) +
`\n\n[truncated: ${approxTokens - opts.maxTokens} tokens omitted. ` +
`call again with stricter filters to see specific parts.]`;
}One tool_result can consume more tokens than the entire system prompt. An agent that reads a 4MB file and receives all of it in context has thrown away every minute spent on the prompt.
Rules we enforce on every tool that returns data:
- hard cap on size — default 8K tokens. Above that, truncate with an explicit notice.
- summary instead of dump — if the user asked for
list_files, do not return 5,000 lines. Return 100, label "4,900 more", expose filters. - stable format — JSON with a fixed schema, not prose. Saves tokens and lets the model parse structure.
function capToolResult(result, opts = { maxTokens: 8000 }) {
const text = typeof result === 'string' ? result : JSON.stringify(result);
const approxTokens = Math.ceil(text.length / 3.5);
if (approxTokens <= opts.maxTokens) return text;
const cutChars = opts.maxTokens * 3.5;
return text.slice(0, cutChars) +
`\n\n[truncated: ${approxTokens - opts.maxTokens} tokens omitted. ` +
`call again with stricter filters to see specific parts.]`;
}תקציב הדפדוף ומה שמתרסק כשטועיםContext budget and where it breaks
תקציב context צריך להיות מספרי, לא תחושתי. אצלנו לכל job יש cap קשיח של 180K input tokens (Opus 4.6 — 200K minus margin) ו-budget רך של 50K לכל turn. כשעוברים את הרך, ה-runtime מפעיל compaction. כשעוברים את הקשה, ה-loop נעצר עם budget_exceeded.
חמש טעויות נפוצות שמפוצצות תקציב:
- raw logs ל-context. סוכן עושה
cat error.logושופך 60K tokens. תמידtail -n 200. - tool_result-ים שמחזירים את ה-input. רק ה-output החדש. לא לחזור על מה שכבר ב-history.
- schema ענקיות של tools. 40 tools של 1K tokens כל אחד = 40K לפני שהתחלנו. tiering פותר.
- חיפוש שמחזיר את הקבצים המלאים. רק snippets של ±200 tokens סביב המאצ'.
- system prompt שגדל לפי features. כל פיצ'ר מוסיף 500 tokens ואף אחד לא מוחק. סקירה רבעונית של ה-prompt.
הניהול הטוב של ה-context הוא לא מיומנות LLM, הוא מיומנות מערכות. אם בנית אותו טוב, המודל יזרום. אם לא — שום prompt engineering לא יציל אותך.
The context budget should be numeric, not vibes-based. Each of our jobs gets a hard cap of 180K input tokens (Opus 4.6 — 200K minus margin) and a soft budget of 50K per turn. Crossing the soft budget triggers compaction. Crossing the hard cap stops the loop with budget_exceeded.
Five common mistakes that blow the budget:
- Raw logs into context. An agent runs
cat error.logand dumps 60K tokens. Alwaystail -n 200. - tool_results that echo the input. Return only the new output. Do not repeat what is already in history.
- Huge tool schemas. 40 tools at 1K tokens each = 40K before we start. Tiering fixes this.
- Search that returns full files. Snippets of ±200 tokens around the match are enough.
- System prompt that grows by feature. Each feature adds 500 tokens and nobody deletes. Quarterly prompt review.
Good context management is not an LLM skill, it is a systems skill. Build it well and the model glides. Build it badly and no amount of prompt engineering will rescue you.