NewRelease 0.12.3 is out: Engine v0.12.3, Dashboard 0.5.2, Helm 0.12.3, the four SDKs, CLI 0.8.2 and MCP 0.12.3Release 0.12.3 is outSee what's new →
Intermediate Optimization

Context Window Management

⏱ ~30 min to implement 📦 Requires: Dakera v0.11+

Even with 200k-token context windows, naively dumping all recalled memories into every prompt is expensive, slow, and degrades response quality. The right approach is selective loading: score each candidate memory by a composite of relevance, recency, and importance, then fill the memory budget starting from the top — cutting off when the budget is exhausted. This pattern gives you maximum signal in minimum tokens.

Start Building Free →
Prerequisites
  • Dakera instance running (quickstart)
  • SDK installed: pip install dakera / npm i @dakera-ai/dakera
  • Understanding of your LLM's token limits and cost structure
  • A token counting strategy — either a dedicated tokenizer library or character-based approximation (1 token ≈ 4 characters)

The Problem

Production agents with persistent memory face a compounding pressure: memory stores grow over time, but context windows don't. Three concrete failure modes emerge:

Architecture

This pattern implements a priority queue with a token budget cap. Dakera recalls a broad candidate set (top_k=50), ranked by its own score (smart_score), which already combines relevance, importance and recency (weights 0.55, 0.20 and 0.13 on the default route). Your application then re-scores each candidate with its own composite, composite = (score × W_r) + (importance × W_i) + (recency_factor × W_rc), and adds memories to the context in that order until the memory token budget is exhausted. The scoring, token counting and trimming are application logic; Dakera supplies the candidates and their fields. Critical memories (lessons, active goals) are pinned to always be included.

Diagram of the token budget for a 128,000-token context window: system prompt 8,000 tokens, pinned memories 12,000, scored memories 20,000 (the managed budget), conversation 18,000, user message 3,000, and 67,000 reserved for the response. A pinned recall with top_k 10 and min_importance 0.90 for goals, lessons and identity fills the pinned block first, as [Critical context]. A broad recall with top_k 50 and min_importance 0.30 fills the scored block until it is full, after scoring and trimming.Diagram of the token budget for a 128,000-token context window: system prompt 8,000 tokens, pinned memories 12,000, scored memories 20,000 (the managed budget), conversation 18,000, user message 3,000, and 67,000 reserved for the response. A pinned recall with top_k 10 and min_importance 0.90 for goals, lessons and identity fills the pinned block first, as [Critical context]. A broad recall with top_k 50 and min_importance 0.30 fills the scored block until it is full, after scoring and trimming.
Every part of the prompt gets a fixed share; the scored memory block is the one this pattern manages.Full size ↗Full size ↗
Workflow of composite scoring and budget fill. The query triggers two Dakera recalls. The pinned recall, top_k 10 with min_importance 0.90, goes straight into the memory context outside the budget. The broad recall, top_k 50 with min_importance 0.30, returns 50 candidates that your application scores with the Dakera recall score at 0.5, importance at 0.3 and recency at 0.2, sorted in descending order, and added one by one while they fit the budget, counting tokens as length divided by 4. Memories that would exceed the budget are trimmed. The memory context lists pinned memories first, then scored ones.Workflow of composite scoring and budget fill. The query triggers two Dakera recalls. The pinned recall, top_k 10 with min_importance 0.90, goes straight into the memory context outside the budget. The broad recall, top_k 50 with min_importance 0.30, returns 50 candidates that your application scores with the Dakera recall score at 0.5, importance at 0.3 and recency at 0.2, sorted in descending order, and added one by one while they fit the budget, counting tokens as length divided by 4. Memories that would exceed the budget are trimmed. The memory context lists pinned memories first, then scored ones.
Pinned memories always go in; scored memories fill the budget from the top.Full size ↗Full size ↗

Implementation Steps

Tip: Reserve room for the response

A common mistake is maximizing memory injection at the expense of response quality. For a 128k context model, reserve at least 30k tokens (23%) for the expected response — for long-form content generation, reserve 40–60k. Stuffing memories to 90% of context hurts output coherence and invites hallucination. More memory is not always better.

Implementation

# Step 1: broad recall for the candidate set
curl -X POST http://localhost:3000/v1/memory/recall \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "doc-analysis-agent", "query": "document analysis context user preferences project", "top_k": 50, "min_importance": 0.3}'

