Context Engineering & Memory
Context is the model's workspace for one decision, not a document warehouse. Memory is not permanent chat storage; it is a controlled process that extracts reusable facts and retrieves them for an appropriate future task. Reliability depends on what enters the current inference, not how much text sits in a database.
Separate four kinds of state
| Concept | Lifetime | Visible to | Typical content |
|---|---|---|---|
| Local run state | One run | Application code and tools | Database clients, user ID, retry count |
| LLM-visible context | One model call | The model | Instructions, messages, tool definitions, retrieved passages |
| Working memory | Current task | Agent and model | Plan, completed steps, open decisions |
| Long-term memory | Across sessions | Persistent system, retrieved on demand | Confirmed preferences, business facts, past decisions |
The OpenAI Agents SDK makes the same distinction between local context and model-visible context. A tool can access application dependencies without sending a database connection or secret to the model.
business data and dependencies → local context → tool
│
└─ only task results enter LLM context
past events → memory write gate → memory store
│
current task → retrieval + policy ────┘ → working set → LLM
The goal is not a full context window
Anthropic describes context engineering as continuously selecting the tokens most likely to support the desired behaviour. The practical target is a smallest sufficient set.
Organise one model call in layers:
- Stable instructions: role, safety boundaries, and output contract.
- Current objective: user outcome and acceptance criteria.
- Working state: completed steps, open questions, and remaining budget.
- On-demand knowledge: retrieved passages or memories with sources.
- Tool definitions: only the tools available for this decision.
- Recent observations: results that still affect the next action.
Do not hard-code a rule that context must consume a particular percentage of the window. Models, tasks, and output needs vary. Reserve output and tool-result capacity first, then set thresholds through evals.
Allocate a context budget
type ContextBudget = {
modelLimit: number;
reservedOutput: number;
stableInstructions: number;
taskState: number;
retrievedKnowledge: number;
toolDefinitions: number;
recentObservations: number;
};
function assertBudget(b: ContextBudget) {
const inputBudget = b.modelLimit - b.reservedOutput;
const planned =
b.stableInstructions +
b.taskState +
b.retrievedKnowledge +
b.toolDefinitions +
b.recentObservations;
if (planned > inputBudget) throw new Error('CONTEXT_BUDGET_EXCEEDED');
}
When over budget, remove duplication, truncate low-value tool output, compact old observations, reduce retrieval passages, and defer unused tools. Stable safety instructions should not be the first content removed.
What belongs in memory
| Type | Example | Long-term fit |
|---|---|---|
| Semantic fact | User timezone or project name | Store when confirmed and useful |
| Episodic event | Failed deployment or handled order | Store when recovery or audit needs it |
| Preference | Concise responses or default export type | Store after explicit expression |
| Procedure | Team publication checklist | Version as shared knowledge, not personal preference |
| Temporary state | Current form input or one-time code | Never long-term memory |
| Sensitive data | Password or full payment details | Prohibit or use a dedicated compliant system |
“The user said it” does not mean “remember it forever.” Before writing, ask whether the information is explicit, likely to change, useful later, permitted to store, and subject to expiry.
Put a gate in front of memory writes
type MemoryCandidate = {
tenantId: string;
userId: string;
kind: 'fact' | 'preference' | 'event' | 'procedure';
value: string;
sourceRef: string;
observedAt: string;
confidence: number;
expiresAt?: string;
sensitivity: 'public' | 'internal' | 'personal' | 'restricted';
};
function mayPersist(candidate: MemoryCandidate) {
return (
candidate.confidence >= 0.9 &&
candidate.sensitivity !== 'restricted' &&
Boolean(candidate.tenantId && candidate.userId && candidate.sourceRef)
);
}
A real system also applies consent, regional retention, deletion requests, and field-level encryption. The model may propose a candidate; deterministic policy decides whether to persist it.
Retrieval is more than vector similarity
current task
→ hard tenant/user/region filter
→ kind, time, and permission filter
→ semantic or keyword recall
→ deduplication and conflict detection
→ relevance + recency + confidence reranking
→ token-budget truncation
→ inject with sourceRef
const memories = await memoryStore.search({
tenantId: run.tenantId,
userId: run.userId,
query: run.objective,
kinds: ['fact', 'preference'],
notExpiredAt: new Date().toISOString(),
limit: 12
});
const workingSet = rerank(memories)
.filter(item => item.score >= retrievalThreshold)
.slice(0, maxMemoryItems)
.map(({ value, sourceRef, observedAt }) => ({ value, sourceRef, observedAt }));
Tenant and user constraints belong in the database query. Never retrieve across tenants and ask the model to decide what it may reveal.
Conflicts, expiry, and correction
Memory is not immutable truth. If a user first lives in Brisbane and later explicitly moves to Sydney, preserve provenance and mark the older fact as superseded.
{
"memoryId": "mem_204",
"key": "home_city",
"value": "Sydney",
"observedAt": "2026-08-25T11:10:00+10:00",
"sourceRef": "message_991",
"status": "active",
"supersedes": "mem_102"
}
When a conflict cannot be resolved deterministically, ask the user. For volatile facts such as prices, laws, job roles, and exam rules, prefer an authoritative current source over an old memory.
Preserve recoverable state during compaction
## Objective
Deploy checkout-api after regression tests pass.
## Confirmed facts
- Current branch: release/checkout-42 [source: git]
- Payment E2E is failing at 3DS callback [source: run_87]
## Decisions
- Do not bypass the 3DS test.
- Use provider sandbox account only.
## Completed
- Unit tests passed at commit 1a2b3c4.
## Open work
- Fix callback signature verification.
## External actions
- No deployment has occurred.
Remove small talk, duplicate logs, and observations that no longer affect decisions. Preserve the objective, hard constraints, evidence, external side effects, and next safe action.
Prevent persistent prompt injection
Web pages, email, and tool results are untrusted data. If attack text is summarised into long-term memory, it can influence unrelated future tasks.
| Risk | Boundary |
|---|---|
| Document says to ignore system instructions | Mark retrieved content as data, never higher-priority instructions |
| Malicious text becomes memory | Extract facts into candidates; do not copy commands |
| Cross-tenant leakage | Enforce tenant filters at query time |
| Secrets enter traces | Redact fields before persistence |
| Old fact overrides new fact | Store time, source, status, and supersession chain |
The model must not be the sole authority deciding that arbitrary text is safe to remember permanently.
Evaluate context and memory behaviour
| Eval | What to measure |
|---|---|
| Retrieval precision | How many injected memories help the current task |
| Retrieval recall | Whether every necessary fact was found |
| Context efficiency | Input tokens per successful task |
| Faithfulness | Whether answers follow sourceRef without invented detail |
| Update correctness | Whether new facts correctly supersede old facts |
| Isolation | Whether other tenant/user content remains zero |
| Deletion | Whether deleted memory stays unretrievable |
| Topic switch | Whether stale working context is removed |
Include same-name users, conflicting preferences, expired facts, malicious documents, empty retrieval, and oversized tool output. “Remember that I like coffee” is not a sufficient production eval.
Common mistakes
| Mistake | Result | Fix |
|---|---|---|
| Persisting entire conversations | High cost plus stored secrets and injections | Persist structured candidates only |
| Using vector top-k alone | Similar but unauthorised or expired results | Hard-filter before recall and reranking |
| Omitting source and time | Conflicts cannot be resolved | Require sourceRef and observedAt |
| Sending local context to the model | Secrets and dependencies become visible | Separate code state from LLM context |
| Compaction keeps conclusions only | A new session cannot verify them | Preserve evidence IDs and external state |
Practice: a cross-session project assistant
- Define schemas for facts, preferences, and events.
- Reject passwords, one-time codes, and unconfirmed inferences at the write gate.
- Filter by tenant, user, expiry, and
sourceRef. - Simulate a city change and verify the new fact supersedes the old one.
- Add malicious instructions to a retrieved document and confirm they never enter long-term memory.
- Record answer quality, input tokens, retrieval precision, and cross-user leakage.
Definition of done
- I can distinguish local state, LLM context, working memory, and long-term memory.
- Code and policy control writes; the model does not decide alone.
- Retrieval hard-filters tenant and user before semantic search.
- Every long-term fact has source, time, status, and deletion path.
- Compaction preserves objectives, decisions, evidence, and side effects.
Related reading
Official references
📚 Related resources
❓ Common questions
Open a question to review the practical answer.
History keeps growing and tokens blow up — how do I manage it?
Combine three strategies: (1) sliding window — keep only the last N turns; (2) summarisation — compress older turns into bullets while preserving IDs / timestamps; (3) topical caches — store per-topic summaries and swap them in/out as the subject shifts. On topic change also trigger a reset: resend the core instructions and drop stale history. Aim for context ≤ 60-70% of the window so 1/3 stays free for output.
In what order should I lay out system / user / history / tools?
Instruction hierarchy: (1) System holds non-negotiable rules (role, language, safety) at the top; (2) Task/User is the current request and constraints; (3) History keeps only necessary turns — summarise the older content first; (4) Tools carry function specs and expectations. For RAG: instructions → constraints → retrieved snippets (with IDs) → question. Critical info sits at the start or the end, never in the middle.
Where do I store long-term memory like user preferences or historical facts?
Short-term memory is the live dialogue plus working set; long-term memory is a vector store or KV store of facts/preferences, retrieved by query plus tenant/user. Ephemeral memory auto-expires or rotates so PII does not pile up. Store facts as bullet lists or key-value blocks, never prose; tag every fact with an ID for citation; keep numbers and dates in canonical units and formats.
How do I keep the memory system safe from prompt injection?
Three gates: (1) when summarising, strip out user-supplied prompt fragments so injection cannot persist into memory; (2) redact secrets and PII before write and read; (3) tag every record by tenant / user / region and enforce filters at retrieval. In production also run token audits (context size under typical and peak load) and regression checks (core instructions still present after packing).
How do I make sure context packing has not crowded out the core instructions?
Minimum checklist: (1) instruction hierarchy enforced — core rules always make it in; (2) history trimmed/summarised with IDs, total context budget ≤ 70% of the window; (3) retrieved snippets deduped, cited and tenant-filtered. Add a topic-switch test: deliberately shift subjects and confirm summary + reset behaviour. Run the whole regression each release.