{"slug": "the-grading-agent-that-cannot-grade-and-the-four-bugs-that-taught-me-why-that-s", "title": "The grading agent that cannot grade, and the four bugs that taught me why that's hard", "summary": "A developer built an AI agent that prepares grading materials for student essays but cannot grade them, with grades stored in a separate database and write access denied. The project revealed four bugs, including a nonexistent model ID and a citation validation mismatch caused by prompt interleaving, highlighting the challenges of building reliable AI systems.", "body_md": "*I created this piece of content for the purposes of entering the All Things Agentic Hackathon.*\n\nMost AI agents are pitched on what they can do. This one is built around what it deliberately leaves out.\n\nI built an agent that reads student essays and prepares everything an instructor needs to grade them, and cannot grade them itself. The judgment is the professional's work, and I kept it that way on purpose, in the architecture, where a product update can't quietly take it back.\n\nNot \"declines to\". Cannot. There is no field on any record in the system that could hold a grade, the schema rejects fields it doesn't recognise, and grades live in a separate Firestore database that every pipeline service account is denied write access to.\n\nA separate collection, which is what I built first, would not have done it: Firestore IAM does not grant below the database, so a role scoped to \"the events collection\" is really a role over everything beside it. A reviewer caught that. It is the single most useful piece of feedback I got, and the fix, a second named database with the binding conditioned on the resource path, is the only part of this system where the security boundary is enforced by the platform rather than by my code being correct.\n\nThat was the easy part. Here is what actually went wrong.\n\nMy design document pinned gemini-3.5-pro for the analysis tier. Reasonable-sounding. It does not exist. The Gemini 3.5 family ships as Flash and Flash-Lite; the newest Pro-tier model is gemini-3.1-pro-preview.\n\nThe intuitive repair is the trap. \"Pro was specified, use the Pro that exists\" pins a 3.1 model, and the hackathon's mandatory requirement is Gemini 3.5 or newer. Capability tier satisfied. Version bar failed. Only one of those was being graded pass/fail, and the failure would have surfaced at the first live API call, which on my calendar was after the architecture was frozen.\n\nI pinned gemini-3.6-flash instead: newer, cheaper, and carrying the 1M context window my no-vector-database argument depends on. Then I wrote a preflight that resolves every pinned model ID against the live catalogue and fails loudly.\n\nA pinned model ID is a claim about the world. Check it like one.\n\nEvery citation my agent produces carries the text immediately before and after the quote. That sounds like metadata. It's the mechanism.\n\nConsider: a quote genuinely lifted from paragraph 12, attributed to paragraph 47, where the same phrase occurs in both. Is the span real? Yes. Does the quote occur in it? Yes. A validator built from \"does this span exist\" and \"does this text appear in it\", which is what \"check the citation\" usually means, accepts that citation and points an instructor at the wrong paragraph, in a document where one concedes a point and the other reverses it.\n\nWhat separates them is context. So the citation carries it, and the validator recomputes it.\n\nThen the first end-to-end run rejected almost everything.\n\nThe analyst sees the essay with span markers interleaved. The validator computed context from the frozen rendition, which has no markers. For a quote in the middle of a paragraph, both agree. For a quote near the start of one, the characters the model saw include the marker and the tail of the previous paragraph, while the ones the validator computed do not.\n\nEvery such citation was rejected as a misattribution. The retry produced the identical mismatch. In production this would have appeared as an unexplained escalation rate concentrated on first sentences, the sentences most likely to contain a thesis, which is the criterion an instructor most wants evidence for.\n\nWhen a model reports something that will be checked, the thing it sees and the thing the checker sees must be the same artifact. Interleaving anything into a prompt silently makes them different.\n\nI needed fifteen synthetic student essays that read like fifteen different people. My spec was explicit: generate them in separate passes, never reconciled. I did exactly that: fifteen independent generations, none able to see any other's output.\n\nThen I handed all fifteen to a reviewer who knew nothing about how they were made.\n\nAll fifteen took the same position on the prompt. Twelve of fifteen used the phrase \"a 2024 municipal broadband study\" verbatim. One aphorism appeared, restyled, in five. A twelve-word clause about a service appointment appeared verbatim in the two essays meant to be the most stylistically distant. Invented researcher surnames recombined from a pool of ten. The page number 41 anchored five different essays. The same fabricated author was male in one and female in another.\n\nAnd the observation that actually mattered: \"Each weak essay carries exactly one engineered lesion. Real weak student writing fails on four axes at once and in ways nobody designed.\"\n\nIndependent passes buy independence of context, not independence of prior. Fifteen blind samples from one distribution land on the mode fifteen times. My variation instruction pointed at style, so style is what varied; the argument underneath was never asked to move.\n\nThe fix was to allocate divergence rather than request it: assigned positions, disjoint invented source pools with non-overlapping page ranges, an explicit ban list of every shared phrase the reviewer found, distinct structural templates, and, for the weak papers, instructions to fail several ways at once. A second blind review confirmed the fix and found a third tier of collisions, which got repaired mechanically.\n\nThe residual homogeneity that remains is documented in the repository rather than hidden. Nobody in my corpus makes a factual error or misreads a source without noticing. Real composition classes produce both.\n\nEvery markdown file in the submissions directory was a submission. So MANIFEST.md, my own fixture documentation, was ingested, frozen into a rendition, given a span registry, analysed against all five rubric criteria, and rendered into the class overview as a student.\n\nIts evidence sheet was indistinguishable from a real one.\n\nA real instructor's folder contains the assignment sheet, the rubric, a syllabus excerpt, and whatever the LMS dropped in. Every one of them would have become a student, and the class overview's counts would have been wrong while looking authoritative.\n\nWhat caught it was a test asserting that exactly sixteen units were dispatched, against a hand-counted expectation. The more elegant assertion, \"every dispatched unit reaches a terminal state\", would have passed. MANIFEST reached one.\n\nThe one I'd have shipped\n\nThe verdict lint masks grade-shaped language before it reaches the screen. An early version flagged a student who wrote \"this policy is excellent.\"\n\nThat student was evaluating a policy. Not their essay. The lint was about to put a review chip on honest student prose for using an ordinary adjective about the thing they were arguing about.\n\nThe lint is now split by speaker. Generated text, the system talking, gets masked. A student's quoted words are never masked, only flagged, and only when the quality term attaches to their own work rather than to their subject.\n\nIts boundary is documented rather than discovered: paraphrase gets through. \"The writer has done what the assignment asked, and done it well\" expresses a level of quality and matches no pattern. The public challenge page says so, and there is a committed test asserting the known misses are still missed, so widening the patterns without updating the published claim fails CI.\n\nThat last part is the design principle underneath all four bugs. Four defensive layers, and the honest thing is to name which one is weakest. The schema holds. The IAM boundary holds. The lint is cosmetic, and a system that presents its flimsiest defence as its strongest is inviting exactly the attack that defeats it.\n\nTry it\n\nThe docket is live, and so is an arena where you can paste any essay and watch the full pipeline refuse to grade it:\n\n[https://karani-docket-u42sxjnqkq-uc.a.run.app](https://karani-docket-u42sxjnqkq-uc.a.run.app)\n\n[https://karani-arena-u42sxjnqkq-uc.a.run.app](https://karani-arena-u42sxjnqkq-uc.a.run.app)\n\n[https://github.com/Jeremiah-Sakuda/Karani](https://github.com/Jeremiah-Sakuda/Karani) (make demo runs everything offline, zero credentials)\n\nBuilt with Gemini 3.6 Flash and 3.5 Flash-Lite on Vertex AI, Google ADK, Gemma via Ollama, Cloud Run, Firestore, and Cloud Scheduler.", "url": "https://wpnews.pro/news/the-grading-agent-that-cannot-grade-and-the-four-bugs-that-taught-me-why-that-s", "canonical_source": "https://dev.to/jeremiah_sakuda/the-grading-agent-that-cannot-grade-and-the-four-bugs-that-taught-me-why-thats-hard-24mp", "published_at": "2026-08-31 23:16:10+00:00", "updated_at": "2026-08-31 23:53:06.176198+00:00", "lang": "en", "topics": ["artificial-intelligence", "ai-agents", "ai-tools", "developer-tools"], "entities": ["Gemini", "Firestore", "All Things Agentic Hackathon"], "alternates": {"html": "https://wpnews.pro/news/the-grading-agent-that-cannot-grade-and-the-four-bugs-that-taught-me-why-that-s", "markdown": "https://wpnews.pro/news/the-grading-agent-that-cannot-grade-and-the-four-bugs-that-taught-me-why-that-s.md", "text": "https://wpnews.pro/news/the-grading-agent-that-cannot-grade-and-the-four-bugs-that-taught-me-why-that-s.txt", "jsonld": "https://wpnews.pro/news/the-grading-agent-that-cannot-grade-and-the-four-bugs-that-taught-me-why-that-s.jsonld"}}