Context engineering, from vague request to working change
Picture a developer picking up this ticket on Monday morning:
Add
POST /auth/refreshto our FastAPI service. Reuse the JWT code that is already there.
They open a coding assistant but paste only this generic sentence, leaving the ticket out:
You are an expert Python developer. Write clean, scalable, production-ready authentication code following best practices.
The sentence sounds professional, but it does not describe the ticket or the codebase. The assistant still has to guess what to build and how this service works.
What you will understand
Section titled “What you will understand”| Chapter | Question you will be able to answer |
|---|---|
| 1. Mental model | How do “tell it, show it, check it” fit together? |
| 2. Difference | What belongs to the prompt, and what belongs to context? |
| 3. Worked ticket | Which files does a FastAPI change need? |
| 4. Selection | How can the application choose a small context packet? |
| 5. Risk | What must never enter the model context? |
| 6. Evaluation | How do we test the context builder itself? |
Chapter 1: Tell it, show it, and check it
Section titled “Chapter 1: Tell it, show it, and check it”Use this whenever a model works on real code:
| Step | Plain-English question | Engineering job | FastAPI example |
|---|---|---|---|
| 1. Tell it | What exactly should change? | Prompt engineering | Add one refresh route, preserve current behavior, return a patch and tests |
| 2. Show it | What facts does it need? | Context engineering | Supply the auth route, JWT helper, user model, response schema, and tests |
| 3. Check it | How will we know it worked? | Validation and evaluation | Run the same tests for valid, expired, and disabled-user tokens |
Tell it. Show it. Check it. The first two help the model propose a change. The third decides whether that change belongs in the codebase.
The message sent to the assistant misses the first two steps:
- The task is vague. Should the model add login, refresh tokens, OAuth, or session cookies? What must it return?
- The project is missing. The model cannot see the framework, current authentication flow, data models, utilities, tests, or security boundaries unless the application supplies them.
Prompt engineering addresses the first gap. Context engineering addresses the second.
Chapter 2: Prompt and context answer different questions
Section titled “Chapter 2: Prompt and context answer different questions”| Question | Prompt engineering | Context engineering |
|---|---|---|
| What does it control? | The task, instructions, constraints, examples, and output contract | The complete working set available for this model call |
| What does an engineer change? | Usually the written instruction | Which information is retrieved, filtered, ordered, compressed, or removed |
| What would failure look like? | The model adds login instead of refresh, or returns prose instead of a patch | The model invents a JWT API because the real helper was absent |
| What is the FastAPI example? | “Add one endpoint, preserve behavior, return a patch and tests” | The auth router, JWT utility, user model, API schema, relevant tests, and approved tools |
Anthropic describes prompt engineering as writing and organizing instructions, while context engineering curates all tokens used during inference, including tools, external data, and message history.1 GitHub’s Copilot guide makes the same practical distinction: a prompt is the request, and the product may also use the current file and chat history as context.2
They are complements, not rivals. A perfect prompt cannot reveal a private API that was never supplied. A large context packet cannot repair an ambiguous task or an undefined output format. Measure both against representative tasks instead of assuming one always matters more.
At the API level, the prompt text is itself part of the model’s context. The two engineering terms describe different areas of focus, not two separate requests sent to the model.
Chapter 3: Walk through the FastAPI ticket
Section titled “Chapter 3: Walk through the FastAPI ticket”1. Tell it what done means
Section titled “1. Tell it what done means”Turn the ticket into a bounded request:
TaskAdd POST /auth/refresh to the existing FastAPI service.
Instructions- Reuse the current JWT helpers and response model.- Make the smallest change and preserve existing behavior.- Add tests for success, expired tokens, and disabled users.
Output contractReturn a patch, tests, and any unresolved assumption.The phrase “production-ready” adds little. The route, constraints, cases, and output format do the real work.
2. Show it the local contracts
Section titled “2. Show it the local contracts”Do not dump the repository. Start with the files that answer a concrete question:
| File | Why it belongs in this request |
|---|---|
app/api/auth.py |
Shows where the route belongs and which response types it returns |
app/security/jwt.py |
Defines the real token verification and creation functions |
app/models/user.py |
Carries the disabled-account rule the new route must preserve |
tests/test_auth.py |
Shows fixtures, naming, and behavior the project already expects |
pyproject.toml |
Pins Python and reveals the test, formatting, and typing conventions |
This packet gives the assistant a destination and a map. It still does not grant permission to read secrets, call the network, or edit unrelated files.
3. Check the proposed change
Section titled “3. Check the proposed change”The review should answer three questions in order:
- Does it fit? The patch imports existing helpers and follows the current route and response patterns.
- Does it behave? Valid refresh tokens succeed; expired tokens and disabled users fail in the expected way.
- Does it stay inside the boundary? The patch does not expose secrets, widen permissions, or change unrelated authentication behavior.
A fluent explanation is not a passing result. The code must compile and the acceptance tests must pass.
- 01Tell it: define the change
- 02Show it: select trusted context
- 03Check it: compile, test, and review
Chapter 4: Select a small, useful context packet
Section titled “Chapter 4: Select a small, useful context packet”| Context type | Coding-assistant example | Question to ask |
|---|---|---|
| Relevant files | Router, service, model, configuration | Does this file affect the requested behavior? |
| API and schema definitions | Function signatures, OpenAPI, database schema | Which contracts must generated code obey? |
| Project conventions | Formatter, error shape, directory pattern | Which local choices replace generic “best practices”? |
| Tests and examples | Existing auth tests and fixtures | What behavior already counts as correct? |
| Tools | Search, test runner, type checker, version control | Which capability is needed and permitted? |
| History and state | The user’s correction, failed test, current plan | What recent decision must survive this call? |
| Runtime data | Signed-in tenant, feature flag, current policy | Is it current, authorized, and necessary? |
Do not send every row on every request. Context engineering is the selection process, not the act of filling the context window.
A deterministic selector
Section titled “A deterministic selector”This standard-library example ranks synthetic project files by task tags. Before ranking, it blocks secrets, stale material, and irrelevant files. It then fits the remaining items into a fixed illustrative budget.
"""Build a small, inspectable context packet for a coding task."""
from dataclasses import dataclassfrom datetime import datefrom typing import Literal
Sensitivity = Literal["public", "internal", "secret"]
@dataclass(frozen=True)class ContextItem: path: str tags: frozenset[str] token_estimate: int last_verified: date sensitivity: Sensitivity = "internal"
def item( path: str, tags: str, tokens: int, verified: str = "2026-08-15", sensitivity: Sensitivity = "internal",) -> ContextItem: """Keep the synthetic fixtures short enough to scan in the lesson.""" return ContextItem( path=path, tags=frozenset(tags.split()), token_estimate=tokens, last_verified=date.fromisoformat(verified), sensitivity=sensitivity, )
FILES = ( item("app/api/auth.py", "auth fastapi route", 65, "2026-08-14"), item("app/security/jwt.py", "auth jwt api", 55), item("app/models/user.py", "auth user-model", 45, "2026-08-10"), item("tests/test_auth.py", "auth tests", 60), item("pyproject.toml", "conventions", 25, "2026-08-01"), item(".env", "auth jwt", 12, sensitivity="secret"), item("docs/legacy-oauth.md", "auth oauth", 110, "2025-10-01"), item("app/billing/invoices.py", "billing", 90),)
def select_context( candidates: tuple[ContextItem, ...], needed_tags: frozenset[str], *, as_of: date, max_age_days: int, token_budget: int,) -> tuple[tuple[ContextItem, ...], tuple[tuple[str, str], ...]]: """Select relevant, allowed, fresh items without exceeding the budget.""" ranked: list[tuple[int, ContextItem]] = [] rejected: list[tuple[str, str]] = []
for candidate in candidates: if candidate.sensitivity == "secret": rejected.append((candidate.path, "blocked: secret")) continue
age_days = (as_of - candidate.last_verified).days if age_days > max_age_days: rejected.append((candidate.path, f"blocked: stale ({age_days} days)")) continue
overlap = len(candidate.tags & needed_tags) if overlap == 0: rejected.append((candidate.path, "skipped: irrelevant")) continue
# Prefer more matching signals, then the smaller item. The path breaks # exact ties, so identical inputs always produce an identical packet. score = overlap * 1000 - candidate.token_estimate ranked.append((score, candidate))
selected: list[ContextItem] = [] used_tokens = 0 for _, candidate in sorted(ranked, key=lambda pair: (-pair[0], pair[1].path)): if used_tokens + candidate.token_estimate > token_budget: rejected.append((candidate.path, "skipped: budget")) continue selected.append(candidate) used_tokens += candidate.token_estimate
return tuple(selected), tuple(rejected)
if __name__ == "__main__": prompt = { "task": "Add POST /auth/refresh to the existing FastAPI service.", "instructions": "Reuse existing JWT helpers; preserve behavior; add tests.", "output_contract": "Return a patch, tests, and unresolved assumptions.", } selected, rejected = select_context( FILES, frozenset( {"auth", "fastapi", "route", "jwt", "api", "user-model", "tests", "conventions"} ), as_of=date(2026, 8, 15), max_age_days=120, token_budget=250, )
print(f"TASK: {prompt['task']}") print("\nSELECTED CONTEXT") for context_item in selected: print(f"- {context_item.path} ({context_item.token_estimate} estimated tokens)") print("\nREJECTED CONTEXT") for path, reason in rejected: print(f"- {path}: {reason}")Run it on Windows from an activated Python 3.10+ virtual environment:
python --versionpython src/examples/foundations/context_packet.pyOn macOS or Linux:
python3 --versionpython3 src/examples/foundations/context_packet.pyExpected result:
SELECTED CONTEXT- app/security/jwt.py (55 estimated tokens)- app/api/auth.py (65 estimated tokens)- app/models/user.py (45 estimated tokens)- tests/test_auth.py (60 estimated tokens)- pyproject.toml (25 estimated tokens)
REJECTED CONTEXT- .env: blocked: secret- docs/legacy-oauth.md: blocked: stale (318 days)- app/billing/invoices.py: skipped: irrelevantThe token counts are fixture metadata, not real tokenizer output. A production implementation should use the selected model’s tokenizer, derive authorization from the signed-in identity, and test whether the selection improves task success.
Chapter 5: Treat repository context as untrusted and governed
Section titled “Chapter 5: Treat repository context as untrusted and governed”| Risk | Example | Control |
|---|---|---|
| Budget | Full repository files crowd out the task and output room | Estimate input and output tokens; retrieve sections; cap tool results |
| Security | .env, another tenant’s code, or private records enter the packet |
Enforce access in application code; redact secrets; log source IDs, not sensitive text |
| Staleness | An old OAuth design overrides the current JWT implementation | Prefer source-of-truth files; keep version and verification metadata; refresh before use |
| Noise | Billing code and old chat turns distract from authentication | Rank for task relevance; deduplicate; drop superseded history; evaluate selection precision |
Retrieved content can also contain instructions written by an attacker. OWASP calls this indirect prompt injection and recommends separating external content, limiting privileges, validating output, and requiring approval for high-risk actions.3 Labeling data as “context” does not make it trusted. The model should not decide its own permissions.
Enforce filesystem, network, and tool permissions outside the model. Those controls limit what a followed injection can do; prompt wording alone does not.
Chapter 6: Evaluate the context builder, not just the prose
Section titled “Chapter 6: Evaluate the context builder, not just the prose”Create a small set of real coding tasks with acceptance tests. For each context-builder version, record the same signals:
| Signal | Scoring question | First place to inspect |
|---|---|---|
| Required-context coverage | What fraction of labeled required contracts entered the packet? supplied required contracts / all labeled required contracts |
Retrieval or selection |
| Noise | What fraction of supplied items were labeled irrelevant or duplicate? irrelevant supplied items / all supplied items |
Ranking, deduplication, or budget rules |
| Trust | Were all selected items authorized and current? | Access and freshness filters |
| Task success | Did the patch compile and pass the same acceptance tests? | Inspect the prompt, context, and model output |
| Tokens, latency, and cost | What did this version consume? | Packet size, caching, or model routing |
Count tokens instead of items when one giant irrelevant file would otherwise look the same as one short irrelevant snippet. Keep the denominator fixed when comparing context-builder versions.
If an authentication patch fails because issue_access_token() was missing, fix retrieval or selection. If the right files were present but the model invented an endpoint shape, sharpen the instructions or schema. If the patch passes but exposes a secret, fix the security boundary; no wording change is sufficient.
Interview answer
Section titled “Interview answer”I remember it as tell it, show it, check it. Prompt engineering tells the model what change to make and how to return it. Context engineering shows it the smallest current, relevant, and authorized working set. Then tests and review check the result. For a FastAPI refresh route, that means a precise task, the existing auth contracts and tests, and acceptance checks for valid, expired, and disabled-user tokens.
Next: LLM foundations, the context-engineering glossary entry, or prompt-injection boundaries.
Footnotes
Section titled “Footnotes”-
Anthropic, “Effective context engineering for AI agents”, defines prompt engineering as writing and organizing instructions and context engineering as curating and maintaining the full token set used during inference. ↩
-
GitHub, “Prompt engineering for GitHub Copilot Chat”, says Copilot uses the prompt plus context such as the current file and chat history, and recommends indicating relevant code while removing irrelevant history. ↩
-
OWASP Gen AI Security Project, “LLM01:2025 Prompt Injection”, covers indirect instructions in external files or websites and lists separation, least privilege, validation, and human approval as mitigations. ↩