feat(explore): U4 kallsted 1 - --explore former mandatet, og sporet overlever taket (ORDRE 20260823T204216Z) [skip-docs]
Kallsted (1) + artefaktet (4) av oerkt 57s fire.
--explore "<prompt>" + --explore-config FILE i run.py: utforskningen kjoerer FOER
pipelinen og mandatet den former gaar rett inn i run_project(mandate=...). Flagget
er opt-in, og hver ting det ikke kan aere NEKTES ved navn:
- --explore-config uten --explore (--embedder-config-presedensen, ordrett)
- --explore uten --explore-config: CLI-en oppfinner ALDRI grenser, fordi
MagenticBuilders egen fallback er "ubegrenset"
- --explore + --mandate: TO KILDER TIL ETT MANDAT. Nektet, aldri slaatt sammen -
explore() tar objective fra prompten og hardkoder allow_own_proposals=True, saa
en sammenslaaing ville stille overskrevet tre felt operatoeren skrev selv.
Nekten NAVNGIR biblioteksdoera (seed_approaches), fordi C.6 doer 1 er et ekte
behov denne flaten ikke betjener
- --explore + --live-dry-run (motstrid), --explore uten --bundle-dir (leser
ingenting), --portfolio --explore (partisjonen)
- enable_plan_review=true nektes HER, ikke i explore(): ExplorationError er en
RuntimeError og ligger UTENFOR main()s (ValueError, FileNotFoundError,
ValidationError)-tuppel, saa den ville forlatt som traceback i stedet for rc 1
ExplorationTrace er en KALLER-EID akkumulator (funn-1-sinken, ett lag opp):
explore() raiser BudgetExceeded paa rundetaket og tokentaket fyrer fra middleware
midt i loepet - paa begge stier finnes ingen ExplorationResult, og C.2 krever at
artefaktet er lesbart uansett hvilken vakt som fyrte. ExplorationResult.ledger_log
BYGGES FRA akkumulatoren, aldri ved siden av (kø-(p)).
{run_id}-exploration.json skrives fra en finally (write_parse_failures-presedensen)
med rundene, plan-reviewene og quick_validate-dommene - de siste bor bevisst ikke i
ExplorationResult. `completed` er et eget felt: en stop: null som betyr BAADE
"avsluttet normalt" og "vi fikk aldri vite" er stillheten cost_baseline_anchored
ble paakrevd for aa lukke.
Load-bearing MAALT (tests/test_explore_callsites_loadbearing.py, 15 tester), ti
mutasjoner alle roede mot HELE suiten + groenn kontroll 990/5: detach sink-appenden
(1 roed) - andre liste for rundene (4 roede) - detach --mandate-nekten (1) - detach
--explore-config-nekten (1) - skriv artefaktet kun naar kjoeringen fullfoerte (1) -
detach mandate= inn i run_project (1) - slipp enable_plan_review gjennom (1) -
detach --bundle-dir-kravet (1) - detach --live-dry-run-nekten (1) - fjern --explore
fra portefoelje-partisjonen (1).
EN MUTASJON FALSIFISERTE TESTEN FOERST (repoets vakuoes-gate-klasse, syvende gang):
portefoelje-testen asserterte kun at meldingen nevnte --explore, og var groenn UTEN
partisjonen - kjoeringen falt da gjennom til "--explore requires --bundle-dir", som
nevner --explore ogsaa. To nekter som deler en delstreng; testen navngir naa
--portfolio.
Golden-transkriptet byte-uendret (ea8c534773acdbe41ae68f2c55724d69aaf8be4f).
mypy + ruff rene.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YRZhBJcxqTcqWyMW6hBttx
This commit is contained in:
parent
ddc33eed8b
commit
8ba824c96f
4 changed files with 897 additions and 21 deletions
|
|
@ -54,6 +54,15 @@ from portfolio_optimiser.datasource import (
|
|||
retrieve_chunks,
|
||||
)
|
||||
from portfolio_optimiser.dimension import Dimension, admits, load_dimension
|
||||
from portfolio_optimiser.explore import (
|
||||
ExplorationContract,
|
||||
ExplorationResult,
|
||||
ExplorationTrace,
|
||||
explore,
|
||||
exploration_notice,
|
||||
load_exploration_contract,
|
||||
trace_payload,
|
||||
)
|
||||
from portfolio_optimiser.generate import ParseFailure, generate_via_llm
|
||||
from portfolio_optimiser.ir import SavingsProposal
|
||||
from portfolio_optimiser.mandate import (
|
||||
|
|
@ -1411,6 +1420,27 @@ def main(argv: list[str] | None = None) -> int:
|
|||
"afterwards, one row per approach. Valid in both modes; in portfolio mode it applies to "
|
||||
"every project in the pass",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--explore",
|
||||
default=None,
|
||||
metavar="PROMPT",
|
||||
help="U4 opt-in: run a Magentic EXPLORATION over the knowledge base first and let it shape "
|
||||
"the mandate this run then evaluates. The exploration chooses which base to open and which "
|
||||
"directions are worth testing; every number it produces is still gated by the same "
|
||||
"deterministic validator, and the exploration itself writes nothing. REQUIRES "
|
||||
"--explore-config and --bundle-dir; refused together with --mandate (two sources of one "
|
||||
"mandate)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--explore-config",
|
||||
default=None,
|
||||
metavar="FILE",
|
||||
help="the exploration's bounds (JSON, fail-fast, REQUIRES --explore): max_rounds, "
|
||||
"max_tokens, max_stall_count, max_reset_count, max_plan_revisions, enable_plan_review. "
|
||||
"Every field is required and none has a default — an omitted bound would fall back to "
|
||||
"MAF's unbounded loop, not to something conservative. enable_plan_review must be false "
|
||||
"here: the synchronous review has no reviewer on this surface",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--mcp-config",
|
||||
default=None,
|
||||
|
|
@ -1551,6 +1581,8 @@ def main(argv: list[str] | None = None) -> int:
|
|||
"--semantic-retrieval": args.semantic_retrieval,
|
||||
"--embedder-config": args.embedder_config is not None,
|
||||
"--scripted-replies": args.scripted_replies is not None,
|
||||
"--explore": args.explore is not None,
|
||||
"--explore-config": args.explore_config is not None,
|
||||
}
|
||||
if any(report_forbidden.values()):
|
||||
print(
|
||||
|
|
@ -1590,6 +1622,12 @@ def main(argv: list[str] | None = None) -> int:
|
|||
"--outbox-dir": args.outbox_dir,
|
||||
"--run-id": args.run_id,
|
||||
"--live-dry-run": args.live_dry_run,
|
||||
# One exploration shapes ONE mandate against ONE knowledge base, and --bundle-dir (its
|
||||
# only source of bases here) is already single-project-only. Refusing it by NAME beats
|
||||
# letting it fall through to the --bundle-dir requirement below: an operator who wrote
|
||||
# --portfolio --explore has to hear which of the two is wrong.
|
||||
"--explore": args.explore,
|
||||
"--explore-config": args.explore_config,
|
||||
}
|
||||
offending = [name for name, value in single_only.items() if value]
|
||||
if offending:
|
||||
|
|
@ -1673,6 +1711,84 @@ def main(argv: list[str] | None = None) -> int:
|
|||
)
|
||||
return 1
|
||||
|
||||
# The exploration door (U4). Every refusal here is BY NAME and happens before anything runs,
|
||||
# for the reason the whole block above exists: a flag that cannot take effect is refused, never
|
||||
# silently ignored. Four of them, each closing a different way this could go quietly wrong.
|
||||
#
|
||||
# 1. --explore-config alone would be loaded and dropped (the --embedder-config case verbatim).
|
||||
# 2. --explore alone has no bounds, and the CLI may not invent them: EVERY ExplorationContract
|
||||
# field is required without a default precisely because MagenticBuilder's own fallback is
|
||||
# "unbounded", which is the one shape shared/method-spec.md §8 forbids outright.
|
||||
# 3. --explore + --mandate are TWO SOURCES OF ONE MANDATE. Refused rather than merged, and the
|
||||
# decision is deliberate: ``explore()`` takes the objective from the prompt and hardcodes
|
||||
# ``allow_own_proposals=True``, so composing them would silently overwrite three fields an
|
||||
# operator wrote by hand — the silent merge this repo's flag contract forbids. § C.6 door 1
|
||||
# (the expert's own hypotheses seeding the exploration) is a real need, and it is served by
|
||||
# the library API; the refusal names it rather than only forbidding.
|
||||
# 4. --explore + --live-dry-run contradict: the drill stops before the first model call and an
|
||||
# exploration IS model calls (the --scripted-replies precedent, same words).
|
||||
if args.explore_config is not None and args.explore is None:
|
||||
print(
|
||||
"run refused: --explore-config requires --explore (the bounds describe an exploration "
|
||||
"that would never run, so the config would be loaded and then ignored)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if args.explore is not None:
|
||||
if args.explore_config is None:
|
||||
print(
|
||||
"run refused: --explore requires --explore-config (an exploration's bounds are "
|
||||
"never defaulted — an omitted cap falls back to an unbounded loop, not to a "
|
||||
"conservative one)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if args.mandate is not None:
|
||||
print(
|
||||
"run refused: --explore and --mandate are two sources of one mandate. The "
|
||||
"exploration SHAPES a mandate (objective from the prompt, own proposals allowed), "
|
||||
"so merging would silently overwrite what you wrote. To seed an exploration with "
|
||||
"an expert's own hypotheses, use the library door: "
|
||||
"explore(..., seed_approaches=[Approach(...)])",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if args.live_dry_run:
|
||||
print(
|
||||
"run refused: --explore and --live-dry-run contradict each other (the drill stops "
|
||||
"before the first model call; an exploration is model calls) — pick one",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
if not args.bundle_dir:
|
||||
print(
|
||||
"run refused: --explore requires --bundle-dir (the exploration navigates knowledge "
|
||||
"bases, and with none configured it would spend its budget reading nothing)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
exploration_contract: ExplorationContract | None = None
|
||||
if args.explore_config is not None:
|
||||
try:
|
||||
exploration_contract = load_exploration_contract(args.explore_config)
|
||||
except (FileNotFoundError, ValidationError, ValueError) as exc:
|
||||
print(f"run refused: {exc}", file=sys.stderr)
|
||||
return 1
|
||||
if exploration_contract.enable_plan_review:
|
||||
# Refused HERE rather than left to ``explore()``, which refuses it too: ExplorationError
|
||||
# is a RuntimeError and therefore outside this CLI's (ValueError, FileNotFoundError,
|
||||
# ValidationError) refusal tuple, so it would leave as a traceback instead of the rc 1
|
||||
# line every other misconfiguration produces. The synchronous review (U13) needs a
|
||||
# reviewer that blocks the loop, and this surface has none to offer.
|
||||
print(
|
||||
"run refused: --explore-config sets enable_plan_review, but this surface has no "
|
||||
"reviewer to answer it (the run would stop at a review nobody can answer). The "
|
||||
"synchronous door is the library API: explore(..., plan_reviewer=...)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
|
||||
# The commission, loaded fail-fast BEFORE anything runs: a missing or malformed mandate is
|
||||
# REFUSED rather than degraded to "no mandate", because the settlement would then describe work
|
||||
# nobody ordered. Placed with the other refusals and ABOVE the scripted banner, for the same
|
||||
|
|
@ -1732,6 +1848,52 @@ def main(argv: list[str] | None = None) -> int:
|
|||
scripted_client_factory = scripted_factory(replies, [])
|
||||
print(_SCRIPTED_BANNER)
|
||||
|
||||
# U4: the exploration runs BEFORE the announcement, because what it produces IS the mandate the
|
||||
# announcement describes. Its own model calls are therefore un-announced — stated plainly
|
||||
# rather than papered over: the announcement's contract is that a COMMISSION is declared before
|
||||
# the work it commissions, and until the exploration returns there is no commission to declare.
|
||||
# ``exploration_notice`` is what covers the gap, printed the moment the loop is done.
|
||||
#
|
||||
# Every ExplorationError ``explore()`` can raise for a CONFIG reason is unreachable from here by
|
||||
# construction: both plan-review preconditions are refused above, and the duplicate-base-id
|
||||
# refusal needs two bases where this surface passes one. What can still escape — an unreadable
|
||||
# marked hypothesis, an exhausted budget — is the RUN failing, not the caller erring, and leaves
|
||||
# as it does for the debate today.
|
||||
if args.explore is not None:
|
||||
assert (
|
||||
exploration_contract is not None
|
||||
) # guarded above: --explore requires --explore-config
|
||||
exploration_trace = ExplorationTrace()
|
||||
exploration: ExplorationResult | None = None
|
||||
try:
|
||||
exploration = asyncio.run(
|
||||
explore(
|
||||
args.explore,
|
||||
contract=exploration_contract,
|
||||
bundle_dirs=(args.bundle_dir,),
|
||||
profile=args.profile,
|
||||
client_factory=scripted_client_factory,
|
||||
trace=exploration_trace,
|
||||
)
|
||||
)
|
||||
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
|
||||
# nothing. ``completed`` says which of the two happened, so a reader never has to infer
|
||||
# it from an absent ``stop``.
|
||||
if args.outbox_dir and args.run_id:
|
||||
outbox.write_exploration(
|
||||
args.outbox_dir,
|
||||
args.run_id,
|
||||
payload=trace_payload(
|
||||
exploration_trace,
|
||||
stop=exploration.stop if exploration is not None else None,
|
||||
completed=exploration is not None,
|
||||
),
|
||||
)
|
||||
print(exploration_notice(exploration))
|
||||
mandate = exploration.mandate
|
||||
|
||||
if mandate is not None:
|
||||
# The scope line reads the dimension config only to NAME it. A config that fails to load is
|
||||
# left unnamed here and refused a moment later by the dispatch below, which stays the single
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue