{"course":{"slug":"context-engineering","title":"Context Engineering","url":"https://addaly.com/learn/context-engineering","level":"advanced","track":"build"},"lesson":{"slug":"the-facts-that-must-survive","title":"State as fields, not as prose","url":"https://addaly.com/learn/context-engineering/the-facts-that-must-survive","position":52,"of":87,"minutes":8,"answer":"Hold the facts that must not be lost (ids, amounts, constraints, decisions and rejections) as named fields with provenance and supersession rather than as sentences in a summary.","keyIdea":"Hold the facts that must not be lost (ids, amounts, constraints, decisions and rejections) as named fields with provenance and supersession rather than as sentences in a summary, render them at a fixed position with an explicit precedence rule, and show them to the user so a bad extraction can be corrected before it compounds."},"body":{"format":"markdown","text":"## A slot cannot be paraphrased away\n\nSummarisation loses specifics because it is language compression, and language compression rounds. The fix is not a better summariser. It is to keep the things that must not be lost outside the prose entirely, as named fields.\n\n```json\n{\n  \"task\": \"Resolve a duplicate charge on order ORD-88213\",\n  \"entities\": {\"order_id\": \"ORD-88213\", \"amount\": \"4,180 INR\",\n                \"customer_id\": \"c_9912\"},\n  \"constraints\": [\"refund to original payment method\",\n                  \"customer wants confirmation by email\"],\n  \"decisions\": [{\"what\": \"issue partial refund\", \"turn\": 7}],\n  \"rejected\":  [{\"what\": \"store credit\", \"why\": \"customer declined\", \"turn\": 5}],\n  \"open\": [\"whether the second charge was captured or only authorised\"]\n}\n```\n\nRendered into the window in a fixed place, in a fixed order, on every turn. Perhaps 150 tokens. An order id in a JSON field survives any amount of compaction, because nothing summarises it: it is copied.\n\n## Update rules that do not lose history\n\nThe temptation is to overwrite fields as new information arrives. Resist it in two specific places.\n\n**Never silently overwrite a value the user stated.** If the amount changes from 4,180 to 4,050, that is either a correction or a different charge, and the difference matters. Record the new value with its turn and keep the old one marked superseded. A field that changes with no trace turns a real disagreement into a mystery.\n\n**Keep provenance.** Which turn set this, and was it stated by the user or inferred by the model? Those two have very different reliability, and a downstream decision (issue a refund of this amount) should be able to tell them apart.\n\n```python\nstate.set(\"amount\", \"4,050 INR\", turn=11, source=\"user\", supersedes=\"4,180 INR\")\n```\n\n## Extraction is a model call, so it has an error rate\n\nSomething has to populate the fields, and unless the user filled a form, that something is a model. Which means the state can be wrong, and unlike a wrong answer, a wrong state persists and shapes every subsequent turn.\n\nThree defences.\n\n**Extract with a schema and validate.** A date field that must parse, an amount that must match a currency pattern, an order id that must match your format and exist. Reject rather than store what fails.\n\n**Prefer explicit signals.** A value the user typed is more reliable than one inferred from a paraphrase. Where the interface can capture something directly (a selected order, an attached file, a chosen option) take it from the interface rather than from the text.\n\n**Show the state to the user.** This is the strongest defence and it is a product decision as much as an engineering one. A visible panel saying \"Order ORD-88213 · refund to original method · 4,050 INR\" lets a human catch the error the extractor made, in one glance, before it does any damage. Make it editable and you have turned an unreliable inference into a confirmed fact.\n\n## Where it goes in the window\n\nAt a fixed position, immediately before the recent turns and after the compacted middle. Fixed matters for two reasons: the model learns where to look within the request, and a stable position means the diff between two turns is small, which is friendlier to caching than a block that moves.\n\nLabel it plainly and say what it is:\n\n```\n<state>\nEstablished facts for this conversation. These override anything in the\nsummary below if they conflict.\n...\n</state>\n```\n\nThat conflict rule earns its tokens. Compaction will eventually produce a summary that contradicts a field, and stating the precedence resolves it deterministically instead of leaving it to the model.\n\n## Keep it small\n\nState is not a database. If the block grows past a few hundred tokens it has stopped being a set of load-bearing facts and become a second transcript, with the same growth problem and none of the readability. Cap the number of entries per field, drop resolved open questions, and archive superseded values to storage rather than carrying them in the window.\n\nA useful discipline: if you cannot read the whole state block in ten seconds, it is too big, and something in it is not load-bearing.\n\n## The pattern generalises\n\nThis is the same idea as the manifest in the selection module and the handover note later in this one: **keep the load-bearing facts in a structure your code controls, and let the prose be prose.**\n\nAnything you would be unhappy to see paraphrased belongs in a field. Everything else can be summarised, dropped, or left to the transcript. Once you have drawn that line, compaction stops being frightening: the worst it can do is lose texture, and the facts are somewhere it cannot reach."},"check":{"question":"Why does storing an order id in a structured state field survive compaction when the same id inside a summary paragraph often does not?","options":[{"text":"State fields are placed nearer the question, so position bias protects them.","correct":false,"tempting":"Position helps material be used, but it does not prevent a summariser from dropping a value during rewriting."},{"text":"Field values are copied by code rather than rewritten by a language model, so nothing rounds them away.","correct":true},{"text":"Structured blocks are excluded from the history budget, so they are never compacted.","correct":false,"tempting":"They consume budget like anything else; their protection comes from how they are produced, not from exemption."},{"text":"Models attend more strongly to JSON than to prose, so the id is weighted higher.","correct":false,"tempting":"Format affects legibility somewhat, but the id's survival is decided before the model reads anything."}]},"prev":{"slug":"compaction-when-and-how","title":"Compacting without losing the thread","url":"https://addaly.com/learn/context-engineering/compaction-when-and-how"},"next":{"slug":"memory-across-sessions","title":"Memory between conversations","url":"https://addaly.com/learn/context-engineering/memory-across-sessions"},"authorship":"Founder and series editor: Dr Mustafa Mun. Written by AI models to a written standard. Exam keys checked blind by a second model: 3,665 of 3,666 agreed. Academic review invited.","review":{"status":"Draft, open for academic review"},"cite":"Addaly (2026) 'State as fields, not as prose', in Context Engineering. Edited by M. Mun. Addaly. Available at: https://addaly.com/learn/context-engineering/the-facts-that-must-survive (Accessed: 13 October 2026).","licence":{"name":"CC BY-NC-SA 4.0","url":"https://creativecommons.org/licenses/by-nc-sa/4.0/"},"reader":{"schema":1,"title":"State as fields, not as prose","courseTitle":"Context Engineering","module":{"index":6,"slug":"conversation-and-memory","title":"Conversation, memory and compaction"},"answer":null,"keyIdea":"Hold the facts that must not be lost (ids, amounts, constraints, decisions and rejections) as named fields with provenance and supersession rather than as sentences in a summary, render them at a fixed position with an explicit precedence rule, and show them to the user so a bad extraction can be corrected before it compounds.","hero":{"kind":"plate","image":null,"alt":null,"caption":null},"blocks":[{"t":"h2","id":"a-slot-cannot-be-paraphrased-away","runs":[{"text":"A slot cannot be paraphrased away"}]},{"t":"p","runs":[{"text":"Summarisation loses specifics because it is language compression, and language compression rounds. The fix is not a better summariser. It is to keep the things that must not be lost outside the prose entirely, as named fields."}]},{"t":"code","lang":"json","label":"JSON","text":"{\n  \"task\": \"Resolve a duplicate charge on order ORD-88213\",\n  \"entities\": {\"order_id\": \"ORD-88213\", \"amount\": \"4,180 INR\",\n                \"customer_id\": \"c_9912\"},\n  \"constraints\": [\"refund to original payment method\",\n                  \"customer wants confirmation by email\"],\n  \"decisions\": [{\"what\": \"issue partial refund\", \"turn\": 7}],\n  \"rejected\":  [{\"what\": \"store credit\", \"why\": \"customer declined\", \"turn\": 5}],\n  \"open\": [\"whether the second charge was captured or only authorised\"]\n}"},{"t":"p","runs":[{"text":"Rendered into the window in a fixed place, in a fixed order, on every turn. Perhaps 150 tokens. An order id in a JSON field survives any amount of compaction, because nothing summarises it: it is copied."}]},{"t":"h2","id":"update-rules-that-do-not-lose-history","runs":[{"text":"Update rules that do not lose history"}]},{"t":"p","runs":[{"text":"The temptation is to overwrite fields as new information arrives. Resist it in two specific places."}]},{"t":"p","runs":[{"text":"Never silently overwrite a value the user stated.","b":true},{"text":" If the amount changes from 4,180 to 4,050, that is either a correction or a different charge, and the difference matters. Record the new value with its turn and keep the old one marked superseded. A field that changes with no trace turns a real disagreement into a mystery."}]},{"t":"p","runs":[{"text":"Keep provenance.","b":true},{"text":" Which turn set this, and was it stated by the user or inferred by the model? Those two have very different reliability, and a downstream decision (issue a refund of this amount) should be able to tell them apart."}]},{"t":"code","lang":"python","label":"Python","text":"state.set(\"amount\", \"4,050 INR\", turn=11, source=\"user\", supersedes=\"4,180 INR\")"},{"t":"h2","id":"extraction-is-a-model-call-so-it-has-an-error-rate","runs":[{"text":"Extraction is a model call, so it has an error rate"}]},{"t":"p","runs":[{"text":"Something has to populate the fields, and unless the user filled a form, that something is a model. Which means the state can be wrong, and unlike a wrong answer, a wrong state persists and shapes every subsequent turn."}]},{"t":"p","runs":[{"text":"Three defences."}]},{"t":"p","runs":[{"text":"Extract with a schema and validate.","b":true},{"text":" A date field that must parse, an amount that must match a currency pattern, an order id that must match your format and exist. Reject rather than store what fails."}]},{"t":"p","runs":[{"text":"Prefer explicit signals.","b":true},{"text":" A value the user typed is more reliable than one inferred from a paraphrase. Where the interface can capture something directly (a selected order, an attached file, a chosen option) take it from the interface rather than from the text."}]},{"t":"p","runs":[{"text":"Show the state to the user.","b":true},{"text":" This is the strongest defence and it is a product decision as much as an engineering one. A visible panel saying “Order ORD-88213 · refund to original method · 4,050 INR” lets a human catch the error the extractor made, in one glance, before it does any damage. Make it editable and you have turned an unreliable inference into a confirmed fact."}]},{"t":"h2","id":"where-it-goes-in-the-window","runs":[{"text":"Where it goes in the window"}]},{"t":"p","runs":[{"text":"At a fixed position, immediately before the recent turns and after the compacted middle. Fixed matters for two reasons: the model learns where to look within the request, and a stable position means the diff between two turns is small, which is friendlier to caching than a block that moves."}]},{"t":"p","runs":[{"text":"Label it plainly and say what it is:"}]},{"t":"code","lang":"","label":"Text","text":"<state>\nEstablished facts for this conversation. These override anything in the\nsummary below if they conflict.\n...\n</state>"},{"t":"p","runs":[{"text":"That conflict rule earns its tokens. Compaction will eventually produce a summary that contradicts a field, and stating the precedence resolves it deterministically instead of leaving it to the model."}]},{"t":"h2","id":"keep-it-small","runs":[{"text":"Keep it small"}]},{"t":"p","runs":[{"text":"State is not a database. If the block grows past a few hundred tokens it has stopped being a set of load-bearing facts and become a second transcript, with the same growth problem and none of the readability. Cap the number of entries per field, drop resolved open questions, and archive superseded values to storage rather than carrying them in the window."}]},{"t":"p","runs":[{"text":"A useful discipline: if you cannot read the whole state block in ten seconds, it is too big, and something in it is not load-bearing."}]},{"t":"h2","id":"the-pattern-generalises","runs":[{"text":"The pattern generalises"}]},{"t":"p","runs":[{"text":"This is the same idea as the manifest in the selection module and the handover note later in this one: "},{"text":"keep the load-bearing facts in a structure your code controls, and let the prose be prose.","b":true}]},{"t":"p","runs":[{"text":"Anything you would be unhappy to see paraphrased belongs in a field. Everything else can be summarised, dropped, or left to the transcript. Once you have drawn that line, compaction stops being frightening: the worst it can do is lose texture, and the facts are somewhere it cannot reach."}]}],"check":{"question":[{"text":"Why does storing an order id in a structured state field survive compaction when the same id inside a summary paragraph often does not?"}],"options":[{"text":[{"text":"State fields are placed nearer the question, so position bias protects them."}],"correct":false,"tempting":[{"text":"Position helps material be used, but it does not prevent a summariser from dropping a value during rewriting."}]},{"text":[{"text":"Field values are copied by code rather than rewritten by a language model, so nothing rounds them away."}],"correct":true,"tempting":null},{"text":[{"text":"Structured blocks are excluded from the history budget, so they are never compacted."}],"correct":false,"tempting":[{"text":"They consume budget like anything else; their protection comes from how they are produced, not from exemption."}]},{"text":[{"text":"Models attend more strongly to JSON than to prose, so the id is weighted higher."}],"correct":false,"tempting":[{"text":"Format affects legibility somewhat, but the id’s survival is decided before the model reads anything."}]}],"order":[0,1,2,3],"why":null},"prev":{"slug":"compaction-when-and-how","title":"Compacting without losing the thread","minutes":9},"next":{"slug":"memory-across-sessions","title":"Memory between conversations","minutes":9}}}