# Step 2: separate recall for pinned memories (high-importance critical context)
curl -X POST http://localhost:3000/v1/memory/recall \
  -H "Authorization: Bearer dk-..." \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "doc-analysis-agent", "query": "active goals lessons user identity critical context", "top_k": 10, "min_importance": 0.9}'

# Each result carries content, importance, created_at and score (Dakera's smart_score).
# Scoring, token counting and trimming happen client-side.
# Budget allocation example for a 128k model:
#   system_prompt:      8,000 tokens
#   pinned_memories:   12,000 tokens
#   scored_memories:   20,000 tokens
#   conversation:      18,000 tokens
#   user_message:       3,000 tokens
#   response_reserved: 67,000 tokens
#   TOTAL:            128,000 tokens
from dakera import DakeraClient
from datetime import datetime, timezone
from typing import NamedTuple

client = DakeraClient(base_url="http://localhost:3000", api_key="dk-...")

# Token budget for a 128k context model
TOKEN_BUDGET = {
    "system_prompt": 8_000,
    "pinned_memories": 12_000,
    "scored_memories": 20_000,  # the budget this pattern manages
    "conversation": 18_000,
    "user_message": 3_000,
    "response_reserved": 67_000,
}

# Composite weights (application logic): tune for your use case
SCORING_WEIGHTS = {
    "relevance": 0.50,   # Dakera's recall score (smart_score)
    "importance": 0.30,  # the memory's importance, 0.0-1.0
    "recency": 0.20,     # from created_at
}
RECENCY_DECAY_HOURS = 168  # 7 days: older memories get near-zero recency

class ScoredMemory(NamedTuple):
    content: str
    composite_score: float
    token_estimate: int
    memory_id: str

