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
133 lines
5.5 KiB
Python
133 lines
5.5 KiB
Python
"""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())
|