feat(run): S10 del 1 — run-lag: §9-citations, artefakt-persistens, SDK-klient
TDD offline (RØD bekreftet før implementasjon): resolve_model (rolle->modell-id, ukjent profil feiler fail-fast), build_citations (eksakte char-spans, verdict- ekskludering, uncitable kontekst -> raise FØR spend), persist_run_artifacts (deterministiske bytes; validator/checker-avgjørelser speilet VERBATIM fra RunResult — §9 non-konflatering). Run-path-only, aldri importert av tester: SdkModelClient (claude-agent-sdk 0.2.110 verifisert mot installert pakke; max_turns=1, tools=[], max_budget_usd per kall; manglende usage -> None så §8-meteret feiler lukket) + run_s10 (kontrakter FØR klient §10, BudgetExceeded som strukturert stopp). 178/178 uten nøkkel; ruff+mypy --strict rene; fire detach-bevis røde -> revertert grønne. Live-kjøringen gjenstår (credential-gated). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
This commit is contained in:
parent
22bfc80dda
commit
0238507df4
5 changed files with 583 additions and 0 deletions
133
src/portfolio_optimiser_claude/run_s10.py
Normal file
133
src/portfolio_optimiser_claude/run_s10.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
"""S10 — the ONE live model run of the programme (D6). RUN-PATH ONLY.
|
||||
|
||||
Wires the offline-proven pipeline (S6–S9) to the real Claude Agent SDK client
|
||||
for a single controlled run on the micro-bundle: startup contracts validated
|
||||
fail-fast BEFORE the client is built (§10), citations proven citable BEFORE any
|
||||
spend (§9), the read-context navigated + experience-folded exactly as the
|
||||
offline suite proves (§3 Step 1), the loop bounded by the §8 meter AND the
|
||||
per-call USD cap, and the captured artifacts persisted for S11 with the cost
|
||||
logged. Never imported by the test suite.
|
||||
|
||||
Run: uv run python -m portfolio_optimiser_claude.run_s10
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from portfolio_optimiser_claude.artifacts import build_citations, persist_run_artifacts
|
||||
from portfolio_optimiser_claude.budget import BudgetExceeded, BudgetMeter
|
||||
from portfolio_optimiser_claude.contracts import load_contracts
|
||||
from portfolio_optimiser_claude.experience import (
|
||||
CandidateFeatures,
|
||||
VerdictStore,
|
||||
fold_experience,
|
||||
seed_store_from_bundle,
|
||||
)
|
||||
from portfolio_optimiser_claude.ir import load_validator_input
|
||||
from portfolio_optimiser_claude.loop import run_project
|
||||
from portfolio_optimiser_claude.okf import bundle_context, navigate_bundle
|
||||
from portfolio_optimiser_claude.provenance import Provenance
|
||||
from portfolio_optimiser_claude.sdk_client import SdkModelClient
|
||||
from portfolio_optimiser_claude.validator import Rejection
|
||||
|
||||
_DEFAULT_BUNDLE = Path(__file__).resolve().parents[2] / "shared" / "examples" / "bygg-energi-mikro"
|
||||
_PROPOSER_ROLE = "proposer"
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(description="S10: the single live run (hard caps, D6).")
|
||||
parser.add_argument("--bundle", type=Path, default=_DEFAULT_BUNDLE)
|
||||
parser.add_argument("--out", type=Path, default=Path("runs") / "s10")
|
||||
parser.add_argument("--max-rounds", type=int, default=12)
|
||||
parser.add_argument("--max-tokens", type=int, default=150_000)
|
||||
parser.add_argument("--max-budget-usd-per-call", type=float, default=0.25)
|
||||
parser.add_argument("--max-debate-rounds", type=int, default=3)
|
||||
parser.add_argument("--max-attempts", type=int, default=3)
|
||||
parser.add_argument("--top-k", type=int, default=3)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if not os.environ.get("ANTHROPIC_API_KEY"):
|
||||
# The bundled CLI resolves its own credentials (Claude Code login) when
|
||||
# no key is exported; an unauthenticated run fails on the FIRST call.
|
||||
print("note: ANTHROPIC_API_KEY not set — relying on the CLI's own credentials.")
|
||||
|
||||
# §10: ALL startup contracts schema-validated BEFORE any model client exists.
|
||||
contracts = load_contracts(
|
||||
data_source={"docs_dir": str(args.bundle), "top_k": args.top_k},
|
||||
termination={"max_rounds": args.max_rounds, "max_tokens": args.max_tokens},
|
||||
feedback={"decision": "approved", "rationale": "startup shape check (§10)"},
|
||||
)
|
||||
|
||||
# §9: prove the context citable BEFORE any spend; §3 Step 1: navigate + fold.
|
||||
concepts = navigate_bundle(args.bundle)
|
||||
citations = build_citations(concepts)
|
||||
ir_projection = load_validator_input(args.bundle)
|
||||
store = VerdictStore()
|
||||
seeded = seed_store_from_bundle(store, args.bundle)
|
||||
context = fold_experience(
|
||||
store,
|
||||
CandidateFeatures.from_proposal(ir_projection),
|
||||
bundle_context(args.bundle),
|
||||
contracts.data_source.top_k,
|
||||
)
|
||||
|
||||
meter = BudgetMeter(contracts.termination)
|
||||
client = SdkModelClient(
|
||||
contracts.model_map, max_budget_usd_per_call=args.max_budget_usd_per_call
|
||||
)
|
||||
print(
|
||||
f"S10 live run: bundle={args.bundle.name} seeded_verdicts={seeded} "
|
||||
f"caps: max_rounds={args.max_rounds} max_tokens={args.max_tokens} "
|
||||
f"max_budget_usd_per_call={args.max_budget_usd_per_call}"
|
||||
)
|
||||
try:
|
||||
result = run_project(
|
||||
client,
|
||||
context,
|
||||
meter=meter,
|
||||
max_debate_rounds=args.max_debate_rounds,
|
||||
max_attempts=args.max_attempts,
|
||||
default_project_id=ir_projection.project_id,
|
||||
)
|
||||
except BudgetExceeded as stop:
|
||||
# §8: the structured stop event — report it, never a silent hang.
|
||||
print(f"STOPPED by budget: {stop.kind} observed {stop.observed} > limit {stop.limit}")
|
||||
print(f"cost so far: {client.total_cost_usd:.6f} USD")
|
||||
return 3
|
||||
|
||||
provenance = Provenance(
|
||||
citations=citations,
|
||||
model=client.last_model or "unknown", # §9: the REAL id, neutral fallback
|
||||
role=_PROPOSER_ROLE,
|
||||
validator_decision=result.validator_decision,
|
||||
tokens_used=meter.tokens_used,
|
||||
)
|
||||
paths = persist_run_artifacts(
|
||||
args.out,
|
||||
run=result,
|
||||
provenance=provenance,
|
||||
termination=contracts.termination,
|
||||
tokens_used=meter.tokens_used,
|
||||
rounds_used=meter.rounds_used,
|
||||
cost_usd=round(client.total_cost_usd, 6),
|
||||
)
|
||||
outcome_kind = "rejected" if isinstance(result.outcome, Rejection) else "validated"
|
||||
print(
|
||||
f"result: validator={result.validator_decision} checker={result.checker_decision} "
|
||||
f"attempts={result.attempts} outcome={outcome_kind}"
|
||||
)
|
||||
print(
|
||||
f"usage: tokens={meter.tokens_used}/{contracts.termination.max_tokens} "
|
||||
f"rounds={meter.rounds_used}/{contracts.termination.max_rounds}"
|
||||
)
|
||||
print(f"model: {client.last_model} cost: {client.total_cost_usd:.6f} USD")
|
||||
for name, path in sorted(paths.items()):
|
||||
print(f"artifact: {name} -> {path}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue