Repository navigation
fix: fail closed on unknown turn header keys and unconsumable queue output (#82, #83, #84) - #85
Merged
Merged
Conversation
…utput (#82, #83, #84) Three defects of one kind: a configuration or prompt mistake that produced no error, only a run that looked successful. An unrecognized per-turn header key became the first line of the prompt (#82). `tool_rounds: 12` set no budget and shipped as prose; a typo'd `toolset:` left the turn holding the previous turn's scope — the case where a turn written to drop a write tool keeps it. A declaration-shaped line naming an unrecognized key now fails the load, and the header ends at the first blank line so prompt text has an escape hatch. No agent under examples/ or dev/ changes meaning: all 75 load clean. Lifecycle `output: type: queue` enqueued an item with no hide (#83). A curing loads its item's hide first, so every item this route produced dead-lettered on first touch; there was no input for which it worked. It now stages the response in the hide store and enqueues a reference, as curing-to-curing chaining already did. The store is resolved through a function because serve registers agents before it builds the tannery. `require_tool:` closes the gap prose could not (#84): an acting turn no longer ends on a text response until one of its named tools is dispatched, and the run fails if the rounds run out first, rather than recording success over work that never happened. Unsatisfiable requirements are caught by `leather validate` and again before the first LLM call. Docs: turn-header key table, the require_tool contract, a shape-selection table, five authoring anti-patterns from live failures, and corrections to `hide_types` (does not filter chained artifacts) and `page_size_bytes` (a short page silently drops the tail a guard depends on).
Steps 3-5 read as chores that must produce a diff. Cutting v0.5.3 hit all three: no version strings are pinned outside the CHANGELOG, a patch on the current minor needs no SECURITY.md row, and Step 5 asked for a README commands table the README does not have — it carries a feature table and a routing table instead. The step that cannot be satisfied is the one that invites padding the release with edits to clear a box. They are now checks with an explicit no-op result, reported as one line. Step 5 also drops to the two tables a human maintains; doclint's export gate already covers docs/modules/cli.md. Step 1 sizes by impact rather than pattern-matching commit subjects, and an explicit version from the operator skips the table entirely. The old table made an additive opt-in key a MINOR on a technicality; leather's own history reserves minors for themed batches. Step 2 promotes an existing Unreleased section instead of rewriting prose written when the change was fresh.
TGPSKI
force-pushed
the
fix/turn-header-validation-and-queue-output
branch
from
August 9, 2026 09:19
4a2552f to
2c79aa4
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #82, closes #83, closes #84.
Three defects of one kind, all found by operating the reference tannery: a
configuration or prompt mistake that produced no error, only a run that looked
successful. Same class as v0.5.2.
#82 — an unrecognized turn header key became prompt text
parseTurnSectionrecognized four keys; the first line that was not one of themended the header and fell into the prompt, silently.
tool_rounds: 12set nobudget and shipped to the model as prose. A typo'd
toolset:left the turninheriting the previous turn's scope — the case where a turn written to drop a
write tool keeps it.
A header-position line shaped like a declaration (
^[a-z][a-z0-9_]*:) butnaming an unrecognized key now fails the load, naming the file, the turn, and
the recognized set.
The strict check needed an escape hatch, so the header now also ends at the
first blank line. Measured before relying on it: no agent in the repo separates
two declarations with a blank line, and no prompt would newly fail. All 75
*.agent.mdfiles underexamples/anddev/load clean. A URL opening aprompt (
http://…) is excluded explicitly.#83 — lifecycle
output: type: queueproduced items no curing could consumeThe route enqueued an item carrying only a payload, no
HideID. A curing'sfirst act is
LoadIntoBuffer(item.HideID), so every item hit "hide missing" anddead-lettered on first touch. There was no input for which it worked; the schema
accepted it and
leather validatepassed it. The curing-side equivalent(
dispatchQueue) had always written the hide first — the lifecycle route nevergot that step.
Runner.HideStoreFnis a function rather than a*hide.Storebecauseleather serveregisters scheduled agents before it constructs the tannery,while output routing runs long after startup; a pointer captured at registration
would always be nil. Where no store resolves, the route warns and enqueues
nothing rather than producing an item guaranteed to dead-letter.
#84 —
require_tool:, a turn that must act cannot end on textAn acting turn ends the moment the model produces text, and producing text
instead of acting is the failure mode: a fan-out agent composed five dispatch
blocks as its reply, called nothing, and recorded success while zero downstream
work queued. The only defense was a sentence in the prompt.
A text response arriving with none of the named tools called is refused, the
model is told to make the call, and the turn continues — the usual cause is a
model that composed the arguments as prose and needs telling to send them. If
the rounds run out first the run fails, naming the turn and the tool.
Settling the issue's open questions:
a tool that rejects its arguments has its own contract with the model.
Out-of-scope calls never execute, so they never count.
rejected by
leather validateand again at run start, before the first LLMcall — one comparison instead of every round.
Docs
docs/GUIDE.md: the turn-header key table, therequire_toolcontract, ashape-selection table (cron / curing / fan-out / fan-in), five authoring
anti-patterns drawn from live failures, and two corrections —
hide_typesis not a content filter for chained artifacts:dispatchQueuestamps a chained hide's kind with the upstream curing's name, so everything
from one curing arrives with the same kind. Already recorded in LEP-0008;
the GUIDE said otherwise.
page_size_bytessmaller than the document silently drops the tail, and aguard that requires the tail then refuses every item.
docs/modules/agent.mdanddocs/modules/runner.mdupdated; doclint's exportgate covers the new
TurnRequireFor.Verification
gofmt,go vet,golangci-lint run(0 issues),go test ./...all clean.go run ./scripts/doclint— 0 violations.require_toolparsing; runner tests for refuse-then-accept, never-calledfailure, and unsatisfiable-scope; the queue route test now asserts the item
references a readable hide, plus a no-store case asserting nothing is enqueued.