feat(budget): enforce a global portfolio token cap before the call, not after it (S3.4/F10)
PortfolioBudget + PortfolioMeter carry ONE token ledger over a whole portfolio pass -- and, seeded from a persisted spend file, across passes -- while the per-run Budget/TokenMeter pair is untouched. Three enforcement points, each doing a different job: - startup: a remainder that cannot fund one run raises BudgetRefused before anything loads (a pass that can afford zero projects is a caller mistake, not a result); - wave assembly: an unfundable project is NEVER STARTED and the pass stops structurally (budget_stop + stopped_early, completed runs preserved). Because every member of a wave is funded against the SAME pre-wave remainder, admission RESERVES each member's requirement -- otherwise a wave of k over-commits the cap by up to k runs; - pre-call: BudgetMiddleware refuses a call the remainder cannot pay for instead of making it. The post-charge check stays: real usage is only knowable after the response, so the guard stops the NEXT call, never the one in flight. budget_stop is its own field rather than a widened stop_reason -- a goal-stop is success, this is resource exhaustion, and fusing them would make "we stopped" unreadable. PortfolioMeter splits record/check so tokens the provider already billed reach the ledger even when the same charge breaks the run's own cap. read_spend raises on corrupt content (our own accounting state, unlike the tolerant RAW inbox layer); write_spend takes a REQUIRED stamp with no wall-clock default, mirroring promote_verdict. Load-bearing MEASURED, not asserted -- 6 mutations, all red: detach the wave check; detach the pre-call guard; detach the wave reservation; check the run cap before crediting the global ledger; detach the startup refusal; make read_spend tolerant. Files restored from shasum-verified copies after each. Two findings worth keeping: the pre-call guard MASKS a detached wave check if the test asserts on overspend (spend stays under the cap either way), so the load-bearing assertion had to become failures == () plus never-started; and the token arithmetic is probed (32 tokens/run at tokens=8), not guessed. 537 -> 553 tests, ruff + mypy green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015EaxFnaDAbMQkmTeX4u7sd
This commit is contained in:
parent
0d50ab89d3
commit
a831aa1e3b
6 changed files with 733 additions and 11 deletions
23
CLAUDE.md
23
CLAUDE.md
|
|
@ -105,6 +105,29 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
|
|||
(`tests/test_step8_promotion_loadbearing.py`): gaten avviser ikke-godkjent dom (RØD uten gate); godkjent
|
||||
dom er navigerbar (RØD når `link_in_index` detaches); promotert signal holdes ute av `bundle_context`
|
||||
(RØD når en beskrivende index-label lekker det inn). Index-RMW er ikke-atomisk (enprosess-MVP).
|
||||
- **Globalt token-tak håndheves FØR kall, aldri bare etterpå (S3.4, F10):** `PortfolioBudget` +
|
||||
`PortfolioMeter` er ÉN ledger over hele porteføljepasset (og — seedet av `read_spend` — på tvers
|
||||
av pass), mens per-run `Budget`/`TokenMeter` er uendret. Taket har tre tenner, med hver sin jobb:
|
||||
(1) **oppstartsnekt** — en rest som ikke kan finansiere én kjøring raiser `BudgetRefused` FØR noe
|
||||
lastes (et pass som har råd til null prosjekter er en caller-feil, ikke et resultat);
|
||||
(2) **wave-assembly** — et prosjekt som ikke kan finansieres blir ALDRI STARTET, og passet stopper
|
||||
strukturert (`budget_stop` + `stopped_early`, fullførte runs bevart). Aldri-startet er poenget:
|
||||
et ufinansiert prosjekt som bare avbrytes har allerede kostet kall. Fordi hele bølgen sjekkes mot
|
||||
SAMME før-bølge-rest, **reserverer** admission hver members krav — ellers overforplikter en bølge
|
||||
av k taket med inntil k kjøringer; (3) **pre-call-guard** i `BudgetMiddleware` — et kall resten
|
||||
ikke kan betale for NEKTES i stedet for å gjøres (post-charge-sjekken består: ekte usage kjennes
|
||||
først etterpå, så guarden stopper NESTE kall, aldri det som er i lufta). `budget_stop` er et EGET
|
||||
felt, aldri `stop_reason`: et mål-stopp er suksess, dette er ressurs-utmattelse — å slå dem sammen
|
||||
ville gjort «vi stoppet» uleselig. `record`/`check` er SPLITTET i `PortfolioMeter` fordi tokens
|
||||
leverandøren allerede har fakturert må nå ledgeren selv når samme charge bryter run-taket.
|
||||
Spend-fila er vår EGEN regnskapstilstand: `read_spend` raiser på korrupt innhold (kontrast det
|
||||
tolerante RAW-inbox-laget — å lese korrupt som null ville gitt tilbake et allerede brukt budsjett),
|
||||
og `write_spend` tar et PÅKREVD `stamp` uten wall-clock-default (byte-determinisme, speiler
|
||||
`promote_verdict`). `portfolio_meter` og `meter_factory` er gjensidig utelukkende — en
|
||||
factory-meter er ubundet, så begge sammen ville gitt et pass som SER capped ut uten å være det.
|
||||
Load-bearing MÅLT (`tests/test_portfolio_budget_loadbearing.py` + `tests/test_budget.py`), seks
|
||||
mutasjoner alle røde: detach wave-sjekken · detach pre-call-guarden · detach bølge-reservasjonen ·
|
||||
sjekk run-taket før global kreditering · detach oppstartsnekten · gjør `read_spend` tolerant.
|
||||
- **Kostnadsdisiplin:** utvikle primært på lokal profil (gratis); Foundry/Azure (privat tenant finnes) kun til målrettet, minimal verifisering; billigste modeller + små syntetiske data + harde token-tak. Ingen tunge test-kjøringer.
|
||||
- **Offline simulering = primært metode-bevis (kostnadsdrevet, erstatter §11.8):** operatøren kjører
|
||||
IKKE MAF mot ekte modell (verken Azure/Foundry eller Ollama — API for begge repoene er for kostbart
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue