{"slug": "context-assembly-is-a-build-step-versioning-the-prompt-you-actually-send", "title": "Context Assembly Is a Build Step: Versioning the Prompt You Actually Send", "summary": "A developer argues that context assembly should be treated as a versioned build step, proposing a content-hash fingerprint over the inputs that affect model behaviour—model ID, template version, snapshot ID and rendered prompt—logged alongside every trace so quality regressions can be diagnosed without a code diff. The approach tags each context section with a trust level and eviction priority, screens untrusted retrieval and tool output for instruction-like text, and raises a hard error when a pinned section exceeds the token budget rather than silently trimming it.", "body_md": "You review a 30-line template diff. Production sends 40,000 tokens assembled at request time from six sources, four of which changed this week and none of which appear in the pull request. The diff you approved is not the change you shipped.\n\nThis is the part of LLMOps that keeps biting teams after they have evals, tracing, and a gateway in place. The template is versioned in git. The context is assembled by code paths nobody treats as release artifacts.\n\nList what actually goes into a request for a typical RAG-plus-tools system:\n\nEach is a dependency with its own version and its own deploy cadence. The retrieved chunks change when someone reindexes. The tool schema changes when a backend engineer renames a parameter and rewrites the description. Neither triggers a review of the prompt.\n\nSo when quality drops on Tuesday and no code shipped, there is no diff to look at. That is the failure this whole post is about.\n\nCompute a content hash over the inputs that can change model behaviour, and log it with every trace:\n\nStore fingerprints always. Store full rendered prompts sampled or redacted, depending on your data rules — full-text retention is a compliance decision, not an observability one. The fingerprint is small, cheap, and answers \"what changed\" during an incident in one query.\n\nThe trade-off: a fingerprint that includes a timestamp, a request id, or a per-user greeting has near-zero collision rate and zero diagnostic value. Hash the parts that affect generation, not the metadata around it. I have watched a team spend a day on \"why is our prompt hash changing every request\" when the answer was a locale string.\n\nYou do not need a framework for this. You need three properties: every section carries a trust level, every section carries an eviction priority, and over-budget on a pinned section is a hard error rather than a silent trim.\n\n``` python\nfrom dataclasses import dataclass, field\nfrom hashlib import sha256\nimport re\n\n@dataclass\nclass Section:\n    name: str\n    text: str\n    trusted: bool      # False for retrieval, tool output, user uploads\n    priority: int      # higher survives truncation\n    pinned: bool = False  # never evict; over budget means fail loudly\n\nINSTRUCTION_LIKE = re.compile(\n    r\"(ignore (all )?(previous|prior) instructions|you are now|system prompt)\",\n    re.IGNORECASE,\n)\n\ndef screen(text: str) -> str:\n    # Cheap heuristic, not a guarantee. Log hits and route to a stricter path.\n    return INSTRUCTION_LIKE.sub(\"[filtered]\", text)\n\ndef count_tokens(s: str) -> int:\n    # Swap in tiktoken/the provider counter. One counter per model family.\n    return len(s) // 4\n\n@dataclass\nclass Assembled:\n    prompt: str\n    fingerprint: str\n    dropped: list[str] = field(default_factory=list)\n\ndef assemble(sections, budget, model_id, template_version, snapshot_id):\n    ordered = sorted(sections, key=lambda s: (s.pinned, s.priority), reverse=True)\n    kept, dropped, used = [], [], 0\n    for s in ordered:\n        body = f\"<{s.name} trust={'high' if s.trusted else 'low'}>\\n\"\n        body += (s.text if s.trusted else screen(s.text)) + f\"\\n</{s.name}>\"\n        cost = count_tokens(body)\n        if used + cost > budget:\n            if s.pinned:\n                raise ValueError(f\"pinned section {s.name} exceeds budget\")\n            dropped.append(s.name)\n            continue\n        kept.append((s, body))\n        used += cost\n    # Render in original order so layout is stable across requests.\n    pos = {id(s): i for i, s in enumerate(sections)}\n    kept.sort(key=lambda pair: pos[id(pair[0])])\n    prompt = \"\\n\\n\".join(body for _, body in kept)\n    h = sha256()\n    for part in (model_id, template_version, snapshot_id, prompt):\n        h.update(part.encode())\n    return Assembled(prompt, h.hexdigest()[:16], dropped)\n\n# Usage: build once per request, log the fingerprint beside the trace id.\nctx = assemble(\n    sections=[\n        Section(\"question\", \"why was my claim denied?\", trusted=True, priority=10, pinned=True),\n        Section(\"policy_chunk\", retrieved_text, trusted=False, priority=5),\n        Section(\"profile\", tenant_blob, trusted=True, priority=1),\n    ],\n    budget=12_000,\n    model_id=\"claude-sonnet-4\",\n    template_version=\"support-v14\",\n    snapshot_id=index_build_id,\n)\n```\n\nTwo details in there are load-bearing. Sections render in original insertion order rather than priority order, because reordering the same content changes model behaviour and makes your eval deltas unattributable. And `pinned` turns budget pressure into an exception instead of a degraded", "url": "https://wpnews.pro/news/context-assembly-is-a-build-step-versioning-the-prompt-you-actually-send", "canonical_source": "https://dev.to/dima_kruglov_608741d9a416/context-assembly-is-a-build-step-versioning-the-prompt-you-actually-send-18od", "published_at": "2026-10-26 00:01:04+00:00", "updated_at": "2026-10-26 00:12:48.484390+00:00", "lang": "en", "topics": ["large-language-models", "ai-agents", "mlops", "ai-tools", "ai-safety"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/context-assembly-is-a-build-step-versioning-the-prompt-you-actually-send", "markdown": "https://wpnews.pro/news/context-assembly-is-a-build-step-versioning-the-prompt-you-actually-send.md", "text": "https://wpnews.pro/news/context-assembly-is-a-build-step-versioning-the-prompt-you-actually-send.txt", "jsonld": "https://wpnews.pro/news/context-assembly-is-a-build-step-versioning-the-prompt-you-actually-send.jsonld"}}