def estimate_tokens(text: str) -> int:
    """Conservative character-based token estimate. Use tiktoken for precision."""
    return max(1, len(text) // 4)

def compute_recency_factor(created_at) -> float:
    """1.0 for fresh memories, decaying toward 0.0 over RECENCY_DECAY_HOURS."""
    if created_at is None:
        return 0.5  # unknown age: neutral recency
    try:
        created = datetime.fromtimestamp(float(created_at), timezone.utc)  # unix seconds
    except (TypeError, ValueError):
        try:
            created = datetime.fromisoformat(str(created_at).replace('Z', '+00:00'))
        except ValueError:
            return 0.5
    hours_old = (datetime.now(timezone.utc) - created).total_seconds() / 3600
    return max(0.0, 1.0 - hours_old / RECENCY_DECAY_HOURS)

def score_memory(mem) -> ScoredMemory:
    """Composite score for one recalled memory (a RecalledMemory)."""
    composite = (
        mem.score * SCORING_WEIGHTS["relevance"]
        + mem.importance * SCORING_WEIGHTS["importance"]
        + compute_recency_factor(mem.created_at) * SCORING_WEIGHTS["recency"]
    )
    return ScoredMemory(
        content=mem.content,
        composite_score=round(composite, 4),
        token_estimate=estimate_tokens(mem.content),
        memory_id=mem.id
    )

def build_memory_context(
    agent_id: str,
    query: str,
    memory_budget_tokens: int = TOKEN_BUDGET["scored_memories"]
) -> dict:
    """Recall, score, and trim memories to fit the token budget."""
    # Step 1: pinned memories (goals, lessons, identity), outside the scored budget
    pinned = client.recall(
        agent_id=agent_id,
        query="active goals lessons critical identity constraints",
        top_k=10,
        min_importance=0.90
    ).memories
    pinned_ids = {m.id for m in pinned}
    pinned_tokens = sum(estimate_tokens(m.content) for m in pinned)

    # Step 2: broad recall for scored candidates
    candidates = client.recall(
        agent_id=agent_id,
        query=query,
        top_k=50,
        min_importance=0.30
    ).memories

    # Step 3: score every candidate not already pinned, highest first
    scored = sorted(
        (score_memory(m) for m in candidates if m.id not in pinned_ids),
        key=lambda m: m.composite_score,
        reverse=True,
    )

    # Step 4: fill the budget greedily
    selected: list[ScoredMemory] = []
    tokens_used = 0
    trimmed_count = 0
    for mem in scored:
        if tokens_used + mem.token_estimate > memory_budget_tokens:
            trimmed_count += 1
            continue
        selected.append(mem)
        tokens_used += mem.token_estimate

    # Step 5: build the context block, pinned first
    pinned_context = "\n".join(m.content for m in pinned)
    scored_context = "\n".join(m.content for m in selected)
    full_context = ""
    if pinned_context:
        full_context += f"[Critical context]\n{pinned_context}\n\n"
    if scored_context:
        full_context += f"[Relevant context]\n{scored_context}"

    return {
        "context": full_context,
        "stats": {
            "pinned_memories": len(pinned),
            "pinned_tokens": pinned_tokens,
            "scored_selected": len(selected),
            "scored_trimmed": trimmed_count,
            "scored_tokens": tokens_used,
            "total_tokens": pinned_tokens + tokens_used,
            "budget_utilization": round(tokens_used / memory_budget_tokens, 3),
            "hit_rate": round(len(selected) / max(len(scored), 1), 3),
        }
    }

# --- Usage in a long document analysis agent ---
def analyze_document(agent_id: str, document_text: str, question: str) -> str:
    context = build_memory_context(agent_id, query=question)
    stats = context["stats"]
    print(f"Memory context: {stats['scored_selected']} memories, "
          f"{stats['total_tokens']:,} tokens, "
          f"{stats['budget_utilization']:.0%} budget used, "
          f"{stats['scored_trimmed']} memories trimmed")

    system_prompt = f"""You are a document analysis assistant.

{context['context']}

Document excerpt:
{document_text[:10000]}"""

    # return llm.complete(system=system_prompt, user=question)
    return f"[Context loaded: {stats['total_tokens']} tokens] Ready to answer: {question}"

print(analyze_document("doc-analysis-agent", "...", "What are the key risk factors?"))
import { DakeraClient } from '@dakera-ai/dakera';
import type { RecalledMemory } from '@dakera-ai/dakera';

const client = new DakeraClient({ baseUrl: 'http://localhost:3000', apiKey: 'dk-...' });

const TOKEN_BUDGET = {
  systemPrompt: 8_000,
  pinnedMemories: 12_000,
  scoredMemories: 20_000,
  conversation: 18_000,
  userMessage: 3_000,
  responseReserved: 67_000,
};

// Application logic: relevance is Dakera's recall score (smart_score)
const SCORING_WEIGHTS = { relevance: 0.5, importance: 0.3, recency: 0.2 };
const RECENCY_DECAY_HOURS = 168; // 7 days

function estimateTokens(text: string): number {
  return Math.max(1, Math.floor(text.length / 4));
}

function computeRecencyFactor(createdAt?: string | number): number {
  if (createdAt === undefined) return 0.5;
  const n = Number(createdAt);
  const ms = Number.isNaN(n) ? Date.parse(String(createdAt)) : n * 1000; // unix seconds or ISO
  if (Number.isNaN(ms)) return 0.5;
  const hoursOld = (Date.now() - ms) / 3_600_000;
  return Math.max(0, 1 - hoursOld / RECENCY_DECAY_HOURS);
}

interface ScoredMemory {
  content: string;
  compositeScore: number;
  tokenEstimate: number;
}

function scoreMemory(mem: RecalledMemory): ScoredMemory {
  const composite =
    mem.score * SCORING_WEIGHTS.relevance +
    mem.importance * SCORING_WEIGHTS.importance +
    computeRecencyFactor(mem.created_at) * SCORING_WEIGHTS.recency;
  return {
    content: mem.content,
    compositeScore: Math.round(composite * 10000) / 10000,
    tokenEstimate: estimateTokens(mem.content),
  };
}

async function buildMemoryContext(agentId: string, query: string, memoryBudget = TOKEN_BUDGET.scoredMemories) {
  // Pinned memories (always included, outside the scored budget)
  const pinned = (await client.recall(agentId, 'active goals lessons critical identity', {
    top_k: 10,
    min_importance: 0.9,
  })).memories;
  const pinnedIds = new Set(pinned.map(m => m.id));
  const pinnedTokens = pinned.reduce((sum, m) => sum + estimateTokens(m.content), 0);

  // Broad candidate recall, minus anything already pinned
  const candidates = (await client.recall(agentId, query, { top_k: 50, min_importance: 0.3 }))
    .memories.filter(m => !pinnedIds.has(m.id));

  // Score and sort
  const scored = candidates.map(scoreMemory).sort((a, b) => b.compositeScore - a.compositeScore);

  // Budget fill
  const selected: ScoredMemory[] = [];
  let tokensUsed = 0;
  let trimmedCount = 0;
  for (const mem of scored) {
    if (tokensUsed + mem.tokenEstimate > memoryBudget) { trimmedCount++; continue; }
    selected.push(mem);
    tokensUsed += mem.tokenEstimate;
  }

  const pinnedBlock = pinned.map(m => m.content).join('\n');
  const scoredBlock = selected.map(m => m.content).join('\n');
  const context = [
    pinnedBlock ? `[Critical context]\n${pinnedBlock}` : '',
    scoredBlock ? `[Relevant context]\n${scoredBlock}` : '',
  ].filter(Boolean).join('\n\n');

  return {
    context,
    stats: {
      pinnedCount: pinned.length,
      selectedCount: selected.length,
      trimmedCount,
      totalTokens: pinnedTokens + tokensUsed,
      budgetUtilization: Math.round((tokensUsed / memoryBudget) * 1000) / 1000,
    },
  };
}

// Usage
const { context, stats } = await buildMemoryContext('doc-analysis-agent', 'key risk factors in contract');
console.log(`Loaded ${stats.selectedCount} memories, ${stats.totalTokens} tokens (${(stats.budgetUtilization * 100).toFixed(0)}% of budget)`);
use dakera_client::{DakeraClient, RecallRequest};
use std::time::{SystemTime, UNIX_EPOCH};

let client = DakeraClient::builder("http://localhost:3000").api_key("dk-...").build()?;

const MEMORY_BUDGET_TOKENS: usize = 20_000;
const RECENCY_DECAY_HOURS: f64 = 168.0;

fn estimate_tokens(text: &str) -> usize {
    (text.len() / 4).max(1)
}

fn recency(created_at: u64) -> f64 {
    let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
    let hours_old = now.saturating_sub(created_at) as f64 / 3600.0;
    (1.0 - hours_old / RECENCY_DECAY_HOURS).max(0.0)
}

// Broad candidate recall
let candidates = client.recall(
    RecallRequest::new("doc-analysis-agent", "document analysis context user preferences")
        .with_top_k(50)
        .with_min_importance(0.3),
).await?;

// Composite score (application logic): Dakera's score, importance, recency
let mut scored: Vec<(f64, &str, usize)> = candidates.memories.iter()
    .map(|mem| {
        let score = mem.score as f64 * 0.5
            + mem.importance as f64 * 0.3
            + recency(mem.created_at) * 0.2;
        (score, mem.content.as_str(), estimate_tokens(&mem.content))
    })
    .collect();
scored.sort_by(|a, b| b.0.total_cmp(&a.0));

// Budget fill
let mut context_parts: Vec<&str> = Vec::new();
let mut tokens_used = 0usize;
let mut trimmed = 0usize;
for (_, content, tokens) in &scored {
    if tokens_used + tokens > MEMORY_BUDGET_TOKENS {
        trimmed += 1;
        continue;
    }
    context_parts.push(content);
    tokens_used += tokens;
}

let memory_context = context_parts.join("\n");
println!("Selected {} memories, {} tokens, {} trimmed", context_parts.len(), tokens_used, trimmed);
client := dakera.NewClientWithOptions(dakera.ClientOptions{
    BaseURL: "http://localhost:3000",
    APIKey:  "dk-...",
})
ctx := context.Background()

const memoryBudgetTokens = 20_000
const recencyDecayHours = 168.0

func estimateTokens(text string) int {
    t := len(text) / 4
    if t < 1 {
        return 1
    }
    return t
}

// Broad recall
minImp := float32(0.3)
candidates, err := client.Recall(ctx, "doc-analysis-agent", dakera.RecallRequest{
    Query:         "document analysis context user preferences",
    TopK:          50,
    MinImportance: &minImp,
})
if err != nil {
    log.Fatal(err)
}

type scoredMem struct {
    content string
    score   float64
    tokens  int
}

// Composite score (application logic): Dakera's score, importance, recency
scored := make([]scoredMem, len(candidates.Memories))
for i, mem := range candidates.Memories {
    hoursOld := time.Since(time.Unix(mem.CreatedAt, 0)).Hours()
    recency := math.Max(0, 1-hoursOld/recencyDecayHours)
    composite := float64(mem.Score)*0.5 + float64(mem.Importance)*0.3 + recency*0.2
    scored[i] = scoredMem{mem.Content, composite, estimateTokens(mem.Content)}
}
sort.Slice(scored, func(i, j int) bool { return scored[i].score > scored[j].score })

// Budget fill
var selected []string
tokensUsed, trimmed := 0, 0
for _, m := range scored {
    if tokensUsed+m.tokens > memoryBudgetTokens {
        trimmed++
        continue
    }
    selected = append(selected, m.content)
    tokensUsed += m.tokens
}

memoryContext := strings.Join(selected, "\n")
fmt.Printf("Selected %d memories, %d tokens, %d trimmed\n", len(selected), tokensUsed, trimmed)

Stop paying for context you don't use

Budget-managed memory loading keeps prompt size and cost predictable while improving response quality.

Try Free →

Real-World Scenario: Long Document Analysis Agent

A legal research agent analyzes contracts and regulatory documents. It serves 50 attorneys who each have months of interaction history. Naively loading all memories per request was costing $0.18/call and adding 3.2 seconds of latency. After implementing context window management:

Before / After Memory State

Before: Naive Memory Injection
// All 180 memories injected:
// Total: 87,400 tokens
// Cost per call: $0.18
// TTFT: 4.1 seconds
// Response quality: 3.8/5

// Context is 68% memories:
// LLM can't focus on the
// actual question
// High hallucination rate
// on complex queries
After: Budget-Managed Injection
// Budget-managed context:
// Pinned: 8 memories, 9.2k tokens
// Scored: 14 memories, 16.2k tokens
// Total memory: 25.4k tokens
// Cost per call: $0.084 (-53%)
// TTFT: 1.9 seconds (-54%)
// Response quality: 4.5/5
// Budget utilization: 81%

SDK Method Reference

MethodSDKPurpose in this pattern
recall(agent_id, query, top_k, min_importance)PythonBroad candidate recall + pinned memory retrieval
batch_recall(BatchRecallRequest(agent_id, filter))PythonAlternative for pinned memories: list them by tag and min_importance, with no query
recall(agentId, query, {top_k, min_importance})TypeScriptCandidate and pinned recall
batchRecall({agent_id, filter})TypeScriptThe same filter-based listing in TypeScript
client.recall(RecallRequest::new(agent_id, query)).await?RustAsync broad candidate recall
client.Recall(ctx, agentID, dakera.RecallRequest{...})GoRecall with min_importance filter

Edge Cases and Gotchas

Warning: Never inject unscored raw recall into production prompts

It's tempting to skip the scoring step and just inject top_k=10 results directly. This works at small scale but breaks under load: as your memory store grows, Dakera returns increasingly diverse results for the same query. Without scoring and trimming, you lose control over what enters the context window. Always score, always budget — even in development.

Performance Considerations

-53%
Tokens per call in the legal research scenario
20k
Scored memory budget in the 128k example
70-85%
Target memory budget utilization rate
Advanced Configuration: Dynamic weight adjustment by query type

Different query types benefit from different scoring weights. Implement a query classifier that selects the right weight configuration:

WEIGHT_PROFILES = {
    "factual":      {"relevance": 0.70, "importance": 0.20, "recency": 0.10},
    "temporal":     {"relevance": 0.40, "importance": 0.20, "recency": 0.40},
    "preference":   {"relevance": 0.50, "importance": 0.40, "recency": 0.10},
    "emotional":    {"relevance": 0.40, "importance": 0.30, "recency": 0.30},
    "default":      {"relevance": 0.50, "importance": 0.30, "recency": 0.20},
}

def classify_query_type(query: str) -> str:
    """Simple keyword-based query classifier."""
    q = query.lower()
    if any(w in q for w in ["when", "date", "recently", "latest", "yesterday"]):
        return "temporal"
    if any(w in q for w in ["prefer", "like", "want", "style", "format"]):
        return "preference"
    if any(w in q for w in ["feel", "emotion", "frustrated", "happy", "stress"]):
        return "emotional"
    if any(w in q for w in ["what", "who", "how many", "define", "explain"]):
        return "factual"
    return "default"

def build_memory_context_dynamic(agent_id: str, query: str, budget: int) -> dict:
    query_type = classify_query_type(query)
    weights = WEIGHT_PROFILES[query_type]
    return build_memory_context_with_weights(agent_id, query, budget, weights)
Try it live

Run this pattern against a real Dakera.

The Playground executes live API calls in your browser. No account, no install. Then run it on your own server in five minutes.