fix(s7b): BudgetExceeded refuses instead of tracebacking, approaches land in exploration.json
Fiks-ordre 20260904T070930Z-8335015575-from-.claude. Two scoped defects the
S7b syretest observed and left unfixed (docs/2026-09-04-syretest-s7b-k2.md
§ 3.6):
1. `BudgetExceeded` (a RuntimeError, econ 56) could leave `--explore` as a
raw Python traceback from TWO raise sites: the exploration loop's own
round/token cap (`explore()`/`resume_exploration()`, uncaught in
`main()`'s exploration `try`), and `generate_via_llm`'s retry loop when a
mandate's own-proposal evaluation hits an unparseable reply (the full-run
dispatch's `except` tuple only knew `ValueError`/`FileNotFoundError`/
`ValidationError`). Both now end as `run refused: {exc}` on stderr, rc 1,
same shape as every other loader refusal in run.py.
2. `{run_id}-exploration.json` carried rounds/tool_calls/plan_reviews/
quick_validations but not the approaches the loop actually shaped — those
stood only in the stdout mandate announcement. `explore.trace_payload`
now takes a required `mandate` keyword and renders `mandate.approaches`
under an "approaches" key, so an operator reading the artefact days later
(the whole point of the async U12 door) can recover what the run decided
to evaluate without the terminal.
Fifteen mutations across three detach points, all red against the full
suite: the exploration-loop except clause (2 red), the full-run except
tuple (1 red), and the approaches rendering (1 red) — plus a control per
fix proving the happy path is unaffected. Green control 1295 passed / 5
skipped (supersett of S7b's 1290/5, 0 removed), golden
demo-transcript.stdout byte-unchanged (shasum -a 1 = ea8c534…), ruff +
mypy clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
6f8330cc61
commit
5d8844fef5
7 changed files with 479 additions and 26 deletions
|
|
@ -2838,6 +2838,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
exploration_trace = ExplorationTrace()
|
||||
exploration: ExplorationResult | None = None
|
||||
parked_now: PlanReviewParked | None = None
|
||||
budget_now: BudgetExceeded | None = None
|
||||
try:
|
||||
if resumed is not None:
|
||||
# The parked state, not argv, is what rebuilds the workflow: the graph has to match
|
||||
|
|
@ -2882,6 +2883,14 @@ def main(argv: list[str] | None = None) -> int:
|
|||
# than left to escape, because parking is what the operator ASKED for by giving
|
||||
# --checkpoint-dir; the artefact is where a machine reads that it happened.
|
||||
parked_now = parked_exc
|
||||
except BudgetExceeded as budget_exc:
|
||||
# A cap that fired is a refusal at this door, not a programming error — the CLI's
|
||||
# existing contract for every other loader mistake below (stderr + rc 1, never a
|
||||
# traceback), applied to the one raise this block did not yet catch (measured by
|
||||
# accident on K2, S7b's own uttalte grense). Caught here, one frame above every other
|
||||
# refusal, because ``explore()``/``resume_exploration()`` are the only two calls in
|
||||
# this block that can raise it — a handler placed lower would never see it.
|
||||
budget_now = budget_exc
|
||||
finally:
|
||||
# From a ``finally``, exactly as ``write_parse_failures`` is (Fase 1b, funn 1): the run
|
||||
# that most needs this evidence is the one a cap cut short, and that run returns
|
||||
|
|
@ -2895,8 +2904,16 @@ def main(argv: list[str] | None = None) -> int:
|
|||
exploration_trace,
|
||||
stop=exploration.stop if exploration is not None else None,
|
||||
completed=exploration is not None,
|
||||
mandate=exploration.mandate if exploration is not None else None,
|
||||
),
|
||||
)
|
||||
if budget_now is not None:
|
||||
# Same shape as every other refusal in this function: one line on stderr, rc 1, no
|
||||
# traceback. The artefact was already written by the ``finally`` above (``completed``
|
||||
# is ``False`` there, exactly as it is for a park) — this only decides what the
|
||||
# terminal says.
|
||||
print(f"run refused: {budget_now}", file=sys.stderr)
|
||||
return 1
|
||||
if parked_now is not None:
|
||||
outbox.write_plan_review(
|
||||
args.outbox_dir, args.run_id, payload=parked_payload(parked_now.parked)
|
||||
|
|
@ -3161,9 +3178,17 @@ def main(argv: list[str] | None = None) -> int:
|
|||
)
|
||||
),
|
||||
)
|
||||
except (ValueError, FileNotFoundError, ValidationError) as exc:
|
||||
except (ValueError, FileNotFoundError, ValidationError, BudgetExceeded) as exc:
|
||||
# Structured refusal (rc 1, no traceback) for the full-run path: run_project's fail-fast
|
||||
# loaders (contracts, load_dimension, outbox run_id guard) surface here as one clean line.
|
||||
# ``BudgetExceeded`` joined this tuple for fiks-ordre 20260904T070930Z: a mandate this run
|
||||
# is EVALUATING (whether commissioned via ``--explore`` or ``--mandate``) can still exhaust
|
||||
# the round/token cap inside ``generate_via_llm``'s retry loop, and ``_evaluate_mandate``
|
||||
# only swallows that mid-list — the FIRST approach hitting the cap re-raises by design
|
||||
# (``produced`` empty, "nothing honest to return"). Measured by accident on K2's syretest:
|
||||
# an unparseable proposer reply burned the round cap and the process tracebacked instead of
|
||||
# refusing, because this tuple did not yet know ``BudgetExceeded`` is a ``RuntimeError``,
|
||||
# not a ``ValueError``.
|
||||
print(f"run refused: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
kind = type(result.outcome).__name__
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue