Prompt economy: measure the prompt, and errors that name an alternative - #1321
Open
andreasronge wants to merge 2 commits into
Open
Prompt economy: measure the prompt, and errors that name an alternative#1321andreasronge wants to merge 2 commits into
andreasronge wants to merge 2 commits into
Conversation
The shipped system prompt is the one authored surface in the tree with no authoring budget. Runtime limits bound one run, not the source, and no check prices an edit: PR #1310 grew the instruction text twice, including the time it was repaired by deleting a clause, because there was nothing to measure with. prompt.audit is a new pure prelude taking a string. segments splits a rendering into its ordered emission segments and rejoins to the input exactly; measure sizes each one plus authored, dynamic and total rows; delta reports the per-segment character change between two versions. Recognition fails closed, because manifest text reaches the prompt unescaped. A docstring that reproduces an anchor, a duplicated API-notes heading, or an entry list truncated by even one character all yield one unrecognised segment rather than a guess. The notes segment ends at the last blank line before the legend rather than the first after the heading: a docstring may contain a blank line, and ending there would cut manifest text off mid-segment and report its remainder as authored — the one way a reporting row can state the opposite of the truth rather than approximate it. Because the input is a string, one implementation serves the REPL reading a recorded run, the tests guarding the committed artifacts, and the gate. Two artifacts are committed from a canonical fixture mission that renders every segment at once; the gate asserts recognition first, then a character ceiling on the final-turn total. It reads total rather than authored because the dynamic segments carry authored formatter text, so an authored ceiling would not move when render-entry grows. Registering the component changes the declared callable surface, so inspection-analysis-v2 becomes v3. AnalysisResources accepts the profile ID as a literal and would have failed every v3 capture with :invalid_analysis_resources had it been missed; the full set was re-derived by repository search rather than taken from a list, which also caught analysis_session_builder.ex. Line counting uses neither loop/recur nor a regex. Evaluation caps loops at a thousand iterations, and the regex runtime truncates its input at 32,768 bytes and undercounts silently rather than failing — both would have misreported exactly the large mission inventories most worth measuring, one loudly and one quietly. Sequence functions are bounded by neither, and the test counts lines outside the runtime so an undercount fails instead of rounding down into a plausible number. No prompt wording changes here — the instrument lands independently of the thing it will be used to measure. Verified with mix precommit: format, credo, duplication (new=0), spec validation, generated-artifact staleness and the root, Viewer and launcher suites.
A failed name lookup should take the highest rung it can reach: name one alternative verified to exist, list the bounded set the name must have come from, or point at search. The first two cost the model no turns and the third costs one, and no rung may print a name that was not resolved first. The hint ladder lived in format_closure_error/1, which bare symbols never reach: check_undefined_var_candidates/2 intercepts every statically visible undefined name before evaluation, so the model saw a bare "Undefined variable: x" and none of the ladder. The ladder now lives in one function that both producers call, which is what makes these changes observable at all. - Unresolved bare name: points at (apropos "name") and states that it searches prelude exports, not builtins, lexical bindings, or earlier definitions. - Underscore names: the hyphenated candidate is looked up in Env.initial/0 and suggested only when it resolves, then the verified fuzzy match, then the naming convention with no name asserted. map_indexed still yields map-indexed; zzz_qqq_www no longer yields zzz-qqq-www. - Unsupported Java class: lists admitted class names derived from the distinct class_ids of Surface.references/0, not Surface.class_spellings/0, which flat-maps inventory-only classes and constructor spellings such as "java.util.Date." — neither callable. - Java arity error on the analysis path: names a canonical admitted spelling and the accepted arities instead of the internal reference_id atom, resolving through fetch_reference/1 and, for direct-dot failures, member_family_source/1. A field called as a function has no admitted arity at all, so it is named as a field rather than reported as accepting an empty one. - Receiver-class mismatch: a type failure, not a lookup, so it names the member and stops. The receiver profile stays structured detail; :unsupported is not a type the model could act on. - @clojure_alternatives said import was unavailable because there is "no Java interop", which the admitted bounded surface contradicts. refer, defmacro, eval and read-string were missing entirely, so all four collected search advice for functionality absent by category. The format, re-find and re-seq entries are deleted: all three are builtins, so the entries were unreachable and asserted that three available functions were unavailable — and the re-find entry offered `grep`, which has never existed. A test now holds both halves of the invariant: every key is genuinely unbound, and every name the hints offer resolves. Authority refusals keep the carve-out: listing the tools a caller does hold is fine and already happens; listing what a denial hides is not. Asserted per clause rather than as a blanket rule. The rollback statement goes in agent.feedback rather than any evaluator condition. agent.feedback/success tells the model its definitions survived and no failure path stated the converse; only one of the three failure paths carries details.message, so a sentence written into a condition would reach the model on one path in three. Added to all three, with an integration test each. Residuals recorded, not fixed: runtime Java arity still falls through the generic clause, which needs overload lookup and receiver-inclusive arity normalisation rather than a message edit; lisp.ex's catch-all still renders a raw Elixir tuple through inspect/2; and dispatch.ex's unresolved-reference and kind-mismatch conditions remain untouched as internal invariants, since they signal a runtime bug rather than a model mistake. Verified with mix precommit, green including root, Viewer and launcher suites.
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.
Two slices, one commit each. Neither changes a word of the system prompt: the measuring instrument lands independently of the thing it will be used to measure.
Slice A —
feat(prompt): measure the promptpriv/preludes/kernel/agent.prompt.cljrenders the system prompt every agent gets, and it is the one authored surface in the tree with no authoring budget. Runtime limits (max_transcript_chars) bound one run, not the source. PR #1310 grew the instruction text twice — including the time it was repaired by deleting a clause — because there was nothing to measure with.New pure prelude
prompt.audit, all functions taking a string:segments— the ordered emission segments (marker,protocol,language,examples,api-heading,api-notes,api-legend,api-entries); joining them reproduces the input exactly.measure—{"rows" [...] "recognised?" bool}, one row per segment plus derivedauthored,dynamic,total.delta— per-segment character change between two renderings.Because the input is a string, one implementation serves the REPL reading a recorded run, the tests guarding the committed artifacts, and the gate.
Recognition fails closed. Manifest text reaches the prompt unescaped, so a docstring that reproduces an anchor, a duplicated
API notesheading, or an entry list truncated by even one character all yield a singleunrecognisedsegment rather than a guess.The gate reads
total, notauthored, on a pinned canonical fixture that renders all eight segments.authoredwould not move whenrender-entrygrows — exactly the change the gate exists to catch. The ceiling is stated with its reason in the test; raising it is allowed, raising it silently is not.Two corrections to the plan's own reckoning:
inspection-analysis-v2→v3bump. Re-deriving the list by repository search found 16, of which 13 needed changing —analysis_session_builder.exwas missing from the plan. The other three aredocs/plans/*historical records, left at v2 rather than rewritten.AnalysisResourceswas the silent one, as predicted: it pattern-matches the profile ID as a module-attribute literal, so a missed bump fails every v3 capture with:invalid_analysis_resources.Slice B —
fix(lisp): name an alternative in lookup errorsGiven a failed name lookup, take the highest rung reachable: name one alternative verified to exist, list the bounded set the name came from, or point at search. The first two cost the model no turns and the third costs one, and no rung may print a name that was not resolved first.
The plan's premise needed correcting. It places three of the six sites in
format_closure_error/1. Bare symbols never reach it —check_undefined_var_candidates/2intercepts every statically visible undefined name before evaluation. The hint ladder was effectively dead code for the model-facing case:map_indexedproduced a bareUndefined variable: map_indexedwith no hint at all. Fixing onlyformat_closure_errorwould have produced no observable change. The ladder now lives in one function both producers call.Undefined variable: x(apropos "x")and states it searches prelude exports — not builtins, lexical bindings, or earlier definitionsfoo_bar→foo-barunverifiedEnv.initial/0and suggested only when it resolves, then the verified fuzzy match, then the convention with no name assertedclass_ids ofSurface.references/0— notclass_spellings/0, which carries inventory-only classes and constructor spellings like"java.util.Date."Java reference integer_parse_int expects arity [1]unsupported Java member for receiver class:unsupportedis not a type the model could act on@clojure_alternativesimportdenied because there is "no Java interop"refer,defmacro,eval,read-stringaddedThe
format,re-findandre-seqentries are deleted: all three are builtins, so the entries were unreachable and asserted that three available functions were unavailable — and there-findentry offeredgrep, which has never existed in this dialect. A test now holds both halves of the invariant: every key is genuinely unbound, and every name the hints offer resolves.Authority refusals keep the carve-out — listing the tools a caller does hold is fine and already happens; listing what a denial hides is not. Asserted per clause, not as a blanket rule.
The rollback statement went into
agent.feedbackrather than any evaluator condition: only one of the three failure paths carriesdetails.message, so a sentence written into a condition would reach the model on one path in three. Added to all three with an integration test each.Codex review
Three rounds, the cap agreed for this work. Every round found something real.
Round 1's P1 was a correctness bug I had misfiled as the documented residual: a namespace docstring containing a blank line ended
api-notesearly and counted the remainder — manifest text — as authored. That is categorically worse than the residual, which only misattributes in the approximate direction. The notes segment now ends at the last blank line before the legend, which fixes it without the literal-matching the plan explicitly rejected.Round 3's finding was the sharpest: line counting used a regex, and the regex runtime truncates its input at 32,768 bytes and undercounts silently rather than failing. The round-2 test sat at 32,229 bytes — just under. Line counting now uses sequence functions, bounded by neither that limit nor the 1,000-iteration loop cap, and the test counts lines outside the runtime so an undercount fails instead of rounding down into a plausible number. Verified by reverting the implementation: the test fails 1275 vs 2053.
One finding was declined: naming the member in the runtime Java arity path. Codex is factually right that
details.reference_idis available there, but the plan scopes runtime arity out in writing — naming accepted arities needs overload lookup plus receiver-inclusive arity normalisation. Recorded as a residual, with the note that the member name is separable from the arity work.The last fix landed after the third round, so it carries
mix precommitand a reverted-implementation check rather than a fourth review.Documented residual
Recognition does not fully fail closed. The optional tool-envelope sentence is not validated, so arbitrary text inserted immediately before the map-type legend preserves every anchor, the ordering and the rejoin invariant, and is accepted as part of
api-legend. Reaching that state requires a manifest whose namespace docstrings end exactly at that position.Recorded rather than fixed because it cannot move the gated number: the gate reads
totalon a pinned fixture, and segment attribution does not enter that measurement. It can only mislead the reporting rows, which are already documented as approximate. Closing it would mean validating that span as exactly either the 82- or 254-character form — cheap, but it reintroduces literal-matching into the parser for a reporting-only benefit, which is the trade this plan already made once in the other direction.Verification
mix precommitgreen: format, compile, cycles, stable-CLI callers, credo (no issues), duplication (new=0), spec validation 100%, generated-artifact staleness, and the root, Viewer and launcher suites. No new:e2etests — neither slice reaches a provider.